173 lines
5.9 KiB
Python
173 lines
5.9 KiB
Python
"""Yandex Smart Home request handlers — упрощённая версия без строгой Pydantic-валидации."""
|
|
|
|
import logging
|
|
from typing import Any, Callable, Coroutine
|
|
|
|
from homeassistant.const import ATTR_ENTITY_ID
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.util.decorator import Registry
|
|
|
|
from .const import ATTR_CAPABILITY, ATTR_ERROR_CODE, EVENT_DEVICE_ACTION
|
|
from .device import Device, async_get_device_description, async_get_device_states, async_get_devices
|
|
from .helpers import ActionNotAllowed, APIError, RequestData
|
|
from .schema import (
|
|
ActionRequest,
|
|
DeviceDescription,
|
|
DeviceList,
|
|
DeviceStates,
|
|
Response,
|
|
ResponseCode,
|
|
StatesRequest,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
HANDLERS: Registry[
|
|
str,
|
|
Callable[
|
|
[HomeAssistant, RequestData, str],
|
|
Coroutine[Any, Any, dict | None],
|
|
],
|
|
] = Registry()
|
|
|
|
|
|
async def async_handle_request(
|
|
hass: HomeAssistant, data: RequestData, action: str, payload: str
|
|
) -> Response:
|
|
handler = HANDLERS.get(action)
|
|
if handler is None:
|
|
_LOGGER.error(f"Unexpected action '{action}'")
|
|
return Response(request_id=data.request_id)
|
|
|
|
try:
|
|
result = await handler(hass, data, payload)
|
|
_LOGGER.debug("Handler result: %s", result)
|
|
|
|
# Конвертируем Pydantic-модели в обычные словари
|
|
if hasattr(result, "model_dump"):
|
|
result = result.model_dump(exclude_none=True)
|
|
|
|
return Response(request_id=data.request_id, payload=result)
|
|
except Exception:
|
|
_LOGGER.exception("Unexpected exception in handler")
|
|
return Response(request_id=data.request_id)
|
|
|
|
|
|
@HANDLERS.register("/user/devices")
|
|
async def async_device_list(
|
|
hass: HomeAssistant, data: RequestData, _payload: str
|
|
) -> DeviceList:
|
|
assert data.request_user_id
|
|
devices: list[DeviceDescription] = []
|
|
for device in await async_get_devices(hass, data.entry_data):
|
|
if (description := await async_get_device_description(hass, device)) is not None:
|
|
devices.append(description)
|
|
data.entry_data.link_platform(data.platform)
|
|
return DeviceList(user_id=data.request_user_id, devices=devices)
|
|
|
|
|
|
@HANDLERS.register("/user/devices/query")
|
|
async def async_devices_query(
|
|
hass: HomeAssistant, data: RequestData, payload: str
|
|
) -> DeviceStates:
|
|
request = StatesRequest.model_validate_json(payload)
|
|
states = await async_get_device_states(hass, data.entry_data, [rd.id for rd in request.devices])
|
|
return DeviceStates(devices=states)
|
|
|
|
|
|
@HANDLERS.register("/user/devices/action")
|
|
async def async_devices_action(
|
|
hass: HomeAssistant, data: RequestData, payload: str
|
|
) -> dict:
|
|
try:
|
|
request = ActionRequest.model_validate_json(payload)
|
|
_LOGGER.debug("Action request: %s", request.model_dump(exclude_none=True))
|
|
except Exception as e:
|
|
_LOGGER.error(f"Failed to parse action request: {e}")
|
|
return {"devices": []}
|
|
|
|
devices_list = []
|
|
|
|
for device_req in request.payload.devices:
|
|
dev_id = device_req.id
|
|
caps = device_req.capabilities
|
|
|
|
ha_state = hass.states.get(dev_id)
|
|
if not ha_state:
|
|
devices_list.append({
|
|
"id": dev_id,
|
|
"action_result": {"status": "ERROR", "error_code": "DEVICE_NOT_FOUND"}
|
|
})
|
|
continue
|
|
|
|
dev = Device(hass, data.entry_data, dev_id, ha_state)
|
|
|
|
if not dev.should_expose:
|
|
data.entry_data.mark_entity_unexposed(ha_state.entity_id)
|
|
|
|
if dev.unavailable:
|
|
hass.bus.async_fire(
|
|
EVENT_DEVICE_ACTION,
|
|
{ATTR_ENTITY_ID: dev_id, ATTR_ERROR_CODE: ResponseCode.DEVICE_UNREACHABLE.value},
|
|
context=data.context,
|
|
)
|
|
devices_list.append({
|
|
"id": dev_id,
|
|
"action_result": {"status": "ERROR", "error_code": "DEVICE_UNREACHABLE"}
|
|
})
|
|
continue
|
|
|
|
cap_responses = []
|
|
at_least_one_success = False
|
|
|
|
for cap in caps:
|
|
try:
|
|
executed_value = await dev.execute(data.context, cap)
|
|
_LOGGER.debug(f"Executed {cap.type} on {dev_id}: {executed_value}")
|
|
|
|
hass.bus.async_fire(
|
|
EVENT_DEVICE_ACTION,
|
|
{ATTR_ENTITY_ID: dev_id, ATTR_CAPABILITY: cap.model_dump(exclude_none=True)},
|
|
context=data.context,
|
|
)
|
|
|
|
# Для on_off всегда value = null в ответе на action
|
|
cap_responses.append({
|
|
"type": str(cap.type),
|
|
"state": {
|
|
"instance": str(cap.state.instance),
|
|
"value": None,
|
|
"action_result": {"status": "DONE"}
|
|
}
|
|
})
|
|
at_least_one_success = True
|
|
|
|
except Exception as exc:
|
|
_LOGGER.exception(f"Error on {cap.type} for {dev_id}")
|
|
cap_responses.append({
|
|
"type": str(cap.type),
|
|
"state": {
|
|
"instance": str(cap.state.instance),
|
|
"value": None,
|
|
"action_result": {"status": "ERROR", "error_code": "INTERNAL_ERROR"}
|
|
}
|
|
})
|
|
|
|
device_resp = {
|
|
"id": dev_id,
|
|
"capabilities": cap_responses,
|
|
"action_result": {"status": "DONE"} if at_least_one_success else {"status": "ERROR", "error_code": "INTERNAL_ERROR"}
|
|
}
|
|
|
|
devices_list.append(device_resp)
|
|
_LOGGER.debug(f"Device response: {device_resp}")
|
|
|
|
full_payload = {"devices": devices_list}
|
|
_LOGGER.debug(f"RETURNING PAYLOAD: {full_payload}")
|
|
|
|
return full_payload
|
|
|
|
|
|
@HANDLERS.register("/user/unlink")
|
|
async def async_user_unlink(_hass: HomeAssistant, data: RequestData, _payload: str) -> None:
|
|
data.entry_data.unlink_platform(data.platform) |