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 ))