Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,8 @@ omit =
homeassistant/components/somfy/*
homeassistant/components/somfy_mylink/*
homeassistant/components/sonarr/sensor.py
homeassistant/components/songpal/*
homeassistant/components/songpal/__init__.py
homeassistant/components/songpal/media_player.py
homeassistant/components/sonos/*
homeassistant/components/sony_projector/switch.py
homeassistant/components/spc/*
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ homeassistant/components/solax/* @squishykid
homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne
homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti
homeassistant/components/songpal/* @rytilahti @shenxn
homeassistant/components/sonos/* @amelchio
homeassistant/components/spaceapi/* @fabaff
homeassistant/components/speedtestdotnet/* @rohankapoorcom
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/discovery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
"openhome": ("media_player", "openhome"),
"bose_soundtouch": ("media_player", "soundtouch"),
"bluesound": ("media_player", "bluesound"),
"songpal": ("media_player", "songpal"),
"kodi": ("media_player", "kodi"),
"volumio": ("media_player", "volumio"),
"lg_smart_device": ("media_player", "lg_soundbar"),
Expand All @@ -91,6 +90,7 @@
"ikea_tradfri",
"philips_hue",
"sonos",
"songpal",
SERVICE_WEMO,
]

Expand Down
49 changes: 49 additions & 0 deletions homeassistant/components/songpal/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,50 @@
"""The songpal component."""
from collections import OrderedDict
import logging

import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType

from .const import CONF_ENDPOINT, DOMAIN

_LOGGER = logging.getLogger(__name__)

SONGPAL_CONFIG_SCHEMA = vol.Schema(
{vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENDPOINT): cv.string}
)

CONFIG_SCHEMA = vol.Schema(
{vol.Optional(DOMAIN): vol.All(cv.ensure_list, [SONGPAL_CONFIG_SCHEMA])},
extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass: HomeAssistantType, config: OrderedDict) -> bool:
"""Set up songpal environment."""
conf = config.get(DOMAIN)
if conf is None:
return True
for config_entry in conf:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config_entry,
),
)
return True


async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up songpal media player."""
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "media_player")
)
return True


async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Unload songpal media player."""
return await hass.config_entries.async_forward_entry_unload(entry, "media_player")
153 changes: 153 additions & 0 deletions homeassistant/components/songpal/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""Config flow to configure songpal component."""
import logging
from typing import Optional
from urllib.parse import urlparse

from songpal import Device, SongpalException
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components import ssdp
from homeassistant.const import CONF_HOST, CONF_NAME

from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import

_LOGGER = logging.getLogger(__name__)


class SongpalConfig:
"""Device Configuration."""

def __init__(self, name, host, endpoint):
"""Initialize Configuration."""
self.name = name
self.host = host
self.endpoint = endpoint


class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Songpal configuration flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

def __init__(self):
"""Initialize the flow."""
self.conf: Optional[SongpalConfig] = None

async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_ENDPOINT): str}),
)

# Validate input
endpoint = user_input[CONF_ENDPOINT]
parsed_url = urlparse(endpoint)

# Try to connect and get device name
try:
device = Device(endpoint)
await device.get_supported_methods()
interface_info = await device.get_interface_information()
name = interface_info.modelName
except SongpalException as ex:
_LOGGER.debug("Connection failed: %s", ex)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(
CONF_ENDPOINT, default=user_input.get(CONF_ENDPOINT, "")
): str,
}
),
errors={"base": "connection"},
)

self.conf = SongpalConfig(name, parsed_url.hostname, endpoint)

return await self.async_step_init(user_input)

async def async_step_init(self, user_input=None):
"""Handle a flow start."""
# Check if already configured
if self._endpoint_already_configured():
return self.async_abort(reason="already_configured")

if user_input is None:
return self.async_show_form(
step_id="init",
description_placeholders={
CONF_NAME: self.conf.name,
CONF_HOST: self.conf.host,
},
)

await self.async_set_unique_id(self.conf.endpoint)
self._abort_if_unique_id_configured()

return self.async_create_entry(
title=self.conf.name,
data={CONF_NAME: self.conf.name, CONF_ENDPOINT: self.conf.endpoint},
)

async def async_step_ssdp(self, discovery_info):
"""Handle a discovered Songpal device."""
await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN])
self._abort_if_unique_id_configured()

Comment thread
rytilahti marked this conversation as resolved.
_LOGGER.debug("Discovered: %s", discovery_info)

friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME]
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
scalarweb_info = discovery_info["X_ScalarWebAPI_DeviceInfo"]
endpoint = scalarweb_info["X_ScalarWebAPI_BaseURL"]
service_types = scalarweb_info["X_ScalarWebAPI_ServiceList"][
"X_ScalarWebAPI_ServiceType"
]

# Ignore Bravia TVs
if "videoScreen" in service_types:
return self.async_abort(reason="not_songpal_device")

# pylint: disable=no-member
self.context["title_placeholders"] = {
CONF_NAME: friendly_name,
CONF_HOST: parsed_url.hostname,
}

self.conf = SongpalConfig(friendly_name, parsed_url.hostname, endpoint)

return await self.async_step_init()

async def async_step_import(self, user_input=None):
"""Import a config entry."""
name = user_input.get(CONF_NAME)
endpoint = user_input.get(CONF_ENDPOINT)
parsed_url = urlparse(endpoint)

# Try to connect to test the endpoint
try:
device = Device(endpoint)
await device.get_supported_methods()
# Get name
if name is None:
interface_info = await device.get_interface_information()
name = interface_info.modelName
except SongpalException as ex:
_LOGGER.error("Import from yaml configuration failed: %s", ex)
return self.async_abort(reason="connection")

self.conf = SongpalConfig(name, parsed_url.hostname, endpoint)

return await self.async_step_init(user_input)

def _endpoint_already_configured(self):
"""See if we already have an endpoint matching user input configured."""
existing_endpoints = [
entry.data[CONF_ENDPOINT] for entry in self._async_current_entries()
Comment thread
shenxn marked this conversation as resolved.
]
return self.conf.endpoint in existing_endpoints
2 changes: 2 additions & 0 deletions homeassistant/components/songpal/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Constants for the Songpal component."""
DOMAIN = "songpal"
SET_SOUND_SETTING = "set_sound_setting"

CONF_ENDPOINT = "endpoint"
11 changes: 9 additions & 2 deletions homeassistant/components/songpal/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
{
"domain": "songpal",
"name": "Sony Songpal",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/songpal",
"requirements": ["python-songpal==0.11.2"],
"codeowners": ["@rytilahti"]
"requirements": ["python-songpal==0.12"],
"codeowners": ["@rytilahti", "@shenxn"],
"ssdp": [
{
"st": "urn:schemas-sony-com:service:ScalarWebAPI:1",
"manufacturer": "Sony Corporation"
}
]
}
Loading