Files
Victor Alexandrovich Tsyrenschikov 373ed28445 python
2026-03-30 20:25:42 +05:00

350 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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