feat(hass): 新增小爱音箱接入 Home Assistant Assist 的完整示例

- 新增完整的 Home Assistant 集成示例,支持文本对话与上下文会话
- 实现小爱音箱事件解析与 TTS 播报,支持实时打断功能
- 添加连续会话管理,支持区域上下文注入和会话超时结束
- 提供完整的配置系统、类型定义和错误处理
- 包含 Rust 扩展模块用于小爱音箱通信,支持音频流处理
- 添加单元测试、集成测试脚本和 Docker 部署支持
- 提供详细的使用文档和配置说明
This commit is contained in:
2026-03-04 17:11:04 +08:00
parent 0f92b0a26d
commit aafdf024d3
32 changed files with 1785 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
use open_xiaoai::services::connect::message::MessageManager;
use open_xiaoai::services::connect::rpc::RPC;
use open_xiaoai::services::audio::config::AudioConfig;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use serde_json::json;
use server::AppServer;
pub mod macros;
pub mod python;
pub mod server;
#[pyfunction]
fn on_output_data(py: Python, data: Py<PyBytes>) -> PyResult<Bound<PyAny>> {
let bytes = data.as_bytes(py).to_vec();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let _ = MessageManager::instance()
.send_stream("play", bytes, None)
.await;
Ok(())
})
}
#[pyfunction]
fn start_server(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async {
AppServer::run().await;
Ok(())
})
}
#[pyfunction]
fn run_shell(py: Python, script: String, timeout_millis: f64) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let res = RPC::instance()
.call_remote("run_shell", Some(json!(script)), Some(timeout_millis as u64))
.await;
let result = match res {
Err(e) => format!("run_shell error: {}", e),
Ok(res) => match res.data {
Some(data) => serde_json::to_string(&data).unwrap_or_default(),
None => String::new(),
},
};
Ok(result)
})
}
#[pyfunction]
fn start_recording(py: Python, config_json: Option<String>) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let config = config_json
.and_then(|text| serde_json::from_str::<AudioConfig>(&text).ok());
let res = RPC::instance()
.call_remote("start_recording", config.map(|c| json!(c)), None)
.await;
Ok(match res {
Ok(_) => true,
Err(_) => false,
})
})
}
#[pyfunction]
fn stop_recording(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let res = RPC::instance().call_remote("stop_recording", None, None).await;
Ok(match res {
Ok(_) => true,
Err(_) => false,
})
})
}
#[pyfunction]
fn start_play(py: Python, config_json: Option<String>) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let config = config_json
.and_then(|text| serde_json::from_str::<AudioConfig>(&text).ok());
let res = RPC::instance()
.call_remote("start_play", config.map(|c| json!(c)), None)
.await;
Ok(match res {
Ok(_) => true,
Err(_) => false,
})
})
}
#[pyfunction]
fn stop_play(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let res = RPC::instance().call_remote("stop_play", None, None).await;
Ok(match res {
Ok(_) => true,
Err(_) => false,
})
})
}
#[pymodule]
fn open_xiaoai_server(_py: Python, m: Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(start_server, &m)?)?;
m.add_function(wrap_pyfunction!(on_output_data, &m)?)?;
m.add_function(wrap_pyfunction!(run_shell, &m)?)?;
m.add_function(wrap_pyfunction!(start_recording, &m)?)?;
m.add_function(wrap_pyfunction!(stop_recording, &m)?)?;
m.add_function(wrap_pyfunction!(start_play, &m)?)?;
m.add_function(wrap_pyfunction!(stop_play, &m)?)?;
crate::python::init_module(&m)?;
Ok(())
}
+7
View File
@@ -0,0 +1,7 @@
#[macro_export]
macro_rules! pylog {
($($arg:tt)*) => {{
println!($($arg)*);
}};
}
+64
View File
@@ -0,0 +1,64 @@
use pyo3::prelude::*;
use std::collections::HashMap;
use std::sync::{Arc, LazyLock, RwLock};
pub struct PythonManager {
functions: Arc<RwLock<HashMap<String, PyObject>>>,
}
static INSTANCE: LazyLock<PythonManager> = LazyLock::new(PythonManager::new);
impl PythonManager {
pub fn new() -> Self {
Self {
functions: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn instance() -> &'static Self {
&INSTANCE
}
fn register_fn(&self, key: &str, function: PyObject) -> PyResult<()> {
let mut functions = self.functions.write().unwrap();
functions.insert(key.to_string(), function);
Ok(())
}
fn unregister_fn(&self, key: &str) -> PyResult<()> {
let mut functions = self.functions.write().unwrap();
functions.remove(key);
Ok(())
}
pub fn call_fn(&self, key: &str, arg: Option<PyObject>) -> PyResult<PyObject> {
let functions = self.functions.read().unwrap();
if let Some(function) = functions.get(key) {
Python::with_gil(|py| match arg {
None => function.call0(py),
Some(arg) => function.call1(py, (arg,)),
})
} else {
Err(pyo3::exceptions::PyKeyError::new_err(format!(
"未找到函数: {}",
key
)))
}
}
}
#[pyfunction]
fn register_fn(key: &str, function: PyObject) -> PyResult<()> {
PythonManager::instance().register_fn(key, function)
}
#[pyfunction]
fn unregister_fn(key: &str) -> PyResult<()> {
PythonManager::instance().unregister_fn(key)
}
pub fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(register_fn, m)?)?;
m.add_function(wrap_pyfunction!(unregister_fn, m)?)?;
Ok(())
}
+104
View File
@@ -0,0 +1,104 @@
use crate::python::PythonManager;
use open_xiaoai::base::{AppError, VERSION};
use open_xiaoai::services::connect::data::{Event, Request, Response, Stream};
use open_xiaoai::services::connect::handler::MessageHandler;
use open_xiaoai::services::connect::message::{MessageManager, WsStream};
use open_xiaoai::services::connect::rpc::RPC;
use pyo3::types::{PyBytes, PyString};
use pyo3::Python;
use serde_json::json;
use std::sync::{LazyLock, RwLock};
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::accept_async;
static REMOTE_ADDR: LazyLock<RwLock<Option<String>>> = LazyLock::new(|| RwLock::new(None));
pub struct AppServer;
impl AppServer {
pub async fn connect(stream: TcpStream) -> Result<WsStream, AppError> {
let ws_stream = accept_async(stream).await?;
Ok(WsStream::Server(ws_stream))
}
pub async fn run() {
let addr = "0.0.0.0:4399";
let listener = TcpListener::bind(&addr)
.await
.expect(format!("❌ 绑定地址失败: {}", &addr).as_str());
crate::pylog!("✅ 已启动: {:?}", addr);
while let Ok((stream, addr)) = listener.accept().await {
AppServer::handle_connection(stream, addr).await;
}
}
async fn handle_connection(stream: TcpStream, addr: std::net::SocketAddr) {
let Ok(ws_stream) = AppServer::connect(stream).await else {
crate::pylog!("❌ 连接异常: {}", addr);
return;
};
crate::pylog!("✅ 已连接: {:?}", addr);
{
let mut remote = REMOTE_ADDR.write().unwrap();
*remote = Some(addr.ip().to_string());
}
AppServer::init(ws_stream).await;
if let Err(e) = MessageManager::instance().process_messages().await {
crate::pylog!("❌ 消息处理异常: {}", e);
}
AppServer::dispose().await;
{
let mut remote = REMOTE_ADDR.write().unwrap();
*remote = None;
}
crate::pylog!("❌ 已断开连接");
}
async fn init(ws_stream: WsStream) {
MessageManager::instance().init(ws_stream).await;
MessageHandler::<Event>::instance()
.set_handler(on_event)
.await;
MessageHandler::<Stream>::instance()
.set_handler(on_stream)
.await;
let rpc = RPC::instance();
rpc.add_command("get_version", get_version).await;
}
async fn dispose() {
MessageManager::instance().dispose().await;
}
}
async fn get_version(_: Request) -> Result<Response, AppError> {
let data = json!(VERSION.to_string());
Ok(Response::from_data(data))
}
async fn on_stream(stream: Stream) -> Result<(), AppError> {
let Stream { tag, bytes, .. } = stream;
match tag.as_str() {
"record" => {
let data = Python::with_gil(|py| PyBytes::new(py, &bytes).into());
PythonManager::instance().call_fn("on_input_data", Some(data))?;
}
_ => {}
}
Ok(())
}
async fn on_event(event: Event) -> Result<(), AppError> {
let mut event_json_value = serde_json::to_value(&event)?;
if let Some(event_obj) = event_json_value.as_object_mut() {
let remote = REMOTE_ADDR.read().unwrap().clone();
if let Some(ip) = remote {
event_obj.insert("remote_addr".to_string(), serde_json::Value::String(ip));
}
}
let event_json = serde_json::to_string(&event_json_value)?;
let data = Python::with_gil(|py| PyString::new(py, &event_json).into());
PythonManager::instance().call_fn("on_event", Some(data))?;
Ok(())
}