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,162 @@
from __future__ import annotations
import asyncio
from asyncio import TimeoutError
from dataclasses import asdict, dataclass
from datetime import timedelta
import json
import logging
from typing import Any, cast
from aiohttp import (
ClientConnectionError,
ClientResponseError,
ClientSession,
ClientWebSocketResponse,
WSMessage,
WSMsgType,
)
from aiohttp.web_request import Request as AIOWebRequest
from homeassistant.components.stream import Stream
from homeassistant.components.stream.hls import (
HlsInitView,
HlsMasterPlaylistView,
HlsPartView,
HlsPlaylistView,
HlsSegmentView,
)
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant
from homeassistant.helpers.event import async_call_later
from multidict import MultiDictProxy
import yarl
from .const import CLOUD_STREAM_BASE_URL
_LOGGER = logging.getLogger(__name__)
RECONNECTION_DELAY = 2
WAIT_FOR_CONNECTION_TIMEOUT = 10
@dataclass
class Request:
view: str
sequence: str = None
part_num: str = None
url_query: str = None
@dataclass
class ResponseMeta:
status_code: int
headers: dict[str, Any] | None = None
class WebRequest:
def __init__(self, hass: HomeAssistant, url: yarl.URL):
self.app = {'hass': hass}
self._url = url
@property
def query(self) -> MultiDictProxy[str]:
return MultiDictProxy(self._url.query)
class CloudStream:
def __init__(self, hass: HomeAssistant, stream: Stream, session: ClientSession):
self._hass = hass
self._stream = stream
self._running_stream_id: str | None = None
self._session = session
self._unsub_reconnect: CALLBACK_TYPE | None = None
self._connected = asyncio.Event()
self._ws: ClientWebSocketResponse | None = None
@property
def stream_url(self) -> str | None:
if not self._running_stream_id:
return None
return f'{CLOUD_STREAM_BASE_URL}/{self._running_stream_id}/master_playlist.m3u8'
async def start(self):
if self._ws or not self._stream.access_token:
return
self._running_stream_id = self._stream.access_token
self._hass.loop.create_task(self._connect())
await asyncio.wait_for(self._connected.wait(), timeout=WAIT_FOR_CONNECTION_TIMEOUT)
await self._keepalive()
async def _keepalive(self, *_):
if self._stream.access_token != self._running_stream_id:
return await self._disconnect()
async_call_later(self._hass, timedelta(seconds=1), HassJob(self._keepalive))
async def _connect(self, *_):
if not self._running_stream_id:
return
ws_url = f'{CLOUD_STREAM_BASE_URL}/{self._running_stream_id}/connect'
# noinspection PyBroadException
try:
_LOGGER.debug(f'Connecting to {ws_url}')
self._ws = await self._session.ws_connect(ws_url, heartbeat=30)
_LOGGER.debug('Connection to Yandex Smart Home cloud established')
self._connected.set()
async for msg in self._ws: # type: WSMessage
if msg.type == WSMsgType.TEXT:
await self._handle_request(msg.json())
_LOGGER.debug(f'Disconnected: {self._ws.close_code}')
if self._ws.close_code is not None:
self._try_reconnect()
except (ClientConnectionError, ClientResponseError, TimeoutError):
_LOGGER.exception('Failed to connect to Yandex Smart Home cloud')
self._try_reconnect()
except Exception:
_LOGGER.exception('Unexpected exception')
self._try_reconnect()
async def _disconnect(self, *_):
self._running_stream_id = None
self._connected.clear()
if self._unsub_reconnect:
self._unsub_reconnect()
self._unsub_reconnect = None
if self._ws:
await self._ws.close()
self._ws = None
async def _handle_request(self, payload: dict):
_LOGGER.debug(f'Request: {payload}')
request = Request(**payload)
request_url = yarl.URL.build(path=f'{request.view}', query=request.url_query)
web_request = cast(AIOWebRequest, WebRequest(self._hass, request_url))
views = {
'master_playlist': HlsMasterPlaylistView,
'playlist': HlsPlaylistView,
'init': HlsInitView,
'part': HlsPartView,
'segment': HlsSegmentView
}
view = views[request.view]()
r = await view.get(web_request, self._stream.access_token, request.sequence, request.part_num)
meta = ResponseMeta(status_code=r.status, headers=dict(r.headers))
response = bytes(json.dumps(asdict(meta)), 'utf-8') + b'\r\n' + r.body
await self._ws.send_bytes(response, compress=False)
def _try_reconnect(self):
_LOGGER.debug(f'Trying to reconnect in {RECONNECTION_DELAY} seconds')
self._unsub_reconnect = async_call_later(self._hass, RECONNECTION_DELAY, HassJob(self._connect))