TVHeadend integration (basic for now)

streaming
tvheadend
Tags: #<Tag:0x00007f1829226c60> #<Tag:0x00007f1829226940>

(Joachim Boeddeker) #1

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 :wink: )
  • 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 :frowning: )
  • 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. :wink:


(horschte) #2

Hello, how about detailed TV guide/info from the current playing channel?
the api url is http://tvheadend:9981/api/epg/events/grid

Best regards!


(Roque Strongo) #3

Hey there nice work. I´m trying to replicate it on my OH.

Could you short explain how the login credentials need to be entered here:

User: myuser
PW: mypassword

Authorization=Basic a29kaTprb2Rp

Greetz

Roque


(Joachim Boeddeker) #4

I couldn’t explain better as in the documentation of the http binding:

If you are running openHAB 2.3 and up, you will run into some problems due to the crippled Jsonpath transformation. I exchanged this where neccessary to the exec binding, curl & jq. (Current subscriptions & recordings are not displayed.)

I will post the updates shortly. (Looks like you are the first one trying to replicate it.)


(Joachim Boeddeker) #5

:smiley:

No. At least not from my side. openHAB is not able to provide that.

  • no Lists widgets are existing
  • no List Items are existing

(Roque Strongo) #6

Thanks for your fast reply,
i got it working now thanks to your hint, didn´t know about Base64 encoding.

It didn´t work at first, i had to change TV Headends Authentication Type (Configuration > General > Base) to “Plain and digest”.

Yes I´m running OH 2.3. I´m running TVH with a TBS Satelite Tuner. Here are some of my outputs:

http://192.168.1.5:9981/api/status/subscriptions?limit=0

delivers:

{"entries":[{"id":1186,"start":1538050774,"errors":0,"state":"Running","title":"epggrab","service":"TBS 6985 DVBS/S2 frontend - TUNER4/192E/11493.75H/Raw PID Subscription","in":2068,"out":0,"total_in":4700,"total_out":0}],"totalCount":1}

In Karaf i see:

[WARN ] [nternal.JSonPathTransformationService] - JsonPath expressions with more than one result are not allowed, please adapt your selector. Result: [1538050497,1538050512]

TVHE_SUBSCRITPION_0_DESCRITPION changes to something like:

{"entries":[{"id":1186,"start":1538050774,"errors":0,"state":"Läuft","title":"epggrab","service":"TBS 6985 DVBS/S2 frontend - TUNER4/192E/11493.75H/Raw PID Subscription

Greetz

Roque


(Joachim Boeddeker) #7

I updated the post to my current 2.3 compatible version. Do you use the current version?


(Roque Strongo) #8

Just saw the change, i will replicated it tonight and report back.

Greetz

Roque