Skip to content
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ omit =
homeassistant/components/knx/*
homeassistant/components/knx/climate.py
homeassistant/components/knx/cover.py
homeassistant/components/kodi/__init__.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can mark it as kodi/*

homeassistant/components/kodi/const.py
homeassistant/components/kodi/media_player.py
homeassistant/components/kodi/notify.py
homeassistant/components/konnected/*
Expand Down
90 changes: 90 additions & 0 deletions homeassistant/components/kodi/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,91 @@
"""The kodi component."""

import asyncio
import voluptuous as vol

from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
from homeassistant.components.kodi.const import DOMAIN
from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN


SERVICE_ADD_MEDIA = "add_to_playlist"
SERVICE_CALL_METHOD = "call_method"

ATTR_MEDIA_TYPE = "media_type"
ATTR_MEDIA_NAME = "media_name"
ATTR_MEDIA_ARTIST_NAME = "artist_name"
ATTR_MEDIA_ID = "media_id"
ATTR_METHOD = "method"

MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids})

KODI_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend(
{
vol.Required(ATTR_MEDIA_TYPE): cv.string,
vol.Optional(ATTR_MEDIA_ID): cv.string,
vol.Optional(ATTR_MEDIA_NAME): cv.string,
vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string,
}
)
KODI_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend(
{vol.Required(ATTR_METHOD): cv.string}, extra=vol.ALLOW_EXTRA
)

SERVICE_TO_METHOD = {
SERVICE_ADD_MEDIA: {
"method": "async_add_media_to_playlist",
"schema": KODI_ADD_MEDIA_SCHEMA,
},
SERVICE_CALL_METHOD: {
"method": "async_call_method",
"schema": KODI_CALL_METHOD_SCHEMA,
},
}


async def async_setup(hass, config):
"""Set up the Kodi integration."""
if any(
((CONF_PLATFORM, DOMAIN) in cfg.items() for cfg in config.get(MP_DOMAIN, []))
):
# Register the Kodi media_player services
async def async_service_handler(service):
"""Map services to methods on MediaPlayerDevice."""
method = SERVICE_TO_METHOD.get(service.service)
if not method:
return

params = {
key: value for key, value in service.data.items() if key != "entity_id"
}
entity_ids = service.data.get("entity_id")
if entity_ids:
target_players = [
player
for player in hass.data[DOMAIN].values()
if player.entity_id in entity_ids
]
else:
target_players = hass.data[DOMAIN].values()

update_tasks = []
for player in target_players:
await getattr(player, method["method"])(**params)

for player in target_players:
if player.should_poll:
update_coro = player.async_update_ha_state(True)
update_tasks.append(update_coro)

if update_tasks:
await asyncio.wait(update_tasks)

for service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service]["schema"]
hass.services.async_register(
DOMAIN, service, async_service_handler, schema=schema
)

# Return boolean to indicate that initialization was successful.
return True
2 changes: 2 additions & 0 deletions homeassistant/components/kodi/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Constants for the Kodi platform."""
DOMAIN = "kodi"
91 changes: 7 additions & 84 deletions homeassistant/components/kodi/media_player.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Support for interfacing with the XBMC/Kodi JSON-RPC API."""
import asyncio
from collections import OrderedDict
from functools import wraps
import logging
Expand All @@ -10,9 +9,10 @@
import aiohttp
import voluptuous as vol

from homeassistant.components.kodi import SERVICE_CALL_METHOD
from homeassistant.components.kodi.const import DOMAIN
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
DOMAIN,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
Expand All @@ -34,7 +34,6 @@
SUPPORT_VOLUME_STEP,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
Expand Down Expand Up @@ -134,42 +133,6 @@
}
)

SERVICE_ADD_MEDIA = "kodi_add_to_playlist"
SERVICE_CALL_METHOD = "kodi_call_method"

DATA_KODI = "kodi"

ATTR_MEDIA_TYPE = "media_type"
ATTR_MEDIA_NAME = "media_name"
ATTR_MEDIA_ARTIST_NAME = "artist_name"
ATTR_MEDIA_ID = "media_id"
ATTR_METHOD = "method"

MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids})

MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend(
{
vol.Required(ATTR_MEDIA_TYPE): cv.string,
vol.Optional(ATTR_MEDIA_ID): cv.string,
vol.Optional(ATTR_MEDIA_NAME): cv.string,
vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string,
}
)
MEDIA_PLAYER_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend(
{vol.Required(ATTR_METHOD): cv.string}, extra=vol.ALLOW_EXTRA
)

SERVICE_TO_METHOD = {
SERVICE_ADD_MEDIA: {
"method": "async_add_media_to_playlist",
"schema": MEDIA_PLAYER_ADD_MEDIA_SCHEMA,
},
SERVICE_CALL_METHOD: {
"method": "async_call_method",
"schema": MEDIA_PLAYER_CALL_METHOD_SCHEMA,
},
}


def _check_deprecated_turn_off(hass, turn_off_action):
"""Create an equivalent script for old turn off actions."""
Expand Down Expand Up @@ -205,8 +168,8 @@ def _check_deprecated_turn_off(hass, turn_off_action):

async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Kodi platform."""
if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = dict()
if DOMAIN not in hass.data:
hass.data[DOMAIN] = dict()

unique_id = None
# Is this a manual configuration?
Expand All @@ -231,14 +194,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
# Only add a device once, so discovered devices do not override manual
# config.
ip_addr = socket.gethostbyname(host)
if ip_addr in hass.data[DATA_KODI]:
if ip_addr in hass.data[DOMAIN]:
return

# If we got an unique id, check that it does not exist already.
# This is necessary as netdisco does not deterministally return the same
# advertisement when the service is offered over multiple IP addresses.
if unique_id is not None:
for device in hass.data[DATA_KODI].values():
for device in hass.data[DOMAIN].values():
if device.unique_id == unique_id:
return

Expand All @@ -258,49 +221,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
unique_id=unique_id,
)

hass.data[DATA_KODI][ip_addr] = entity
hass.data[DOMAIN][ip_addr] = entity
async_add_entities([entity], update_before_add=True)

async def async_service_handler(service):
"""Map services to methods on MediaPlayerDevice."""
method = SERVICE_TO_METHOD.get(service.service)
if not method:
return

params = {
key: value for key, value in service.data.items() if key != "entity_id"
}
entity_ids = service.data.get("entity_id")
if entity_ids:
target_players = [
player
for player in hass.data[DATA_KODI].values()
if player.entity_id in entity_ids
]
else:
target_players = hass.data[DATA_KODI].values()

update_tasks = []
for player in target_players:
await getattr(player, method["method"])(**params)

for player in target_players:
if player.should_poll:
update_coro = player.async_update_ha_state(True)
update_tasks.append(update_coro)

if update_tasks:
await asyncio.wait(update_tasks)

if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA):
return

for service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service]["schema"]
hass.services.async_register(
DOMAIN, service, async_service_handler, schema=schema
)


def cmd(func):
"""Catch command exceptions."""
Expand Down
30 changes: 30 additions & 0 deletions homeassistant/components/kodi/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Describes the format for available Kodi services

add_to_playlist:
description: Add music to the default playlist (i.e. playlistid=0).
fields:
entity_id:
description: Name(s) of the Kodi entities where to add the media.
example: 'media_player.living_room_kodi'
media_type:
description: Media type identifier. It must be one of SONG or ALBUM.
example: ALBUM
media_id:
description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library.
example: 123456
media_name:
description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist.
example: 'Highway to Hell'
artist_name:
description: Optional artist name for filtering media.
example: 'AC/DC'

call_method:
description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.'
fields:
entity_id:
description: Name(s) of the Kodi entities where to run the API method.
example: 'media_player.living_room_kodi'
method:
description: Name of the Kodi JSONRPC API method to be called.
example: 'VideoLibrary.GetRecentlyAddedEpisodes'
29 changes: 0 additions & 29 deletions homeassistant/components/media_player/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -232,35 +232,6 @@ soundtouch_remove_zone_slave:
description: Name of slaves entities to remove from the existing zone.
example: 'media_player.soundtouch_bedroom'

kodi_add_to_playlist:
description: Add music to the default playlist (i.e. playlistid=0).
fields:
entity_id:
description: Name(s) of the Kodi entities where to add the media.
example: 'media_player.living_room_kodi'
media_type:
description: Media type identifier. It must be one of SONG or ALBUM.
example: ALBUM
media_id:
description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library.
example: 123456
media_name:
description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist.
example: 'Highway to Hell'
artist_name:
description: Optional artist name for filtering media.
example: 'AC/DC'

kodi_call_method:
description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.'
fields:
entity_id:
description: Name(s) of the Kodi entities where to run the API method.
example: 'media_player.living_room_kodi'
method:
description: Name of the Kodi JSONRPC API method to be called.
example: 'VideoLibrary.GetRecentlyAddedEpisodes'

squeezebox_call_method:
description: 'Call a Squeezebox JSON/RPC API method.'
fields:
Expand Down