mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 15:13:55 +08:00
@@ -75,6 +75,7 @@ docs/_build/
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
*.pid
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
@@ -81,15 +81,16 @@ src/main/java/xiaozhi/AdminApplication.java
|
||||
执行以下命令生产jar包
|
||||
|
||||
```
|
||||
mvn install
|
||||
mvn clean install
|
||||
```
|
||||
|
||||
把jar包放在服务器上,执行
|
||||
|
||||
```
|
||||
nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.active=dev >/dev/null &
|
||||
nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.activate=dev
|
||||
```
|
||||
|
||||
|
||||
# 接口文档
|
||||
启动后打开:http://localhost:8002/xiaozhi-esp32-api/doc.html
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class AdminController {
|
||||
AdminPageUserDTO dto = new AdminPageUserDTO();
|
||||
dto.setMobile((String) params.get("mobile"));
|
||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||
dto.setPage((String) params.get("pages"));
|
||||
dto.setPage((String) params.get(Constant.PAGE));
|
||||
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||
|
||||
@@ -1,35 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/base.xml" />
|
||||
<logger name="org.springframework.web" level="INFO"/>
|
||||
<logger name="org.springboot.sample" level="TRACE" />
|
||||
|
||||
<!-- 开发、测试环境 -->
|
||||
<!-- 启用JansiConsoleAppender以确保控制台输出有颜色 -->
|
||||
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
|
||||
|
||||
<!-- 确保日志目录存在 -->
|
||||
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
|
||||
|
||||
<!-- 定义日志文件存储位置 -->
|
||||
<property name="LOG_HOME" value="./logs" />
|
||||
|
||||
<!-- 使用自定义的初始化监听器确保日志目录存在 -->
|
||||
<define name="LOGBACK_DIR_CHECK" class="ch.qos.logback.core.property.FileExistsPropertyDefiner">
|
||||
<path>${LOG_HOME}</path>
|
||||
<createIfMissing>true</createIfMissing>
|
||||
</define>
|
||||
|
||||
<!-- 引入Spring Boot默认配置 -->
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
|
||||
|
||||
<!-- 控制台输出配置 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}) - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件输出配置 - 所有日志 -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/xiaozhi-esp32-api.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/xiaozhi-esp32-api.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>2GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件输出配置 - 仅错误日志 -->
|
||||
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/error.log</file>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>ERROR</level>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>1GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 开发环境配置 -->
|
||||
<springProfile name="dev">
|
||||
<logger name="org.springframework.web" level="INFO"/>
|
||||
<logger name="org.springboot.sample" level="INFO" />
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<!-- 开发环境也记录日志文件 -->
|
||||
<appender-ref ref="FILE" />
|
||||
<appender-ref ref="ERROR_FILE" />
|
||||
</root>
|
||||
<logger name="xiaozhi" level="DEBUG" />
|
||||
<logger name="org.springframework.web" level="INFO" />
|
||||
<logger name="org.springboot.sample" level="INFO" />
|
||||
</springProfile>
|
||||
|
||||
<!-- 生产环境 -->
|
||||
|
||||
<!-- 测试环境配置 -->
|
||||
<springProfile name="test">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
<appender-ref ref="ERROR_FILE" />
|
||||
</root>
|
||||
<logger name="xiaozhi" level="INFO" />
|
||||
<logger name="org.springframework.web" level="INFO" />
|
||||
<logger name="org.springboot.sample" level="INFO" />
|
||||
</springProfile>
|
||||
|
||||
<!-- 生产环境配置 - 合并两个版本的最佳实践 -->
|
||||
<springProfile name="prod">
|
||||
<appender name="LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<!-- 使用本地日志路径 -->
|
||||
<appender name="PROD_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/xiaozhi-esp32-api.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<!--日志文件输出的文件名-->
|
||||
<FileNamePattern>/home/system/logs/admin.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
|
||||
<!--日志大小-->
|
||||
<maxFileSize>1MB</maxFileSize>
|
||||
<!--日志文件保留天数-->
|
||||
<maxHistory>7</maxHistory>
|
||||
<fileNamePattern>${LOG_HOME}/xiaozhi-esp32-api.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>2GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
<logger name="org.springframework.web" level="ERROR"/>
|
||||
<logger name="org.springboot.sample" level="ERROR"/>
|
||||
<logger name="xiaozhi" level="INFO"/>
|
||||
|
||||
<!-- 生产环境错误日志 -->
|
||||
<appender name="PROD_ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/error.log</file>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>ERROR</level>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>1GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 生产环境根日志配置 -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="PROD_FILE" />
|
||||
<appender-ref ref="PROD_ERROR_FILE" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
|
||||
<!-- 特定包的日志级别 -->
|
||||
<logger name="xiaozhi" level="INFO" />
|
||||
<logger name="org.springframework.web" level="ERROR" />
|
||||
<logger name="org.springboot.sample" level="ERROR" />
|
||||
</springProfile>
|
||||
|
||||
</configuration>
|
||||
|
||||
<!-- 确保日志目录存在 -->
|
||||
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
|
||||
<resetJUL>true</resetJUL>
|
||||
</contextListener>
|
||||
</configuration>
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
开发使用代码编辑器,导入项目时,选择`manager-web`文件夹作为项目目录
|
||||
|
||||
参照[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)开发
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
@@ -187,6 +187,14 @@ ASR:
|
||||
access_token: 你的火山引擎语音合成服务access_token
|
||||
cluster: volcengine_input_common
|
||||
output_dir: tmp/
|
||||
TencentASR:
|
||||
# token申请地址:https://console.cloud.tencent.com/cam/capi
|
||||
# 免费领取资源:https://console.cloud.tencent.com/asr/resourcebundle
|
||||
type: tencent
|
||||
appid: 你的腾讯语音合成服务appid
|
||||
secret_id: 你的腾讯语音合成服务secret_id
|
||||
secret_key: 你的腾讯语音合成服务secret_key
|
||||
output_dir: tmp/
|
||||
VAD:
|
||||
SileroVAD:
|
||||
threshold: 0.5
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional, Tuple, List
|
||||
import wave
|
||||
import opuslib_next
|
||||
|
||||
import requests
|
||||
from core.providers.asr.base import ASRProviderBase
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
class ASRProvider(ASRProviderBase):
|
||||
API_URL = "https://asr.tencentcloudapi.com"
|
||||
API_VERSION = "2019-06-14"
|
||||
FORMAT = "pcm" # 支持的音频格式:pcm, wav, mp3
|
||||
|
||||
def __init__(self, config: dict, delete_audio_file: bool = True):
|
||||
self.secret_id = config.get("secret_id")
|
||||
self.secret_key = config.get("secret_key")
|
||||
self.output_dir = config.get("output_dir")
|
||||
|
||||
# 确保输出目录存在
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
|
||||
def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str:
|
||||
"""将Opus音频数据解码并保存为WAV文件"""
|
||||
|
||||
file_name = f"tencent_asr_{session_id}_{uuid.uuid4()}.wav"
|
||||
file_path = os.path.join(self.output_dir, file_name)
|
||||
|
||||
decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道
|
||||
pcm_data = []
|
||||
|
||||
for opus_packet in opus_data:
|
||||
try:
|
||||
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
|
||||
pcm_data.append(pcm_frame)
|
||||
except opuslib_next.OpusError as e:
|
||||
logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
||||
|
||||
with wave.open(file_path, "wb") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2) # 2 bytes = 16-bit
|
||||
wf.setframerate(16000)
|
||||
wf.writeframes(b"".join(pcm_data))
|
||||
|
||||
return file_path
|
||||
|
||||
@staticmethod
|
||||
def decode_opus(opus_data: List[bytes]) -> bytes:
|
||||
"""将Opus音频数据解码为PCM数据"""
|
||||
import opuslib_next
|
||||
|
||||
decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道
|
||||
pcm_data = []
|
||||
|
||||
for opus_packet in opus_data:
|
||||
try:
|
||||
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
|
||||
pcm_data.append(pcm_frame)
|
||||
except opuslib_next.OpusError as e:
|
||||
logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
||||
|
||||
return b"".join(pcm_data)
|
||||
|
||||
async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
if not opus_data:
|
||||
logger.bind(tag=TAG).warn("音频数据为空!")
|
||||
return None, None
|
||||
|
||||
try:
|
||||
# 检查配置是否已设置
|
||||
if not self.secret_id or not self.secret_key:
|
||||
logger.bind(tag=TAG).error("腾讯云语音识别配置未设置,无法进行识别")
|
||||
return None, None
|
||||
|
||||
# 将Opus音频数据解码为PCM
|
||||
pcm_data = self.decode_opus(opus_data)
|
||||
|
||||
# 将音频数据转换为Base64编码
|
||||
base64_audio = base64.b64encode(pcm_data).decode('utf-8')
|
||||
|
||||
# 构建请求体
|
||||
request_body = self._build_request_body(base64_audio)
|
||||
|
||||
# 获取认证头
|
||||
timestamp, authorization = self._get_auth_headers(request_body)
|
||||
|
||||
# 发送请求
|
||||
start_time = time.time()
|
||||
result = self._send_request(request_body, timestamp, authorization)
|
||||
|
||||
if result:
|
||||
logger.bind(tag=TAG).debug(f"腾讯云语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result}")
|
||||
|
||||
return result, None
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"处理音频时发生错误!{e}", exc_info=True)
|
||||
return None, None
|
||||
|
||||
def _build_request_body(self, base64_audio: str) -> str:
|
||||
"""构建请求体"""
|
||||
request_map = {
|
||||
"ProjectId": 0,
|
||||
"SubServiceType": 2, # 一句话识别
|
||||
"EngSerViceType": "16k_zh", # 中文普通话通用
|
||||
"SourceType": 1, # 音频数据来源为语音文件
|
||||
"VoiceFormat": self.FORMAT, # 音频格式
|
||||
"Data": base64_audio, # Base64编码的音频数据
|
||||
"DataLen": len(base64_audio) # 数据长度
|
||||
}
|
||||
return json.dumps(request_map)
|
||||
|
||||
def _get_auth_headers(self, request_body: str) -> Tuple[str, str]:
|
||||
"""获取认证头"""
|
||||
try:
|
||||
# 获取当前UTC时间戳
|
||||
now = datetime.now(timezone.utc)
|
||||
timestamp = str(int(now.timestamp()))
|
||||
date = now.strftime("%Y-%m-%d")
|
||||
|
||||
# 服务名称必须是 "asr"
|
||||
service = "asr"
|
||||
|
||||
# 拼接凭证范围
|
||||
credential_scope = f"{date}/{service}/tc3_request"
|
||||
|
||||
# 使用TC3-HMAC-SHA256签名方法
|
||||
algorithm = "TC3-HMAC-SHA256"
|
||||
|
||||
# 构建规范请求字符串
|
||||
http_request_method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_query_string = ""
|
||||
|
||||
# 注意:头部信息需要按照ASCII升序排列,且key和value都转为小写
|
||||
# 必须包含content-type和host头部
|
||||
content_type = "application/json; charset=utf-8"
|
||||
host = "asr.tencentcloudapi.com"
|
||||
action = "SentenceRecognition" # 接口名称
|
||||
|
||||
# 构建规范头部信息,注意顺序和格式
|
||||
canonical_headers = f"content-type:{content_type.lower()}\n" + \
|
||||
f"host:{host.lower()}\n" + \
|
||||
f"x-tc-action:{action.lower()}\n"
|
||||
|
||||
signed_headers = "content-type;host;x-tc-action"
|
||||
|
||||
# 请求体哈希值
|
||||
payload_hash = self._sha256_hex(request_body)
|
||||
|
||||
# 构建规范请求字符串
|
||||
canonical_request = f"{http_request_method}\n" + \
|
||||
f"{canonical_uri}\n" + \
|
||||
f"{canonical_query_string}\n" + \
|
||||
f"{canonical_headers}\n" + \
|
||||
f"{signed_headers}\n" + \
|
||||
f"{payload_hash}"
|
||||
|
||||
# 计算规范请求的哈希值
|
||||
hashed_canonical_request = self._sha256_hex(canonical_request)
|
||||
|
||||
# 构建待签名字符串
|
||||
string_to_sign = f"{algorithm}\n" + \
|
||||
f"{timestamp}\n" + \
|
||||
f"{credential_scope}\n" + \
|
||||
f"{hashed_canonical_request}"
|
||||
|
||||
# 计算签名密钥
|
||||
secret_date = self._hmac_sha256(f"TC3{self.secret_key}", date)
|
||||
secret_service = self._hmac_sha256(secret_date, service)
|
||||
secret_signing = self._hmac_sha256(secret_service, "tc3_request")
|
||||
|
||||
# 计算签名
|
||||
signature = self._bytes_to_hex(self._hmac_sha256(secret_signing, string_to_sign))
|
||||
|
||||
# 构建授权头
|
||||
authorization = f"{algorithm} " + \
|
||||
f"Credential={self.secret_id}/{credential_scope}, " + \
|
||||
f"SignedHeaders={signed_headers}, " + \
|
||||
f"Signature={signature}"
|
||||
|
||||
return timestamp, authorization
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"生成认证头失败: {e}", exc_info=True)
|
||||
raise RuntimeError(f"生成认证头失败: {e}")
|
||||
|
||||
def _send_request(self, request_body: str, timestamp: str, authorization: str) -> Optional[str]:
|
||||
"""发送请求到腾讯云API"""
|
||||
headers = {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Host": "asr.tencentcloudapi.com",
|
||||
"Authorization": authorization,
|
||||
"X-TC-Action": "SentenceRecognition",
|
||||
"X-TC-Version": self.API_VERSION,
|
||||
"X-TC-Timestamp": timestamp,
|
||||
"X-TC-Region": "ap-shanghai"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(self.API_URL, headers=headers, data=request_body)
|
||||
|
||||
if not response.ok:
|
||||
raise IOError(f"请求失败: {response.status_code} {response.reason}")
|
||||
|
||||
response_json = response.json()
|
||||
|
||||
# 检查是否有错误
|
||||
if "Response" in response_json and "Error" in response_json["Response"]:
|
||||
error = response_json["Response"]["Error"]
|
||||
error_code = error["Code"]
|
||||
error_message = error["Message"]
|
||||
raise IOError(f"API返回错误: {error_code}: {error_message}")
|
||||
|
||||
# 提取识别结果
|
||||
if "Response" in response_json and "Result" in response_json["Response"]:
|
||||
return response_json["Response"]["Result"]
|
||||
else:
|
||||
logger.bind(tag=TAG).warn(f"响应中没有识别结果: {response_json}")
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"发送请求失败: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _sha256_hex(self, data: str) -> str:
|
||||
"""计算字符串的SHA256哈希值"""
|
||||
digest = hashlib.sha256(data.encode('utf-8')).digest()
|
||||
return self._bytes_to_hex(digest)
|
||||
|
||||
def _hmac_sha256(self, key, data: str) -> bytes:
|
||||
"""计算HMAC-SHA256"""
|
||||
if isinstance(key, str):
|
||||
key = key.encode('utf-8')
|
||||
|
||||
return hmac.new(key, data.encode('utf-8'), hashlib.sha256).digest()
|
||||
|
||||
def _bytes_to_hex(self, bytes_data: bytes) -> str:
|
||||
"""字节数组转十六进制字符串"""
|
||||
return ''.join(f"{b:02x}" for b in bytes_data)
|
||||
@@ -10,7 +10,7 @@ HASS_CACHE = {}
|
||||
def append_devices_to_prompt(conn):
|
||||
if conn.use_function_call_mode:
|
||||
funcs = conn.config["Intent"]["function_call"].get("functions", [])
|
||||
if "hass_get_state" in funcs or "hass_get_state" in funcs:
|
||||
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
||||
prompt = "下面是我家智能设备,可以通过homeassistant控制\n"
|
||||
devices = conn.config["plugins"]["home_assistant"].get("devices", [])
|
||||
if len(devices) == 0:
|
||||
@@ -27,9 +27,13 @@ def initialize_hass_handler(conn):
|
||||
if HASS_CACHE == {}:
|
||||
if conn.use_function_call_mode:
|
||||
funcs = conn.config["Intent"]["function_call"].get("functions", [])
|
||||
if "hass_get_state" in funcs or "hass_get_state" in funcs:
|
||||
HASS_CACHE['base_url'] = conn.config["plugins"]["home_assistant"].get("base_url")
|
||||
HASS_CACHE['api_key'] = conn.config["plugins"]["home_assistant"].get("api_key")
|
||||
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
||||
HASS_CACHE["base_url"] = conn.config["plugins"]["home_assistant"].get(
|
||||
"base_url"
|
||||
)
|
||||
HASS_CACHE["api_key"] = conn.config["plugins"]["home_assistant"].get(
|
||||
"api_key"
|
||||
)
|
||||
|
||||
check_model_key("home_assistant", HASS_CACHE['api_key'])
|
||||
check_model_key("home_assistant", HASS_CACHE["api_key"])
|
||||
return HASS_CACHE
|
||||
|
||||
Reference in New Issue
Block a user