first commit

This commit is contained in:
Your Name
2024-01-11 15:10:51 +03:00
commit eddf07c652
602 changed files with 67154 additions and 0 deletions
@@ -0,0 +1,293 @@
import asyncio
import json
import logging
import voluptuous as vol
from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_ID, \
ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ENTITY_ID, \
EVENT_HOMEASSISTANT_STOP, CONF_TOKEN, CONF_INCLUDE, CONF_DEVICES, \
CONF_HOST, CONF_PORT, CONF_NAME
from homeassistant.core import ServiceCall, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .core import utils
from .core.const import *
from .core.yandex_glagol import YandexIOListener
from .core.yandex_quasar import YandexQuasar
from .core.yandex_session import YandexSession
_LOGGER = logging.getLogger(__name__)
MAIN_DOMAINS = ['media_player', 'select']
SUB_DOMAINS = [
'climate', 'light', 'remote', 'switch', 'vacuum', 'humidifier', 'sensor',
'water_heater'
]
CONF_TTS_NAME = 'tts_service_name'
CONF_INTENTS = 'intents'
CONF_DEBUG = 'debug'
CONF_RECOGNITION_LANG = 'recognition_lang'
CONF_PROXY = 'proxy'
DATA_SPEAKERS = 'speakers'
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_INTENTS): dict,
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_PROXY): cv.string,
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):
"""Main setup of component."""
config: dict = hass_config.get(DOMAIN) or {}
hass.data[DOMAIN] = {
DATA_CONFIG: config,
DATA_SPEAKERS: {}
}
YandexSession.proxy = config.get(CONF_PROXY)
await _init_local_discovery(hass)
await _init_services(hass)
await _setup_entry_from_config(hass)
hass.http.register_view(utils.StreamingView(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 = 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()
# 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_intents(hass, quasar)
await _setup_include(hass, entry)
await _setup_devices(hass, quasar)
async def speaker_update(payload: dict):
hass.bus.async_fire('yandex_speaker', payload)
quasar.handle_updates(speaker_update)
for domain in MAIN_DOMAINS:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, domain
))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
quasar: YandexQuasar = hass.data[DOMAIN][entry.unique_id]
quasar.stop()
await asyncio.gather(*[
hass.config_entries.async_forward_entry_unload(entry, domain)
for domain in MAIN_DOMAINS + SUB_DOMAINS
])
return True
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)
if 'entity' in speaker:
entity: "YandexStation" = speaker['entity']
await entity.init_local_mode()
entity.async_write_ha_state()
zeroconf = await utils.get_zeroconf_singleton(hass)
listener = YandexIOListener(hass.loop)
listener.start(found_local_speaker, 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]
async def send_command(call: ServiceCall):
data = dict(call.data)
device = data.pop('device', None)
entity_ids = (data.pop(ATTR_ENTITY_ID, None) or
utils.find_station(speakers.values(), device))
_LOGGER.debug(f"Send command to: {entity_ids}")
if not entity_ids:
_LOGGER.error("Entity_id parameter required")
return
data = {
ATTR_ENTITY_ID: entity_ids,
ATTR_MEDIA_CONTENT_ID: data.get('text'),
ATTR_MEDIA_CONTENT_TYPE: 'dialog',
} if data.get('command') == 'dialog' else {
ATTR_ENTITY_ID: entity_ids,
ATTR_MEDIA_CONTENT_ID: json.dumps(data),
ATTR_MEDIA_CONTENT_TYPE: 'json',
}
await hass.services.async_call(DOMAIN_MP, SERVICE_PLAY_MEDIA, data,
blocking=True)
hass.services.async_register(DOMAIN, 'send_command', send_command)
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(DOMAIN_MP, 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
# load config/.yandex_station.json
x_token = utils.load_token_from_json(hass)
if x_token:
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_intents(hass: HomeAssistant, quasar: YandexQuasar):
"""Setup Intents MediaPlayer and scenarios for Yandex Account."""
config = hass.data[DOMAIN][DATA_CONFIG]
if CONF_INTENTS not in config:
return
intents: dict = config[CONF_INTENTS]
if CONF_INTENTS not in hass.data[DOMAIN]:
hass.data[DOMAIN][CONF_INTENTS] = True
discovered = {CONF_INTENTS: list(intents.keys())}
hass.async_create_task(discovery.async_load_platform(
hass, DOMAIN_MP, DOMAIN, discovered, config))
if quasar.hass_id:
for i, intent in enumerate(intents.keys(), 1):
try:
await quasar.add_intent(intent, intents[intent], i)
except:
pass
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']
# support device_id in upper/lower cases
upd = confdevices.get(did) or confdevices.get(did.lower())
if upd:
device.update(upd)
async def _setup_include(hass: HomeAssistant, entry: ConfigEntry):
"""Setup additional devices from Yandex account."""
config = hass.data[DOMAIN][DATA_CONFIG]
if CONF_INCLUDE not in config:
return
for domain in SUB_DOMAINS:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, domain
))