Sure. I have Pyscipt installed, and wrote the following script, kodi_tv.py, based on one I found online …
#!/usr/bin/env python
import requests
import json
import time, datetime
from datetime import date
from urllib import parse
import sys
kodi_ip = '192.168.0.200'
kodi_port = '8080'
kodi_url = 'http://' + kodi_ip + ':' + kodi_port + '/jsonrpc'
protocol = 'http'
auth = ''
base_web_url = (
f'{protocol}://{auth}{kodi_ip}:{kodi_port}/image/image%3A%2F%2F'
)
state.persist('pyscript.kodi_tv')
state.setattr('pyscript.kodi_tv.friendly_name', 'Kodi - Latest Episodes')
state.setattr('pyscript.kodi_tv.icon', 'mdi:language-python')
@pyscript_compile
def getKodi(get_params):
try:
get_response = requests.post(kodi_url, data=get_params)
return get_response
except Exception as exc:
return None, exc
def get_web_url(path: str) -> str:
"""Get the web URL for the provided path.
This is used for fanart/poster images that are not a http url. For
example the path is local to the kodi installation or a path to
an NFS share.
:param path: The local/nfs/samba/etc. path.
:returns: The web url to access the image over http.
"""
if path.lower().startswith("http"):
return path
# This looks strange, but the path needs to be quoted twice in order
# to work.
quoted_path = parse.quote(parse.quote(path, safe=""))
return base_web_url + quoted_path
@service
def kodi_tv_update(action=None, id=None):
state.set('pyscript.kodi_tv', 'Updating')
#setup the JSON request parameters to use for the next request to Kodi
#we poll the video library for the list of TV shows, sorted by last added
kodi_params = json.dumps({"jsonrpc":"2.0","method":"VideoLibrary.GetTVShows","params":{"properties":["playcount"],"limits":{"end":50,"start":0},"sort":{"ignorearticle":True,"method":"dateadded","order":"descending","useartistsortname":True}},"id":28})
#kodi_response = requests.post(kodi_url, data=kodi_params)
kodi_response = task.executor(getKodi,kodi_params)
#we format the JSON response so it can be read
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
json_object = json.loads(json_data)
#Below are some examples of how to get data back out of the json_object, limits are the number of inprogress shows.
#print(json_object['result']['limits']['start'])
#print(json_object['result']['limits']['end'])
#print(json_object['result']['tvshows'][0])
#json_object = json.loads(json.dumps(json_object['result'], indent=4, sort_keys=True))
#print(kodi_data.get('tvshows'))
#print(json_object['tvshows'])
#next we begin a loop to build the query which will poll the episodes of the latest shows to get the first 6 that have unwatched episodes
#we need to empty the kodi_params as we will use this to store the multi show query, we put the JSON request for each tvshowid on a line and braket it [ ] once #complete. For each inprogress show Kodi returns - ["season", "episode", "playcount", "firstaired", "tvshowid", "lastplayed"]
kodi_params = ''
showcount = 0
for k in json_object['result']['tvshows']:
#print (k['tvshowid'])
#print (k['label'])
#Only handle unwatched or in progress shows
if k['playcount'] < 1:
showcount = showcount + 1
if kodi_params == '':
kodi_params = ('{"jsonrpc":"2.0","id":1,"method":"VideoLibrary.GetEpisodes","params":{"tvshowid": ' + str(k['tvshowid']) + ', "properties": ["playcount"],"sort":{"ignorearticle":true,"method":"episode","order":"ascending","useartistsortname":true}}}')
else:
kodi_params = kodi_params + ","+ "\n" + ('{"jsonrpc":"2.0","id":1,"method":"VideoLibrary.GetEpisodes","params":{"tvshowid": ' + str(k['tvshowid']) + ', "properties": ["playcount"],"sort":{"ignorearticle":true,"method":"episode","order":"ascending","useartistsortname":true}}}')
#once we have found 6 items we can stop
if showcount >= 6: break
kodi_params = "[" + kodi_params + "]"
#print(kodi_params)
#kodi_response = requests.post(kodi_url, data=kodi_params)
kodi_response = task.executor(getKodi,kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
#print(json_data)
json_object = json.loads(json_data)
#Kodi has now returned all the information for the episodes of the inprogress tvshows which we will need to process to determine the first unwatched episode.
kodi_params = ''
#we loop through the results for each show.
idcount = 0;
for k in json_object:
#we then loop through the episodes in each show, and take the first unwatched episode
for x in k['result']['episodes']:
if x['playcount'] < 1:
#then if the particular episode is the next unwatched for a show it will start writing the multiple "GetEpisodeDetails"
if kodi_params == '':
kodi_params = ('{"jsonrpc":"2.0","id":' + str(idcount) + ',"method":"VideoLibrary.GetEpisodeDetails","params":{"episodeid":' + str(x['episodeid']) + ', "properties": ["title","firstaired","playcount","runtime","season","episode","showtitle","file","dateadded","art","rating"]}}')
else:
kodi_params = kodi_params + ","+ "\n" + ('{"jsonrpc":"2.0","id":' + str(idcount) + ',"method":"VideoLibrary.GetEpisodeDetails","params":{"episodeid":' + str(x['episodeid']) + ', "properties": ["title","firstaired","playcount", "runtime","season","episode","showtitle","file","dateadded","art","rating"]}}')
# print(str(x['firstaired']) + ' ' + str(x['tvshowid'])+ ' ' + str(x['episodeid']))
#once we have found the first unwatched episode for a show we can break the loop and move onto the next show.
idcount = idcount + 1
break
#once the loop is complete we write the multiple requests and bound with [] and send to kodi
kodi_params = "[" + kodi_params + "]"
#print(kodi_params)
#kodi_response = requests.post(kodi_url, data=kodi_params)
kodi_response = task.executor(getKodi,kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
#state.set('pyscript.kodi_tv', json.loads(json_data))
#log.info('Wibble')
#state.set('pyscript.kodi_tv', json.loads(json_data)[0]['result']['episodedetails']['title'])
jason = json.loads(json_data)
i=0
while i<6:
if i < len(jason):
episodeNum = jason[i]['result']['episodedetails']['episode']
seriesNum = jason[i]['result']['episodedetails']['season']
posterRaw = jason[i]['result']['episodedetails']['art']['tvshow.poster']
posterRaw = parse.unquote(posterRaw)[8:].strip("/")
seriesEpisode = 'S' + str(seriesNum).zfill(2) + 'E' + str(episodeNum).zfill(2)
posterURL = get_web_url(
posterRaw
)
state.setattr('pyscript.kodi_tv.showtitle_'+ str(i), jason[i]['result']['episodedetails']['showtitle'])
state.setattr('pyscript.kodi_tv.episode_'+ str(i), seriesEpisode)
state.setattr('pyscript.kodi_tv.poster_'+ str(i), posterURL)
state.setattr('pyscript.kodi_tv.file_'+ str(i), jason[i]['result']['episodedetails']['file'])
else:
state.setattr('pyscript.kodi_tv.showtitle_'+ str(i), '')
state.setattr('pyscript.kodi_tv.episode_'+ str(i), '')
state.setattr('pyscript.kodi_tv.poster_'+ str(i), '')
state.setattr('pyscript.kodi_tv.file_'+ str(i), '')
i += 1
state.set('pyscript.kodi_tv', len(jason))
#print(json_data)
#log.info(json_data)
I then have an HA script that runs the kodi_tv_update service, which is set to fire every afternoon to update the info…
alias: Kodi TV
sequence:
- service: pyscript.kodi_tv_update
data: {}
icon: mdi:language-python
mode: single
The data appears in a sensor like so…
To get the interface, I am using layout-card, card_mod and Mushroom UI.
type: custom:layout-card
layout_type: custom:horizontal-layout
cards:
- type: conditional
conditions:
- condition: numeric_state
entity: pyscript.kodi_tv
above: 0
card:
type: custom:stack-in-card
card_mod:
style: |
ha-card {
margin-top: 4px !important;
margin-bottom: 8px !important;
margin-left: 4px !important;
margin-right: 4px !important;
}
cards:
- type: picture
image: local/images/blank.png
card_mod:
style: |
ha-card {
aspect-ratio: 2 / 2.85;
margin-bottom: 0px !important;
--keep-background: true;
background-image: url("{{ state_attr('pyscript.kodi_tv', 'poster_0')|string }}");
background-position: center;
background-repeat: no-repeat; /* Do not repeat the image */
background-size: cover; /* Resize the background image to cover the entire container */
}
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 0
- type: custom:mushroom-template-card
card_mod:
style: |
ha-card {
background: --card-background-color !important;
--keep-background: true;
margin-top: 0px !important;
top: -4px !important;
left: 4px;
}
primary: '{{ state_attr(''pyscript.kodi_tv'', ''showtitle_0'')|string }}'
secondary: '{{ state_attr(''pyscript.kodi_tv'', ''episode_0'')|string }}'
icon: null
icon_color: teal
badge_color: ''
multiline_secondary: false
fill_container: true
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 0
- type: conditional
conditions:
- condition: numeric_state
entity: pyscript.kodi_tv
above: 1
card:
type: custom:stack-in-card
card_mod:
style: |
ha-card {
margin-top: 4px !important;
margin-bottom: 8px !important;
margin-left: 4px !important;
margin-right: 4px !important;
}
cards:
- type: picture
image: local/images/blank.png
card_mod:
style: |
ha-card {
aspect-ratio: 2 / 2.85;
--keep-background: true;
background-image: url("{{ state_attr('pyscript.kodi_tv', 'poster_1')|string }}");
background-position: center;
background-repeat: no-repeat; /* Do not repeat the image */
background-size: cover; /* Resize the background image to cover the entire container */
}
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 1
- type: custom:mushroom-template-card
card_mod:
style: |
ha-card {
background: --card-background-color !important;
--keep-background: true;
top: -4px !important;
left: 4px;
}
primary: '{{ state_attr(''pyscript.kodi_tv'', ''showtitle_1'')|string }}'
secondary: '{{ state_attr(''pyscript.kodi_tv'', ''episode_1'')|string }}'
icon: null
icon_color: teal
badge_color: ''
multiline_secondary: false
fill_container: true
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 1
- type: conditional
conditions:
- condition: numeric_state
entity: pyscript.kodi_tv
above: 2
card:
type: custom:stack-in-card
card_mod:
style: |
ha-card {
margin-top: 4px !important;
margin-bottom: 8px !important;
margin-left: 4px !important;
margin-right: 4px !important;
}
cards:
- type: picture
image: local/images/blank.png
card_mod:
style: |
ha-card {
aspect-ratio: 2 / 2.85;
--keep-background: true;
background-image: url("{{ state_attr('pyscript.kodi_tv', 'poster_2')|string }}");
background-position: center;
background-repeat: no-repeat; /* Do not repeat the image */
background-size: cover; /* Resize the background image to cover the entire container */
}
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 2
- type: custom:mushroom-template-card
card_mod:
style: |
ha-card {
background: --card-background-color !important;
--keep-background: true;
top: -4px !important;
left: 4px;
}
primary: '{{ state_attr(''pyscript.kodi_tv'', ''showtitle_2'')|string }}'
secondary: '{{ state_attr(''pyscript.kodi_tv'', ''episode_2'')|string }}'
icon: null
icon_color: teal
badge_color: ''
multiline_secondary: false
fill_container: true
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 2
- type: conditional
conditions:
- condition: numeric_state
entity: pyscript.kodi_tv
above: 3
card:
type: custom:stack-in-card
card_mod:
style: |
ha-card {
margin-top: 4px !important;
margin-bottom: 8px !important;
margin-left: 4px !important;
margin-right: 4px !important;
}
cards:
- type: picture
image: local/images/blank.png
card_mod:
style: |
ha-card {
aspect-ratio: 2 / 2.85;
--keep-background: true;
background-image: url("{{ state_attr('pyscript.kodi_tv', 'poster_3')|string }}");
background-position: center;
background-repeat: no-repeat; /* Do not repeat the image */
background-size: cover; /* Resize the background image to cover the entire container */
}
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 3
- type: custom:mushroom-template-card
card_mod:
style: |
ha-card {
background: --card-background-color !important;
--keep-background: true;
top: -4px !important;
left: 4px;
}
primary: '{{ state_attr(''pyscript.kodi_tv'', ''showtitle_3'')|string }}'
secondary: '{{ state_attr(''pyscript.kodi_tv'', ''episode_3'')|string }}'
icon: null
icon_color: teal
badge_color: ''
multiline_secondary: false
fill_container: true
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 3
- type: conditional
conditions:
- condition: numeric_state
entity: pyscript.kodi_tv
above: 4
card:
type: custom:stack-in-card
card_mod:
style: |
ha-card {
margin-top: 4px !important;
margin-bottom: 8px !important;
margin-left: 4px !important;
margin-right: 4px !important;
}
cards:
- type: picture
image: local/images/blank.png
card_mod:
style: |
ha-card {
aspect-ratio: 2 / 2.85;
--keep-background: true;
background-image: url("{{ state_attr('pyscript.kodi_tv', 'poster_4')|string }}");
background-position: center;
background-repeat: no-repeat; /* Do not repeat the image */
background-size: cover; /* Resize the background image to cover the entire container */
}
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 4
- type: custom:mushroom-template-card
card_mod:
style: |
ha-card {
background: --card-background-color !important;
--keep-background: true;
top: -4px !important;
left: 4px;
}
primary: '{{ state_attr(''pyscript.kodi_tv'', ''showtitle_4'')|string }}'
secondary: '{{ state_attr(''pyscript.kodi_tv'', ''episode_4'')|string }}'
icon: null
icon_color: teal
badge_color: ''
multiline_secondary: false
fill_container: true
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 4
- type: conditional
conditions:
- condition: numeric_state
entity: pyscript.kodi_tv
above: 5
card:
type: custom:stack-in-card
card_mod:
style: |
ha-card {
margin-top: 4px !important;
margin-bottom: 8px !important;
margin-left: 4px !important;
margin-right: 4px !important;
}
cards:
- type: picture
image: local/images/blank.png
card_mod:
style: |
ha-card {
aspect-ratio: 2 / 2.85;
--keep-background: true;
background-image: url("{{ state_attr('pyscript.kodi_tv', 'poster_5')|string }}");
background-position: center;
background-repeat: no-repeat; /* Do not repeat the image */
background-size: cover; /* Resize the background image to cover the entire container */
}
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 5
- type: custom:mushroom-template-card
card_mod:
style: |
ha-card {
background: --card-background-color !important;
--keep-background: true;
top: -4px !important;
left: 4px;
}
primary: '{{ state_attr(''pyscript.kodi_tv'', ''showtitle_5'')|string }}'
secondary: '{{ state_attr(''pyscript.kodi_tv'', ''episode_5'')|string }}'
icon: null
icon_color: teal
badge_color: ''
multiline_secondary: false
fill_container: true
tap_action:
action: fire-dom-event
browser_mod:
service: script.kodi_play_tv_file
data:
browser_id: THIS
index: 5
layout:
max_cols: 6
width: 120
max_width: 240
card_margin: 0
margin: 0
And finally, to make the buttons play the TV shows when you tap them, I wrote an HA script like this…
alias: Kodi Play TV File
sequence:
- service: kodi.call_method
metadata: {}
data:
method: Player.Open
item:
file: "{{ state_attr('pyscript.kodi_tv', ('file_' + index|string) )|string }}"
target:
entity_id: media_player.192_168_0_200
mode: single
icon: mdi:kodi
So, yeah… it’s a bit fiddly… but it works rather well. I also had to modify a few of the other elements to get the same functionality for movies, but that was a bit easier, as I didn’t have to roll my own sensor!
Oh… and don’t forget to set the IP in the scripts to that of your own KODI server!
Good luck!