This commit is contained in:
Victor Alexandrovich Tsyrenschikov
2026-03-30 20:25:42 +05:00
parent 139f9f1bd2
commit 373ed28445
2449 changed files with 53602 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
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