TVHeadend
TVHeadend is a TV streaming server and recorder for Linux, FreeBSD and Android supporting DVB-S, DVB-S2, DVB-C, DVB-T, ATSC, ISDB-T, IPTV, SAT>IP and HDHomeRun as input sources.
I am using TVHeadend with a single IPTV provider (EntertainTV, Deutsche Telekom), running on Ubuntu 16.04 LTS Server. I use the EPG of www.epgdata.com, a payed provider.
The ease of use with Kodi as a client leaves my two proprietary tv receiver almost unused, although just a limited set of programs is available on unencrypted iptv. (Didnât start one of those receiver up this year, and itâs already February.)
Currently connected are three RaspberryPi 3 and one Pi 2Vero 4k, all of them running with LibreElec osmc. Optionally a connection is be made from Kodi running on Windows 10 on a PC.
TVHeadend provides a JSON API to interfere with, so today i started some basic integration of TVHeadend into openHAB. The integration is done via http binding, JSON Exec binding, curl, jq and JavaScript transformations and rules. You still need the Jsonpath transformation for some of the rules.
Known bugs
- i just discovered the two time items do not work anymore. (recordings until / next recording start). These will be fixed sometime
Features
- show TVHeadend release (no use, really
)
- current subscriptions (am i allowed to restart tvheadend server?)
- count & size of finished recordings (do i need to clean up the recordings?)
- messaging if a recording is failed or finished (should i have cleaned up the recordings)
- show the latest finish of the current recordings
- show the earliest start of the upcoming recordings
Changes
- no more cron trigger, every change is resolved via http binding
- optimized items, no more extreme large strings
- corrected the size of the recordings, just the 50 first were counted
- added latest current & earliest upcoming recording timestamp
- completely removed http binding and jsonpath transformation from the items, everything is resolved with the exec binding
- split subscritions into recordings & live tv
Outlook
I plan to extend to:
- delete watched recordings (currently i am not sure, if that is possible, because the watch count is set when a recording has been started to watch, not after finishing. Watch positions are not updated
)
- add channels for ip cameras
Shell Script
The shell script uses curl & jq to download and parse the json file. My complete path is /etc/openhab2/scripts/shell/jsonpathtransform.sh if you put it somewhere else, please adjust the Things accordingly.
#!/bin/sh
USER=kodi
PASS=kodi
SERVER=http://10.10.10.33:9981
API=$1
JSONPATH=$2
curl -s -u $USER:$PASS $SERVER$API | jq -M -c $JSONPATH | paste -sd, | sed 's/"//g'
Things
Thing exec:command:TVH_VERSION_THING "TVH_VERSION" @ "TVH" [command="/etc/openhab2/scripts/shell/jsonpathtransform.sh /api/serverinfo .sw_version",interval=150]
Thing exec:command:TVH_SUBSCRIPTION_COUNT_THING "TVH_SUBSCRIPTION_COUNT" @ "TVH" [command="/etc/openhab2/scripts/shell/jsonpathtransform.sh /api/status/subscriptions?limit=0 .totalCount",interval=150]
Thing exec:command:TVH_FAILED_COUNT_THING "TVH_FAILED_COUNT" @ "TVH" [command="/etc/openhab2/scripts/shell/jsonpathtransform.sh /api/dvr/entry/grid_failed?limit=0 .total",interval=150]
Thing exec:command:TVH_FINISHED_COUNT_THING "TVH_FINISHED_COUNT" @ "TVH" [command="/etc/openhab2/scripts/shell/jsonpathtransform.sh /api/dvr/entry/grid_finished?limit=0 .total",interval=150]
Thing exec:command:TVH_UPCOMING_COUNT_THING "TVH_UPCOMING_COUNT" @ "TVH" [command="/etc/openhab2/scripts/shell/jsonpathtransform.sh /api/dvr/entry/grid_upcoming?limit=0 .total",interval=150]
Thing exec:command:TVH_RECORDING_STATE_THING "TVH_RECORDING_STATE" @ "TVH" [command="/etc/openhab2/scripts/shell/jsonpathtransform.sh /api/dvr/entry/grid_upcoming?limit=1000 .entries[]|select(.sched_status==\"recording\")|.uuid",interval=150]
Thing exec:command:TVH_SUBSCRIPTION_STATE_THING "TVH_SUBSCRIPTION_STATE" @ "TVH" [command="/etc/openhab2/scripts/shell/jsonpathtransform.sh /api/status/subscriptions?limit=1000 .entries[].start",interval=150]
Items
In order to increase the maximum of 5 description displayed, add another DESCRIPTION-String and another VISIBLE-Switch.
String TVHE_VERSION "version [%s]" <info> {channel="exec:command:TVH_VERSION_THING:output"}
String TVHE_SUBSCRIPTIONS_COUNT "subscriptions [%s]" <info> {channel="exec:command:TVH_SUBSCRIPTION_COUNT_THING:output"}
String TVHE_FINISHED_COUNT "finished [%s]" <info> {channel="exec:command:TVH_FINISHED_COUNT_THING:output"}
String TVHE_FAILED_COUNT "failed [%s]" <info> {channel="exec:command:TVH_FAILED_COUNT_THING:output"}
String TVHE_UPCOMING_COUNT "upcoming [%s]" <info> {channel="exec:command:TVH_UPCOMING_COUNT_THING:output"}
String TVHE_RECORDING_STATE {channel="exec:command:TVH_RECORDING_STATE_THING:output"}
String TVHE_SUBSCRIPTION_STATE {channel="exec:command:TVH_SUBSCRIPTION_STATE_THING:output"}
DateTime TVHE_UPCOMING_NEXT_START "next recording [%1$td.%1$tm %1$tH:%1$tM]" <next>
DateTime TVHE_UPCOMING_CURR_STOP "currently until [%1$td.%1$tm %1$tH:%1$tM]" <record>
// virtual items managed by rules
Number TVHE_FINISHED_SIZE "TVH Finished size [%.1f MB]" (gRestoreOnStartup,gPersistNumber)
Number TVHE_LIVETV_COUNT "Viewing TV [%d]" <kodi>
Number TVHE_RECORDING_COUNT "Recording TV [%d]" <record>
String TVHE_LIVETV_0_DESCRIPTION " [%s]" <kodi>
String TVHE_LIVETV_1_DESCRIPTION " [%s]" <kodi>
String TVHE_LIVETV_2_DESCRIPTION " [%s]" <kodi>
String TVHE_LIVETV_3_DESCRIPTION " [%s]" <kodi>
String TVHE_LIVETV_4_DESCRIPTION " [%s]" <kodi>
Switch TVHE_LIVETV_0_VISIBLE
Switch TVHE_LIVETV_1_VISIBLE
Switch TVHE_LIVETV_2_VISIBLE
Switch TVHE_LIVETV_3_VISIBLE
Switch TVHE_LIVETV_4_VISIBLE
String TVHE_RECORDING_0_DESCRIPTION " [%s]" <recording>
String TVHE_RECORDING_1_DESCRIPTION " [%s]" <recording>
String TVHE_RECORDING_2_DESCRIPTION " [%s]" <recording>
String TVHE_RECORDING_3_DESCRIPTION " [%s]" <recording>
String TVHE_RECORDING_4_DESCRIPTION " [%s]" <recording>
Switch TVHE_RECORDING_0_VISIBLE
Switch TVHE_RECORDING_1_VISIBLE
Switch TVHE_RECORDING_2_VISIBLE
Switch TVHE_RECORDING_3_VISIBLE
Switch TVHE_RECORDING_4_VISIBLE
Transformations
getLatestRecordTVH.js
(function(x){
var json = JSON.parse(x);
var i;
var iLength = json.entries.length;
var index = -1;
var latest = -1;
for (i = 0; i < iLength; i++) {
if(json.entries[i].stop_real > latest)
{
index = i;
latest = json.entries[i].stop_real
}
}
return "" + index + "";
})(input)
getSizeOfRecordsTVH.js
(function(x){
var json = JSON.parse(x);
var i;
var iLength = json.entries.length;
var totalSize = 0;
for (i = 0; i < iLength; i++) {
totalSize = totalSize + (json.entries[i].filesize / 1024 / 1024);
}
return totalSize;
})(input)
getUpcomingRecordsTVH.js
(function(x){
var json = JSON.parse(x);
var i;
var iLength = json.entries.length;
var earliestscheduled = -1;
var earliestscheduled_idx = -1;
var latestrunning = -1
var latestrunning_idx = -1
for (i = 0; i < iLength; i++) {
if (json.entries[i].sched_status=="scheduled" && (json.entries[i].start_real<earliestscheduled || earliestscheduled==-1))
{
earliestscheduled=json.entries[i].start_real;
earliestscheduled_idx=i;
}
if (json.entries[i].sched_status=="recording" && (json.entries[i].stop_real>latestrunning || latestrunning==-1))
{
latestrunning=json.entries[i].stop_real;
latestrunning_idx=i;
}
}
return latestrunning_idx+","+earliestscheduled_idx;
})(input)
Transformation Map
Please put a file in the transform folder named kodihost.map. You need a line for each Kodi.
<ip-adress>=<Display Name of Kodi>
Rules
If you want to use a different messaging service, just exchange the lines with sendTelegram
. You need to exchange the server string with your connection information.
val String logger = "tvheadend"
val String server = "http://kodi:kodi@10.10.10.33:9981"
rule "tvh subscriptions"
when
Item TVHE_SUBSCRIPTIONS_COUNT changed or
Item TVHE_SUBSCRIPTION_STATE changed
then
var String title = ""
var String group = ""
val String subscriptions = sendHttpGetRequest(server + "/api/status/subscriptions")
var Number count = Integer::parseInt(transform("JSONPATH", "$.totalCount", subscriptions))
var Number i=0
var Number cntLTV = 0
var Number cntRec = 0
var Number idx=0
while (i < count)
{
title = transform("JSONPATH", "$.entries[" + i + "].title", subscriptions)
if (!title.startsWith("DVR:"))
{
var String[] servicearr = transform("JSONPATH", "$.entries[" + i + "].service", subscriptions).split("/")
var String hostname = transform("MAP","kodihost.map",transform("JSONPATH", "$.entries[" + i + "].hostname", subscriptions))
title = hostname + ": " + servicearr.get(servicearr.length-1)
group = "LIVETV"
idx = cntLTV
cntLTV = cntLTV + 1
}
else
{
title = transform("JSONPATH", "$.entries[" + i + "].title", subscriptions) + " / " + transform("JSONPATH", "$.entries[" + i + "].channel", subscriptions)
group = "RECORDING"
idx = cntRec
cntRec = cntRec + 1
}
if(idx<5)
{
postUpdate("TVHE_" + group + "_" + idx + "_VISIBLE" , "ON")
postUpdate("TVHE_" + group + "_" + idx + "_DESCRIPTION" , title)
}
i=i+1
}
TVHE_LIVETV_COUNT.postUpdate(cntLTV)
TVHE_RECORDING_COUNT.postUpdate(cntRec)
while (cntLTV < 5)
{
postUpdate("TVHE_LIVETV_" + cntLTV + "_VISIBLE" , "OFF")
postUpdate("TVHE_LIVETV_" + cntLTV + "_DESCRIPTION" , "")
cntLTV = cntLTV + 1
}
while (cntRec < 5)
{
postUpdate("TVHE_RECORDING_" + cntRec + "_VISIBLE" , "OFF")
postUpdate("TVHE_RECORDING_" + cntRec + "_DESCRIPTION" , "")
cntRec = cntRec + 1
}
end
rule "finished recording"
when
Item TVHE_FINISHED_COUNT changed
//or Time cron "0 */1 * * * ? *"
then
var String msg = ""
var String idx = ""
if (TVHE_FINISHED_COUNT.state.toString != "0")
{
val String finishedGrid = sendHttpGetRequest(server + "/api/dvr/entry/grid_finished?limit=" + TVHE_FINISHED_COUNT.state.toString)
idx = transform("JS", "getLatestRecordTVH.js", finishedGrid)
TVHE_FINISHED_SIZE.postUpdate(Double::parseDouble(transform("JS", "getSizeOfRecordsTVH.js", finishedGrid)))
msg = String::format("FINISHED: %s - %s", transform("JSONPATH", "$.entries[" + idx + "].disp_title", finishedGrid), transform("JSONPATH", "$.entries[" + idx + "].disp_subtitle", finishedGrid))
sendTelegram("HAL", msg)
}
end
rule "failed recording"
when
Item TVHE_FAILED_COUNT changed
//or Time cron "0 */1 * * * ? *"
then
var String msg = ""
var String idx = ""
if (TVHE_FAILED_COUNT.state.toString != "0")
{
val String failedGrid = sendHttpGetRequest(server + "/api/dvr/entry/grid_failed?limit=" + TVHE_FAILED_COUNT.state.toString)
idx = transform("JS", "getLatestRecordTVH.js", failedGrid)
msg = String::format("FAILED: %s - %s", transform("JSONPATH", "$.entries[" + idx + "].disp_title", failedGrid), transform("JSONPATH", "$.entries[" + idx + "].disp_subtitle", failedGrid) )
sendTelegram("HAL", msg)
}
end
rule "upcoming recording"
when
Item TVHE_UPCOMING_COUNT changed or
Item TVHE_RECORDING_STATE changed
then
// var String msg = ""
//$.entries[?(@.sched_status=='recording')]
if (TVHE_UPCOMING_COUNT.state.toString != "0")
{
val String upcomingGrid = sendHttpGetRequest(server + "/api/dvr/entry/grid_upcoming?limit=" + TVHE_UPCOMING_COUNT.state.toString)
var String[] idxs = transform("JS", "getUpcomingRecordsTVH.js", upcomingGrid).split(",")
// logInfo(logger,idxs.get(0)+","+idxs.get(1))
if (idxs.get(0) != "-1")
{
var DateTime curr_stop = new DateTime(Long::parseLong(transform("JSONPATH", "$.entries[" + idxs.get(0) + "].stop_real", upcomingGrid)) * 1000L)
TVHE_UPCOMING_CURR_STOP.postUpdate(new DateTimeType(curr_stop.toString))
}
else
{
logInfo(logger,"NO CURRENT RECORDING")
//TVHE_UPCOMING_CURR_STOP.postUpdate(new DateTimeType(UndefType.NULL))
}
if (idxs.get(1) != "-1")
{
var DateTime next_start = new DateTime(Long::parseLong(transform("JSONPATH", "$.entries[" + idxs.get(1) + "].start_real", upcomingGrid)) * 1000L)
TVHE_UPCOMING_NEXT_START.postUpdate(new DateTimeType(next_start.toString))
}
else
{
logInfo(logger,"NO UPCOMING RECORDING")
//TVHE_UPCOMING_NEXT_START.postUpdate(new DateTimeType(UndefType.NULL))
}
}
end
Sitemap fragment
As before, to increase the number of subscriptions visible to more than 5, add more descriptions with visibility.
Frame label="TV Headend"
{
Text label="TV Headend" icon="tvheadend"
{
Frame label="Server"
{
Text item=TVHE_VERSION label="TVH Version [%s]" icon="tvheadend"
Text item=TVHE_SUBSCRIPTIONS_COUNT
Text item=TVHE_FAILED_COUNT
}
Frame item=TVHE_LIVETV_COUNT label="Viewing TV [%s]" visibility=[TVHE_LIVETV_0_VISIBLE==ON,TVHE_LIVETV_1_VISIBLE==ON,TVHE_LIVETV_2_VISIBLE==ON,TVHE_LIVETV_3_VISIBLE==ON,TVHE_LIVETV_4_VISIBLE==ON]
{
Text item=TVHE_LIVETV_0_DESCRIPTION icon="kodi" visibility=[TVHE_LIVETV_0_VISIBLE==ON]
Text item=TVHE_LIVETV_1_DESCRIPTION icon="kodi" visibility=[TVHE_LIVETV_1_VISIBLE==ON]
Text item=TVHE_LIVETV_2_DESCRIPTION icon="kodi" visibility=[TVHE_LIVETV_2_VISIBLE==ON]
Text item=TVHE_LIVETV_3_DESCRIPTION icon="kodi" visibility=[TVHE_LIVETV_3_VISIBLE==ON]
Text item=TVHE_LIVETV_4_DESCRIPTION icon="kodi" visibility=[TVHE_LIVETV_4_VISIBLE==ON]
}
Frame item=TVHE_RECORDING_COUNT label="Recordings [%s]" visibility=[TVHE_RECORDING_0_VISIBLE==ON,TVHE_RECORDING_1_VISIBLE==ON,TVHE_RECORDING_2_VISIBLE==ON,TVHE_RECORDING_3_VISIBLE==ON,TVHE_RECORDING_4_VISIBLE==ON]
{
Text item=TVHE_UPCOMING_CURR_STOP visibility=[TVHE_RECORDING_0_VISIBLE==ON]
Text item=TVHE_RECORDING_0_DESCRIPTION icon="record" visibility=[TVHE_RECORDING_0_VISIBLE==ON]
Text item=TVHE_RECORDING_1_DESCRIPTION icon="record" visibility=[TVHE_RECORDING_1_VISIBLE==ON]
Text item=TVHE_RECORDING_2_DESCRIPTION icon="record" visibility=[TVHE_RECORDING_2_VISIBLE==ON]
Text item=TVHE_RECORDING_3_DESCRIPTION icon="record" visibility=[TVHE_RECORDING_3_VISIBLE==ON]
Text item=TVHE_RECORDING_4_DESCRIPTION icon="record" visibility=[TVHE_RECORDING_4_VISIBLE==ON]
}
Frame item=TVHE_FINISHED_COUNT label="Finished [%s]" visibility=[TVHE_FINISHED_COUNT!="0"]
{
Text item=TVHE_FINISHED_COUNT
Text item=TVHE_FINISHED_SIZE
}
Frame item=TVHE_UPCOMING_COUNT label="Upcoming [%s]" visibility=[TVHE_UPCOMING_COUNT!="0"]
{
Text item=TVHE_UPCOMING_COUNT
Text item=TVHE_UPCOMING_NEXT_START
}
Frame item=TVHE_FAILED_COUNT label="Failed [%s]" visibility=[TVHE_FAILED_COUNT!="0"]
}
}
Comments & ideas welcome
Please feel free to add your comments or ideas.