↩️ Назад

Категории

Джарвис на Hermes Agent и своём умном доме на PHP

12.10.2026 | из категории: Нейросети

Джарвис на Hermes Agent: голосовое управление своим умным до

Никакого Home Assistant. Свой API, свои контроллеры, свой Джарвис. Собираем голосового ассистента, который говорит с вашей PHP-системой напрямую.

Манифест. Настоящий инженер не ставит чужой софт, чтобы управлять своим светом. Он пишет свой API, свой планировщик, свой протокол обмена и — в завершение — своего голосового ассистента в стиле Джарвиса. Home Assistant здесь не нужен: у нас уже есть бэкенд на PHP, а значит, осталось дать ему голос и мозг.

У вас уже есть самописный умный дом на PHP: свои эндпоинты, свои контроллеры, своя БД с устройствами, сценарии, возможно даже WebSocket-шина для событий. Не хватает одного — возможности управлять всем этим голосом, не тыкая пальцами в веб-морду. Именно эту задачу мы и решим: подружим ваш PHP-бэкенд с Hermes Agent и получим полноценного Джарвиса.

Архитектура: свой дом + свой агент

В отличие от связки с Home Assistant, здесь у нас нет готовых инструментов и универсальных API. Зато есть полный контроль. Мы будем использовать Hermes Agent как «мозг» и tool-runner, а ваш PHP-бэкенд — как «руки».

🧠

Hermes Agent

Ядро системы: интерпретирует речь, держит контекст, вызывает инструменты.

🐘

PHP-бэкенд

Ваш умный дом: REST API, авторизация, устройства, сценарии, WebSocket.

🔌

Свой плагин

Python-мост между агентом и вашим API. Пишется за вечер, живёт годами.

🎙

STT / TTS

Whisper и Piper локально. Никаких облаков, никакой телеметрии.

[Микрофон] ↓ (аудио) [STT: Whisper] ↓ (текст) [Hermes Agent] ←→ [LLM: Ollama / GPT] ↓ (tool call) [Ваш custom-плагин на Python] ↓ (HTTP / JSON) [PHP API: /api/device, /api/scene, /api/state] ↓ [Устройства: свет · климат · замки · датчики] ↑ (события) [PHP WebSocket-шина] → [Hermes Gateway] ↓ [TTS: Piper] → [Динамик]

PHP-API: правим бэкенд под агента

Прежде чем подключать Hermes, убедимся, что ваш PHP-бэкенд умеет то, что нужно агенту. Ему потребуются четыре базовых операции: список устройств, чтение состояния, список действий и выполнение действия.

Пример структуры REST API

МетодЭндпоинтНазначение
GET/api/devicesСписок всех устройств с фильтрами по типу и комнате
GET/api/device/{id}Текущее состояние устройства
GET/api/device/{id}/actionsДоступные действия для устройства
POST/api/device/{id}/actionВыполнить действие: включить, выключить, задать параметр
POST/api/scene/{id}/runЗапустить сценарий

Роутер на чистом PHP (index.php)

<?php
// index.php — точка входа API умного дома

require 'vendor/autoload.php';

use App\Auth;
use App\DeviceController;
use App\SceneController;

$auth = new Auth();
$token = $_SERVER['HTTP_X_API_TOKEN'] ?? null;

if (!$auth->verify($token)) {
    http_response_code(401);
    echo json_encode(['error' => 'unauthorized']);
    exit;
}

$path   = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
$input  = json_decode(file_get_contents('php://input'), true);

header('Content-Type: application/json');

match (true) {
    $path === '/api/devices' && $method === 'GET'
        => (new DeviceController())->index($_GET),

    preg_match('#^/api/device/([\w\-]+)$#', $path, $m) && $method === 'GET'
        => (new DeviceController())->show($m[1]),

    preg_match('#^/api/device/([\w\-]+)/actions$#', $path, $m) && $method === 'GET'
        => (new DeviceController())->actions($m[1]),

    preg_match('#^/api/device/([\w\-]+)/action$#', $path, $m) && $method === 'POST'
        => (new DeviceController())->act($m[1], $input),

    preg_match('#^/api/scene/([\w\-]+)/run$#', $path, $m) && $method === 'POST'
        => (new SceneController())->run($m[1]),

    default => (function () {
        http_response_code(404);
        echo json_encode(['error' => 'not found']);
    })(),
};

Контроллер устройств

<?php
// src/DeviceController.php

namespace App;

class DeviceController
{
    public function index(array $query): void
    {
        $type = $query['type'] ?? null;
        $room = $query['room'] ?? null;

        $sql    = 'SELECT id, name, type, room, state FROM devices WHERE 1=1';
        $params = [];

        if ($type) { $sql .= ' AND type = ?'; $params[] = $type; }
        if ($room) { $sql .= ' AND room = ?'; $params[] = $room; }

        $stmt = Database::pdo()->prepare($sql);
        $stmt->execute($params);

        echo json_encode(['devices' => $stmt->fetchAll()]);
    }

    public function show(string $id): void
    {
        $device = Device::find($id);
        if (!$device) { http_response_code(404); echo json_encode(['error' => 'not found']); return; }
        echo json_encode($device->toArray());
    }

    public function actions(string $id): void
    {
        $device = Device::find($id);
        if (!$device) { http_response_code(404); return; }
        echo json_encode(['actions' => $device->availableActions()]);
    }

    public function act(string $id, array $input): void
    {
        $device = Device::find($id);
        if (!$device) { http_response_code(404); return; }

        $action = $input['action'] ?? null;
        $params = $input['params'] ?? [];

        if (!$action || !$device->can($action)) {
            http_response_code(400);
            echo json_encode(['error' => 'invalid action']);
            return;
        }

        $result = $device->execute($action, $params);

        // Публикуем событие в WebSocket-шину для агента
        EventBus::publish('device.state_changed', [
            'device_id' => $id,
            'action'    => $action,
            'state'     => $device->state(),
        ]);

        echo json_encode(['ok' => true, 'state' => $result]);
    }
}
Совет
Заложите в API сразу три вещи: фильтры (по типу/комнате), описание доступных действий и единый формат состояния. Это сэкономит вам кучу времени при написании плагина для Hermes — он будет просто проксировать эти вызовы.

Аутентификация агента

Не используйте cookie-сессии — агенту нужен статический токен. Сгенерируйте его в БД и передавайте в заголовке X-API-Token:

// Храните токен в отдельной таблице api_tokens
// с полями: token_hash, name, scopes, created_at, last_used_at

class Auth
{
    public function verify(?string $token): bool
    {
        if (!$token) return false;
        $hash = hash('sha256', $token);
        $stmt = Database::pdo()->prepare(
            'SELECT id FROM api_tokens WHERE token_hash = ? AND revoked_at IS NULL'
        );
        $stmt->execute([$hash]);
        return (bool) $stmt->fetchColumn();
    }
}

Установка Hermes Agent

Ставим агента на ту же машину, где живёт PHP (или рядом — на Raspberry Pi, мини-ПК, домашний сервер). Hermes — Python-фреймворк, поэтому установка тривиальна:

# Стандартная установка
pip install hermes-agent

# Для ARM-плат (Raspberry Pi 3/4/5, Orange Pi)
pip install 'hermes-agent-iot[iot]==0.21.3.post1'
hermes-iot setup --profile iot

Профиль iot включает MQTT и снижает потребление памяти, что важно для домашнего железа.

Пишем свой плагин для PHP-дома

Самая интересная часть. Раз у нас нет готового плагина под кастомный PHP-бэкенд — напишем его сами. Hermes-плагин это Python-пакет, который регистрирует инструменты (функции), доступные LLM.

Структура плагина

myhome-plugin/
├── pyproject.toml
├── README.md
└── myhome/
    ├── __init__.py
    ├── plugin.py
    └── client.py

Клиент для PHP-API

# myhome/client.py
import os
import httpx
from typing import Any

class MyHomeClient:
    def __init__(self):
        self.base = os.environ["MYHOME_URL"].rstrip("/")
        self.token = os.environ["MYHOME_TOKEN"]
        self.http = httpx.Client(
            timeout=10.0,
            headers={"X-API-Token": self.token},
        )

    def list_devices(self, type_: str | None = None, room: str | None = None) -> dict:
        params: dict[str, Any] = {}
        if type_: params["type"] = type_
        if room:  params["room"] = room
        r = self.http.get(f"{self.base}/api/devices", params=params)
        r.raise_for_status()
        return r.json()

    def get_state(self, device_id: str) -> dict:
        r = self.http.get(f"{self.base}/api/device/{device_id}")
        r.raise_for_status()
        return r.json()

    def list_actions(self, device_id: str) -> dict:
        r = self.http.get(f"{self.base}/api/device/{device_id}/actions")
        r.raise_for_status()
        return r.json()

    def call_action(self, device_id: str, action: str, params: dict | None = None) -> dict:
        r = self.http.post(
            f"{self.base}/api/device/{device_id}/action",
            json={"action": action, "params": params or {}},
        )
        r.raise_for_status()
        return r.json()

    def run_scene(self, scene_id: str) -> dict:
        r = self.http.post(f"{self.base}/api/scene/{scene_id}/run")
        r.raise_for_status()
        return r.json()

Регистрация инструментов в Hermes

# myhome/plugin.py
from hermes.plugins import Plugin, tool
from .client import MyHomeClient

client = MyHomeClient()

class MyHomePlugin(Plugin):
    name = "myhome"
    version = "0.1.0"

    @tool
    async def home_list_devices(self, type_: str = "", room: str = "") -> dict:
        """Список устройств умного дома. Можно фильтровать по типу (light, climate, lock) и комнате."""
        return client.list_devices(type_ or None, room or None)

    @tool
    async def home_get_state(self, device_id: str) -> dict:
        """Получить текущее состояние устройства по его id."""
        return client.get_state(device_id)

    @tool
    async def home_list_actions(self, device_id: str) -> dict:
        """Список действий, доступных для устройства."""
        return client.list_actions(device_id)

    @tool
    async def home_call_action(
        self, device_id: str, action: str, params: dict | None = None
    ) -> dict:
        """Выполнить действие над устройством: turn_on, turn_off, set_brightness, set_temperature и т.д."""
        return client.call_action(device_id, action, params)

    @tool
    async def home_run_scene(self, scene_id: str) -> dict:
        """Запустить сценарий умного дома по его id."""
        return client.run_scene(scene_id)

Установка и конфигурация плагина

pip install -e ./myhome-plugin
hermes plugins enable myhome

Переменные окружения — в ~/.hermes/.env:

MYHOME_URL=http://127.0.0.1:8080
MYHOME_TOKEN=сгенерированный-токен-из-php-админки
Почему это работает лучше универсального плагина
Инструменты описаны в терминах вашей предметной области: у вас свои типы устройств, свои сценарии, свои названия комнат. LLM получает именно те инструменты, которые реально существуют в системе, — без лишних абстракций.

Голос: STT и TTS

Hermes не занимается звуком — это ответственность обвязки. Для приватного локального Джарвиса используем Whisper и Piper.

Распознавание речи (Whisper)

import sounddevice as sd
import numpy as np
from faster_whisper import WhisperModel

model = WhisperModel("small", device="cpu", compute_type="int8")

def listen_once(duration: float = 5.0, samplerate: int = 16000) -> str:
    audio = sd.rec(int(duration * samplerate), samplerate=samplerate, channels=1)
    audio = audio.flatten().astype(np.float32) / 32768.0
    segments, _ = model.transcribe(audio, language="ru")
    return " ".join(s.text.strip() for s in segments).strip()

Синтез речи (Piper)

import subprocess
import tempfile, os

def speak(text: str) -> None:
    with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
        wav = f.name
    subprocess.run([
        "piper",
        "--model", "ru_RU-irina-medium.onnx",
        "--output_file", wav,
    ], input=text.encode("utf-8"), check=True)
    subprocess.run(["aplay", "-q", wav], check=True)
    os.unlink(wav)

Главный цикл ассистента

import asyncio
from hermes import Agent

async def main():
    agent = Agent(
        system_prompt=(
            "Ты — Джарвис, голосовой ассистент умного дома. "
            "Отвечай кратко, по-деловому, с легкой иронией. "
            "Для управления домом используй инструменты home_*."
        ),
        plugins=["myhome"],
    )

    while True:
        text = listen_once()
        if not text or text.lower() in {"стоп", "выход"}:
            continue

        reply = await agent.chat(text)
        speak(reply)

if __name__ == "__main__":
    asyncio.run(main())

Проактивные события из PHP в агента

Настоящий Джарвис не ждет, пока его спросят. Он сам сообщает, что датчик двери сработал или что стиралка закончила цикл. Для этого ваш PHP-бэкенд должен уметь пушить события в Hermes через WebSocket-шину.

Публикация событий из PHP

<?php
// src/EventBus.php — тонкая обёртка над Redis Pub/Sub или собственным WS

namespace App;

class EventBus
{
    public static function publish(string $channel, array $payload): void
    {
        $redis = new \Redis();
        $redis->connect('127.0.0.1', 6379);
        $redis->publish('home.events', json_encode([
            'channel' => $channel,
            'payload' => $payload,
            'ts'      => time(),
        ]));
    }
}

Плагин-подписчик на Python

# myhome/events.py — слушает Redis и перекладывает события в Hermes
import asyncio, json, redis.asyncio as redis
from hermes import Agent

async def event_listener(agent: Agent, on_speak):
    r = redis.Redis(host="127.0.0.1", port=6379)
    pubsub = r.pubsub()
    await pubsub.subscribe("home.events")

    async for msg in pubsub.listen():
        if msg["type"] != "message":
            continue
        event = json.loads(msg["data"])
        prompt = (
            f"Произошло событие в доме: {event['channel']}, "
            f"данные: {json.dumps(event['payload'], ensure_ascii=False)}. "
            "Реши, нужно ли что-то сделать или сообщить пользователю. Ответь одной фразой."
        )
        reply = await agent.chat(prompt)
        if reply and not reply.startswith("IGNORE"):
            await on_speak(reply)
Защита от лавины событий
Всегда держите cooldown по устройствам: например, не чаще одного уведомления в 30 секунд на устройство. Иначе датчик движения ночью превратит Джарвиса в радио «Маяк».

Запуск и голосовые команды

Запускаем ассистента:

python assistant.py

Теперь можно говорить. Примеры фраз и то, что произойдет под капотом:

ФразаЧто делает агент
«Джарвис, выключи свет в гостиной» Зовет home_list_devices(room="living"), находит лампы, вызывает home_call_action(device_id, "turn_off")
«Какая температура в спальне?» Зовет home_get_state("climate.bedroom") и озвучивает результат
«Сделай потемнее в кабинете» Читает состояние, вычисляет новую яркость и вызывает set_brightness
«Запусти сценарий Кино» Зовет home_run_scene("cinema") — дальше уже ваш PHP решает, что гасить и что включать
«Закрой входную дверь» Проверяет состояние замка и вызывает home_call_action("lock.front", "lock")
«Джарвис, выключи свет в гостиной» — и через 800 миллисекунд лампы гаснут. Ваш PHP-бэкенд даже не знает, что команда пришла из голоса: для него это обычный POST на /api/device/light.living_room/action.

Заключение

Hermes Agent + ваш собственный PHP-бэкенд = Джарвис, полностью под вашим контролем. Что вы получаете по сравнению с Home Assistant-связкой:

🔐

Полный контроль

Свои токены, свои scopes, своя логика авторизации. Никакой чужой модели прав.

🐘

Родной PHP

Никаких прослоек вроде MQTT-мостов. Агент бьёт прямо в ваш API.

🧩

Свои инструменты

Термины предметной области: ваши сценарии, комнаты, типы устройств.

🔒

Приватность

Локальный Whisper, локальный Piper, локальная LLM. Данные не покидают дом.

Свой дом, свой API, свой Джарвис. Как и должно быть у инженера.

Теги: #diy #php #гермес_агент #джарвис #умный_дом



Категории:

Категории

← Назад к списку

Посетителей сегодня: 0
о блоге | карта блога | 📡 Подписаться на RSS

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA