350 lines
11 KiB
Python
350 lines
11 KiB
Python
import logging
|
||
from datetime import timedelta
|
||
|
||
import voluptuous as vol
|
||
from homeassistant.components.binary_sensor import HomeAssistant # important for tests
|
||
from homeassistant.components.media_player import (
|
||
ATTR_MEDIA_CONTENT_ID,
|
||
ATTR_MEDIA_CONTENT_TYPE,
|
||
DOMAIN as MEDIA_DOMAIN,
|
||
SERVICE_PLAY_MEDIA,
|
||
)
|
||
from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT
|
||
from homeassistant.const import (
|
||
ATTR_ENTITY_ID,
|
||
CONF_DEVICES,
|
||
CONF_DOMAIN,
|
||
CONF_HOST,
|
||
CONF_INCLUDE,
|
||
CONF_PASSWORD,
|
||
CONF_PORT,
|
||
CONF_TOKEN,
|
||
CONF_USERNAME,
|
||
EVENT_HOMEASSISTANT_STOP,
|
||
)
|
||
from homeassistant.core import ServiceCall
|
||
from homeassistant.exceptions import ConfigEntryNotReady
|
||
from homeassistant.helpers import (
|
||
aiohttp_client as ac,
|
||
config_validation as cv,
|
||
device_registry as dr,
|
||
)
|
||
from homeassistant.helpers.event import async_track_time_interval
|
||
|
||
from .core import stream, utils
|
||
from .core.const import CONF_MEDIA_PLAYERS, DATA_CONFIG, DATA_SPEAKERS, DOMAIN
|
||
from .core.yandex_glagol import YandexIOListener
|
||
from .core.yandex_quasar import YandexQuasar
|
||
from .core.yandex_session import YandexSession
|
||
from .core.yandex_station import YandexStationBase
|
||
from .hass import hass_utils
|
||
|
||
_LOGGER = logging.getLogger(__name__)
|
||
|
||
# only for speakers
|
||
SPEAKER_PLATFORMS = [
|
||
"calendar",
|
||
"camera",
|
||
"media_player",
|
||
"select",
|
||
]
|
||
# for import section
|
||
PLATFORMS = [
|
||
"binary_sensor",
|
||
"button",
|
||
"calendar",
|
||
"camera",
|
||
"climate",
|
||
"cover",
|
||
"humidifier",
|
||
"light",
|
||
"media_player",
|
||
"number",
|
||
"remote",
|
||
"select",
|
||
"sensor",
|
||
"switch",
|
||
"vacuum",
|
||
"water_heater",
|
||
]
|
||
|
||
CONF_TTS_NAME = "tts_service_name"
|
||
CONF_DEBUG = "debug"
|
||
CONF_RECOGNITION_LANG = "recognition_lang"
|
||
CONF_PROXY = "proxy"
|
||
CONF_SSL = "ssl"
|
||
|
||
CONFIG_SCHEMA = vol.Schema(
|
||
{
|
||
DOMAIN: vol.Schema(
|
||
{
|
||
vol.Optional(CONF_USERNAME): cv.string,
|
||
vol.Optional(CONF_PASSWORD): cv.string,
|
||
vol.Optional(CONF_TOKEN): cv.string,
|
||
vol.Optional(CONF_TTS_NAME): cv.string,
|
||
vol.Optional(CONF_INCLUDE): cv.ensure_list,
|
||
vol.Optional(CONF_DEVICES): {
|
||
cv.string: vol.Schema(
|
||
{
|
||
vol.Optional(CONF_HOST): cv.string,
|
||
vol.Optional(CONF_PORT, default=1961): cv.port,
|
||
},
|
||
extra=vol.ALLOW_EXTRA,
|
||
),
|
||
},
|
||
vol.Optional(CONF_MEDIA_PLAYERS): vol.Any(dict, list),
|
||
vol.Optional(CONF_RECOGNITION_LANG): cv.string,
|
||
vol.Optional(CONF_DOMAIN): cv.string,
|
||
vol.Optional(CONF_PROXY): cv.string,
|
||
vol.Optional(CONF_SSL): cv.boolean,
|
||
vol.Optional(CONF_DEBUG, default=False): cv.boolean,
|
||
},
|
||
extra=vol.ALLOW_EXTRA,
|
||
),
|
||
},
|
||
extra=vol.ALLOW_EXTRA,
|
||
)
|
||
|
||
|
||
async def async_setup(hass: HomeAssistant, hass_config: dict):
|
||
config: dict = hass_config.get(DOMAIN) or {}
|
||
hass.data[DOMAIN] = {DATA_CONFIG: config, DATA_SPEAKERS: {}}
|
||
|
||
# if CONF_RECOGNITION_LANG in config:
|
||
# utils.fix_recognition_lang(
|
||
# hass, "frontend_latest", config[CONF_RECOGNITION_LANG]
|
||
# )
|
||
|
||
YandexSession.domain = config.get(CONF_DOMAIN)
|
||
YandexSession.proxy = config.get(CONF_PROXY)
|
||
YandexSession.ssl = config.get(CONF_SSL)
|
||
|
||
await _init_local_discovery(hass)
|
||
await _init_services(hass)
|
||
await _setup_entry_from_config(hass)
|
||
|
||
def import_conversation():
|
||
try:
|
||
from . import conversation
|
||
|
||
SPEAKER_PLATFORMS.append("conversation")
|
||
PLATFORMS.append("conversation")
|
||
except ImportError as e:
|
||
_LOGGER.warning(repr(e))
|
||
|
||
# using executor, because bug with "Detected blocking call"
|
||
await hass.async_add_executor_job(import_conversation)
|
||
|
||
hass.http.register_view(stream.StreamView(hass))
|
||
|
||
return True
|
||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||
async def update_cookie_and_token(**kwargs):
|
||
hass.config_entries.async_update_entry(entry, data=kwargs)
|
||
|
||
session = ac.async_create_clientsession(hass)
|
||
yandex = YandexSession(session, **entry.data)
|
||
yandex.add_update_listener(update_cookie_and_token)
|
||
|
||
try:
|
||
ok = await yandex.refresh_cookies()
|
||
except Exception as e:
|
||
raise ConfigEntryNotReady from e
|
||
|
||
if not ok:
|
||
hass.components.persistent_notification.async_create(
|
||
"Необходимо заново авторизоваться в Яндексе. Для этого [добавьте "
|
||
"новую интеграцию](/config/integrations) с тем же логином.",
|
||
title="Yandex.Station",
|
||
)
|
||
return False
|
||
|
||
quasar = YandexQuasar(yandex)
|
||
await quasar.init()
|
||
|
||
await hass_utils.load_fake_devies(hass, quasar)
|
||
|
||
# entry.unique_id - user login
|
||
hass.data[DOMAIN][entry.unique_id] = quasar
|
||
|
||
# add stations to global list
|
||
speakers = hass.data[DOMAIN][DATA_SPEAKERS]
|
||
for device in quasar.speakers + quasar.modules:
|
||
did = device["quasar_info"]["device_id"]
|
||
if did in speakers:
|
||
device.update(speakers[did])
|
||
speakers[did] = device
|
||
|
||
await _setup_devices(hass, quasar)
|
||
|
||
quasar.start()
|
||
|
||
if hass_utils.incluce_devices(hass, entry):
|
||
quasar.platforms = platforms = PLATFORMS
|
||
entry.async_on_unload(
|
||
async_track_time_interval(
|
||
hass, quasar.devices_passive_update, timedelta(minutes=5)
|
||
)
|
||
)
|
||
else:
|
||
quasar.platforms = platforms = SPEAKER_PLATFORMS
|
||
|
||
await hass.config_entries.async_forward_entry_setups(entry, platforms)
|
||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||
|
||
return True
|
||
|
||
|
||
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||
quasar: YandexQuasar = hass.data[DOMAIN][entry.unique_id]
|
||
quasar.stop()
|
||
|
||
platforms = getattr(quasar, "platforms")
|
||
return await hass.config_entries.async_unload_platforms(entry, platforms)
|
||
|
||
|
||
async def _init_local_discovery(hass: HomeAssistant):
|
||
"""Init descovery local speakers with Zeroconf (mDNS)."""
|
||
speakers: dict = hass.data[DOMAIN][DATA_SPEAKERS]
|
||
|
||
async def found_local_speaker(info: dict):
|
||
speaker = speakers.setdefault(info["device_id"], {})
|
||
speaker.update(info)
|
||
entity: YandexStationBase = speaker.get("entity")
|
||
if entity and entity.hass:
|
||
await entity.init_local_mode()
|
||
entity.async_write_ha_state()
|
||
|
||
zeroconf = await utils.get_zeroconf_singleton(hass)
|
||
|
||
listener = YandexIOListener(
|
||
lambda info: hass.create_task(found_local_speaker(info))
|
||
)
|
||
listener.start(zeroconf)
|
||
|
||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, listener.stop)
|
||
|
||
|
||
async def _init_services(hass: HomeAssistant):
|
||
"""Init Yandex Station TTS service."""
|
||
speakers: dict = hass.data[DOMAIN][DATA_SPEAKERS]
|
||
|
||
try:
|
||
# starting from Home Assistant 2023.7
|
||
from homeassistant.core import ServiceResponse, SupportsResponse
|
||
from homeassistant.helpers import service
|
||
|
||
async def send_command(call: ServiceCall) -> ServiceResponse:
|
||
try:
|
||
# from HA 2025.10
|
||
entity_ids = await service.async_extract_entity_ids(call)
|
||
except TypeError:
|
||
# for HA before 2025.10
|
||
entity_ids = await service.async_extract_entity_ids(hass, call)
|
||
for speaker in speakers.values():
|
||
entity: YandexStationBase = speaker.get("entity")
|
||
if (
|
||
not entity
|
||
or entity.entity_id not in entity_ids
|
||
or not entity.glagol
|
||
):
|
||
continue
|
||
data = service.remove_entity_service_fields(call)
|
||
data.setdefault("command", "sendText")
|
||
if external := data.get("external"):
|
||
data = utils.external_command(**external)
|
||
return await entity.glagol.send(data)
|
||
return {"error": "Entity not found"}
|
||
|
||
hass.services.async_register(
|
||
DOMAIN,
|
||
"send_command",
|
||
send_command,
|
||
supports_response=SupportsResponse.OPTIONAL,
|
||
)
|
||
except ImportError as e:
|
||
_LOGGER.warning(repr(e))
|
||
|
||
async def yandex_station_say(call: ServiceCall):
|
||
entity_ids = call.data.get(ATTR_ENTITY_ID) or utils.find_station(
|
||
speakers.values()
|
||
)
|
||
|
||
_LOGGER.debug(f"Yandex say to: {entity_ids}")
|
||
|
||
if not entity_ids:
|
||
_LOGGER.error("Entity_id parameter required")
|
||
return
|
||
|
||
message = call.data.get("message")
|
||
|
||
data = {
|
||
ATTR_MEDIA_CONTENT_ID: message,
|
||
ATTR_MEDIA_CONTENT_TYPE: "tts",
|
||
ATTR_ENTITY_ID: entity_ids,
|
||
}
|
||
|
||
if "options" in call.data:
|
||
data["extra"] = call.data["options"]
|
||
|
||
await hass.services.async_call(
|
||
MEDIA_DOMAIN, SERVICE_PLAY_MEDIA, data, blocking=True
|
||
)
|
||
|
||
config = hass.data[DOMAIN][DATA_CONFIG]
|
||
service_name = config.get(CONF_TTS_NAME, "yandex_station_say")
|
||
hass.services.async_register("tts", service_name, yandex_station_say)
|
||
|
||
|
||
async def _setup_entry_from_config(hass: HomeAssistant):
|
||
"""Support legacy config from YAML."""
|
||
config = hass.data[DOMAIN][DATA_CONFIG]
|
||
if CONF_USERNAME not in config:
|
||
return
|
||
|
||
# check if already configured
|
||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||
if entry.unique_id == config[CONF_USERNAME]:
|
||
return
|
||
|
||
if x_token := utils.load_token_from_json(hass):
|
||
config["x_token"] = x_token
|
||
|
||
# need username and token or password
|
||
if "x_token" not in config and CONF_PASSWORD not in config:
|
||
return
|
||
|
||
hass.async_create_task(
|
||
hass.config_entries.flow.async_init(
|
||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||
)
|
||
)
|
||
|
||
|
||
async def _setup_devices(hass: HomeAssistant, quasar: YandexQuasar):
|
||
"""Set speakers additional config from YAML."""
|
||
config = hass.data[DOMAIN][DATA_CONFIG]
|
||
if CONF_DEVICES not in config:
|
||
return
|
||
|
||
confdevices = config[CONF_DEVICES]
|
||
|
||
for device in quasar.speakers + quasar.modules:
|
||
did = device["quasar_info"]["device_id"]
|
||
if upd := confdevices.get(did) or confdevices.get(did.lower()):
|
||
device.update(upd)
|
||
|
||
|
||
async def async_remove_config_entry_device(
|
||
hass: HomeAssistant, entry: ConfigEntry, device: dr.DeviceEntry
|
||
) -> bool:
|
||
"""Supported from Hass v2022.3"""
|
||
dr.async_get(hass).async_remove_device(device.id)
|
||
return True
|