TVHeadend integration (basic for now)

streaming
tvheadend
Tags: #<Tag:0x00007f6cf03c39f8> #<Tag:0x00007f6cf03c3688>

(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 2, all of them running with LibreElec. 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 and JavaScript transformations and rules.

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

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: )
  • split subscritions into recordings & live tv
  • add channels for ip cameras

Items

You need to replace 10.10.10.33 with your server and <a29kaTprb2Rp> with the appropriate user and password to connect to your server (base64 encoded). Please have a look at the http binding documentation for an explanation of the authorization
In order to increase the maximum of 5 description displayed, add another DESCRIPTION-String and another VISIBLE-Switch.

// real items
String TVHE_VERSION  "TVH Version [%s]"                     { http="<[http://10.10.10.33:9981/api/serverinfo{Authorization=Basic a29kaTprb2Rp}:60000:JSONPATH(['sw_version'])]" }
String TVHE_SUBSCRIPTIONS_COUNT  "TVH Subscriptions [%s]"   { http="<[http://10.10.10.33:9981/api/status/subscriptions?limit=0{Authorization=Basic a29kaTprb2Rp}:60000:JSONPATH(['totalCount'])]" }
String TVHE_SUBSCRIPTIONS  "TVH Subscriptions [%s]"   { http="<[http://10.10.10.33:9981/api/status/subscriptions?limit=0{Authorization=Basic a29kaTprb2Rp}:60000:JSONPATH($.entries[*].start)]" }
String TVHE_FAILED_COUNT "TVH Failed recordings [%s]"       { http="<[http://10.10.10.33:9981/api/dvr/entry/grid_failed?limit=0{Authorization=Basic a29kaTprb2Rp}:60000:JSONPATH(['total'])]" }
String TVHE_FINISHED_COUNT "TVH Finished recordings [%s]"   { http="<[http://10.10.10.33:9981/api/dvr/entry/grid_finished?limit=0{Authorization=Basic a29kaTprb2Rp}:60000:JSONPATH(['total'])]" }
String TVHE_UPCOMING_COUNT "TVH Upcoming recordings [%s]"   { http="<[http://10.10.10.33:9981/api/dvr/entry/grid_upcoming?limit=0{Authorization=Basic a29kaTprb2Rp}:60000:JSONPATH(['total'])]" }

String TVHE_RECORDINGS_CURRENT "TVH Current recordings [%s]"   { http="<[http://10.10.10.33:9981/api/dvr/entry/grid_upcoming?limit=1000{Authorization=Basic a29kaTprb2Rp}:60000:JSONPATH($.entries[?(@.sched_status=='recording')].uuid)]" }
DateTime TVHE_UPCOMING_NEXT_START "TVH next recording start [%1$td.%1$tm %1$tH:%1$tM]"
DateTime TVHE_UPCOMING_CURR_STOP "TVH current recording stop [%1$td.%1$tm %1$tH:%1$tM]"

// virtual items managed by rules

Number TVHE_FINISHED_SIZE "TVH Finished size [%.1f MB]" (gRestoreOnStartup,gPersistNumber) 

String TVHE_SUBSCRIPTION_0_DESCRIPTION  " [%s]" 
String TVHE_SUBSCRIPTION_1_DESCRIPTION  " [%s]" 
String TVHE_SUBSCRIPTION_2_DESCRIPTION  " [%s]" 
String TVHE_SUBSCRIPTION_3_DESCRIPTION  " [%s]" 
String TVHE_SUBSCRIPTION_4_DESCRIPTION  " [%s]" 

Switch TVHE_SUBSCRIPTION_0_VISIBLE 
Switch TVHE_SUBSCRIPTION_1_VISIBLE 
Switch TVHE_SUBSCRIPTION_2_VISIBLE 
Switch TVHE_SUBSCRIPTION_3_VISIBLE 
Switch TVHE_SUBSCRIPTION_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)

Rules

In order to increase the maximum of 5 subscriptions visible, increase the number in the loop while (i < 5) in the first rule. 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 subscription count"
when 
   	Item TVHE_SUBSCRIPTIONS_COUNT changed or 
	Item TVHE_SUBSCRIPTIONS changed
then
	var String title = ""	 
    var String visible = ""
	var Integer count = Integer::parseInt(TVHE_SUBSCRIPTIONS_COUNT.state.toString)
    val String subscriptions = sendHttpGetRequest(server + "/api/status/subscriptions")

	var Integer i=0
	while (i < 5) 
	{
		if (i < count) 							
		{
            visible = "ON"
			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)		
			} 
			else
			{
				title =  transform("JSONPATH", "$.entries[" + i + "].title", subscriptions) + " / " +	transform("JSONPATH", "$.entries[" + i + "].channel", subscriptions)		
			}
		} 
		else 
		{
            visible = "OFF"
            title = ""
		}

		postUpdate("TVHE_SUBSCRIPTION_" + i + "_VISIBLE" , visible)
		postUpdate("TVHE_SUBSCRIPTION_" + i + "_DESCRIPTION" , title)
		i=i+1
	}	
end

rule "finished recording" 
when 
	Item TVHE_FINISHED_COUNT changed
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("openHAB", msg)		
	} 
end

rule "failed recording" 
when 
	Item TVHE_FAILED_COUNT changed
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("openHAB", msg)		
	}
end

rule "upcoming recording" 
when 
	Item TVHE_UPCOMING_COUNT changed
	or Item TVHE_RECORDINGS_CURRENT changed    
then
	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(",")

		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))
		}
		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))
		} 
	} 
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" 
    {
        Frame label="Server" 
        {
            Text item=TVHE_VERSION  label="TVH Version [%s]"
            Text item=TVHE_SUBSCRIPTIONS_COUNT

            Text item=TVHE_FAILED_COUNT

        }
        Frame item=TVHE_SUBSCRIPTIONS_COUNT label="Subscriptions [%s]" visibility=[TVHE_SUBSCRIPTION_0_VISIBLE==ON,TVHE_SUBSCRIPTION_1_VISIBLE==ON,TVHE_SUBSCRIPTION_2_VISIBLE==ON,TVHE_SUBSCRIPTION_3_VISIBLE==ON,TVHE_SUBSCRIPTION_4_VISIBLE==ON]
        {				
            Text item=TVHE_SUBSCRIPTION_0_DESCRIPTION visibility=[TVHE_SUBSCRIPTION_0_VISIBLE==ON]
            Text item=TVHE_SUBSCRIPTION_1_DESCRIPTION visibility=[TVHE_SUBSCRIPTION_1_VISIBLE==ON]
            Text item=TVHE_SUBSCRIPTION_2_DESCRIPTION visibility=[TVHE_SUBSCRIPTION_2_VISIBLE==ON]
            Text item=TVHE_SUBSCRIPTION_3_DESCRIPTION visibility=[TVHE_SUBSCRIPTION_3_VISIBLE==ON]
            Text item=TVHE_SUBSCRIPTION_4_DESCRIPTION visibility=[TVHE_SUBSCRIPTION_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
            Text item=TVHE_UPCOMING_CURR_STOP
        }	
        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. I have not really much knowledge of JavaScript or JSON, so sorry if i did some stupid things. I want to learn. :wink: