python
This commit is contained in:
349
custom_components/yandex_station/__init__.py
Normal file
349
custom_components/yandex_station/__init__.py
Normal 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
|
||||
Reference in New Issue
Block a user