Загрузка данных


        f"({request_name}, invocation_id={invocation_id}). Смотреть вложения received ws message"
    )


async def _receive_subscribed_messages(
    ws_client: WebSocketClient,
    ws_message_type: str,
    ws_invoke_type: str,
    ws_invoke_params: Any,
    *,
    attempts: int,
    per_attempt_timeout: float,
) -> AsyncIterator[list]:
    """
    Генератор сообщений подписки: подписывается и последовательно отдаёт сообщения
    нужного типа. При таймауте или обрыве соединения переподключается и подписывается
    заново, продолжая попытки до attempts. После исчерпания попыток генератор просто
    завершается — итоговую ошибку формирует вызывающая функция.
    """
    needs_subscribe = True
    for attempt in range(1, attempts + 1):
        try:
            with allure.step(
                f"Получение сообщения с контентом типа: {ws_message_type} - попытка {attempt} из {attempts}"
            ):
                if needs_subscribe:
                    await connect(ws_client, ws_invoke_type, ws_invoke_params)
                    needs_subscribe = False
                yield await ws_client.receive_by_type(ws_message_type, timeout=per_attempt_timeout)
        except asyncio.TimeoutError:
            await ws_client.reconnect()
            needs_subscribe = True
        except Exception:
            await ws_client.reconnect()
            needs_subscribe = True


async def connect_and_get_parsed_msg_by_tu_id(
    tu_id: int,
    ws_client: WebSocketClient,
    ws_message_type: str,
    ws_invoke_type: str,
    ws_invoke_params: Any = None,
    attempts: int = TestConst.SUBSCRIBE_MESSAGE_POLL_ATTEMPTS,
) -> SubscribeAllLeaksInfoReply:
    """
    Подписывается и ищет allLeaksInfo сообщение для конкретного ТУ.
    Читает сообщения нужного типа с ретраями и реконнектом, фильтруя по tuId.
    """
    async for payload in _receive_subscribed_messages(
        ws_client,
        ws_message_type,
        ws_invoke_type,
        ws_invoke_params,
        attempts=attempts,
        per_attempt_timeout=TestConst.POLL_BY_TIMEOUT_SECONDS,
    ):
        parsed_payload = ws_message_parser.parse_all_leaks_info_msg(payload)
        #  Ищет сообщение с нужным ТУ
        if parsed_payload.replyContent.tuId == tu_id:
            return parsed_payload

    fail(f"Не удалось получить сообщение {ws_message_type} для ТУ {tu_id} за {attempts} попыток")


async def connect_and_poll_subscribed_msg(
    ws_client: WebSocketClient,
    ws_message_type: str,
    ws_invoke_type: str,
    ws_invoke_params: Any = None,
    retries: int = TestConst.SUBSCRIBE_MESSAGE_POLL_ATTEMPTS,
) -> list:
    """
    Подписывается и ищет нужное сообщение в потоке, с повторным переподключением ws клиента
    """
    ws_client.suppress_recv_logging = True
    try:
        async for payload in _receive_subscribed_messages(
            ws_client,
            ws_message_type,
            ws_invoke_type,
            ws_invoke_params,
            attempts=retries,
            per_attempt_timeout=TestConst.POLL_BY_TIMEOUT_SECONDS,
        ):
            return payload
        fail(f"Не удалось получить сообщение с контентом типа: {ws_message_type} за {retries} попыток")
    finally:
        ws_client.suppress_recv_logging = False


async def connect_and_poll_subscribed_signal(
    ws_client: WebSocketClient,
    ws_message_type: str,
    ws_invoke_type: str,
    ws_invoke_params: Any,
    sensor_id: int,
    sensor_description: str,
    parse_payload: Callable[[list], ParsedPayloadType],
    extract_signals: Callable[[ParsedPayloadType], List[InputSignalType]],
    *,
    max_messages: int = TestConst.SUBSCRIBE_MESSAGE_POLL_ATTEMPTS,
    timeout: float = TestConst.POLL_BY_TIMEOUT_SECONDS,
) -> tuple[ParsedPayloadType, InputSignalType]:
    """
    Подписывается и читает до max_messages сообщений из потока, пока не найдёт сигнал
    с нужным id. При таймауте/обрыве соединения переподключается и подписывается заново.
    """
    received_ids: Set[int] = set()

    async for payload in _receive_subscribed_messages(
        ws_client,
        ws_message_type,
        ws_invoke_type,
        ws_invoke_params,
        attempts=max_messages,
        per_attempt_timeout=timeout,
    ):
        parsed = parse_payload(payload)
        signals = extract_signals(parsed) or []

        for signal in signals:
            signal_id = getattr(signal, "id", None)
            if signal_id is not None:
                received_ids.add(signal_id)

        target_signal = find_object_by_field(signals, "id", sensor_id)
        if target_signal is not None:
            allure.attach(
                f"Сигнал id={sensor_id} ({sensor_description}) найден\n"
                f"Всего сигналов в сообщении: {len(signals)}\n"
                f"Полученные id: {sorted(received_ids)}",
                name=f"Результат поиска сигнала в {ws_message_type}",
                attachment_type=allure.attachment_type.TEXT,
            )
            return parsed, target_signal

    received_ids_text = ", ".join(str(signal_id) for signal_id in sorted(received_ids)) or "нет"
    fail(
        f"Информация по датчику {sensor_description} (id={sensor_id}) не пришла в ответе {ws_message_type} "
        f"после {max_messages} попыток чтения из потока подписки. Полученные id: {received_ids_text}"
    )


async def poll_balance_algorithm_diagnostic_areas(