适用版本:
1.1.0
适用 Home Assistant:支持 Config Flow /panel_custom/ 签名链接(async_sign_path)的版本(2024.x 及以上)
依赖:requests(唯一第三方依赖)
- 集成名称:NPS 隧道管理(Home Assistant 集成域
nps) - 对接对象:ehang-io/nps —— 一款轻量级、高性能、功能强大的内网穿透代理服务器。
- 集成目标:把 NPS 服务端的运行数据完整映射为 HA 实体,并额外提供一个可视化管理面板(隧道 / 客户端 / 域名映射的增删改启停),面板可在侧边栏、设备页悬浮窗中打开。
| 能力 | 说明 |
|---|---|
| 仪表盘统计 | 17 个中文名称的传感器:客户端总数/在线数、连接数、各类隧道数量、进出流量、实时速率等 |
| 隧道控制 | 每个 NPS 隧道生成一个 switch 实体,支持开启/停止 |
| 客户端控制 | 每个 NPC 客户端生成一个 switch 实体,支持启用/禁用(断开/恢复连接) |
| 域名映射统计 | 读取并展示 HTTP(S) 域名反向代理数量(部分旧版 NPS 无此接口,已做容错) |
| 管理面板 | 可视化界面(panel/nps-panel.js):隧道 / 客户端 / 域名映射三个选项卡,支持搜索、类型筛选、新建、编辑、复制、启停、删除 |
| 设备页悬浮窗 | 设备页点「访问」不再整页跳转,而是弹出可拖动、可缩放的 iframe 悬浮窗(panel/nps-embed.js) |
| 访问密钥传感器 | 密钥加盐 + 整点轮换(旧密钥宽限 1 小时),状态 = 剩余分钟数,属性含密钥、剩余时长、可直接打开的访问地址 |
| 管理面板访问开关 | 一键禁用/启用面板访问(禁用后设备链接、面板页面、WS/HTTP 接口全部拒绝) |
| 服务器时间 | 每次轮询顺带读取 NPS 服务端时间(/auth/get_time)存到 coordinator.server_time,便于排查时钟偏差(服务端要求时间戳偏差 ≤ 20 秒) |
| 多实例 | 以 unique_id = nps_{host} 支持配置多个 NPS 服务端,面板内可切换实例 |
| 轮询间隔可调 | 选项流可设置 5~3600 秒(默认 30 秒),修改后自动重载 |
| 入口 | 打开方式 | 说明 |
|---|---|---|
| 侧边栏「NPS 管理」 | panel_custom 面板(路径 /nps) |
整页加载,需 HA 管理员;模块 nps-panel.js 同时被注册为全局前端资源 |
| 设备页「访问」 | 设备 configuration_url = 签名地址 |
被 nps-embed.js 拦截,改为同页悬浮窗 |
| 任意浏览器(含外部) | http(s)://<HA>/nps/console?entry=<entry_id>&authSig=<jwt>http(s)://<HA>/nps/console?entry=<entry_id>&key=<密钥> |
签名地址 24 小时有效;密钥短链接整点轮换(旧密钥宽限 1 小时)。都可不登录 HA 直接打开 |
y:\custom_components\nps\
├── __init__.py # 集成入口:注册面板、创建 client/coordinator、转发平台
├── api.py # NpsApiClient:HTTP 请求 + MD5 签名鉴权 + 全部增删改启停接口
├── config_flow.py # UI 配置流 + 选项流(scan_interval)
├── const.py # 全局常量(DOMAIN / CONF_SCAN_INTERVAL / CONF_PANEL_ACCESS)
├── coordinator.py # NpsDataUpdateCoordinator:周期轮询,聚合仪表盘数据
├── console_auth.py # 签名地址签发、会话 Cookie、authSig 校验、面板密钥(加盐整点轮换)
├── helpers.py # 设备信息、面板地址、访问开关读写
├── panel.py # 管理面板后端:websocket 命令 + 独立页面 + HTTP 数据接口
├── sensor.py # 19 个 sensor:17 个仪表盘 + 面板访问密钥 + 所有实体
├── switch.py # NpsTunnelSwitch / NpsClientSwitch / NpsPanelAccessSwitch
├── panel/
│ ├── nps-panel.js # 管理面板 webcomponent(nps-panel,侧边栏与悬浮窗共用)
│ └── nps-embed.js # 设备页「访问」悬浮窗拦截模块(add_extra_js_url 注入)
├── manifest.json # HA 集成元数据
├── strings.json # HA 基准翻译文件(必须存在)
├── translations/
│ └── zh-Hans.json # 简体中文 UI 文案
└── brand/
└── icon.png # 集成图标
| 文件 | 职责 |
|---|---|
__init__.py |
async_setup 里注册面板(async_setup_panel);async_setup_entry 创建 NpsApiClient + NpsDataUpdateCoordinator,存入 hass.data[DOMAIN][entry_id],首次刷新验证连通性后转发 [SWITCH, SENSOR] 平台 |
config_flow.py |
输入 host(ip:port)+ auth_key;test_connection() 验证;unique_id = nps_{host} 去重;选项流只含 scan_interval(OptionsFlowWithReload,改完自动重载) |
api.py |
唯一网络层。NpsApiClient 用 MD5 签名 + POST form 调用 NPS 官方 API;禁止 302 重定向;同一路由支持多候选路径(尾部斜杠差异) |
coordinator.py |
30 秒(可配)轮询;产出 data(隧道)、clients_data、hosts_data、dashboard_data、server_time |
sensor.py |
按 DASHBOARD_SENSORS 字典表批量创建 17 个统计传感器;NpsAccessKeySensor(面板访问密钥);NpsAllEntitiesSensor(所有实体汇总) |
switch.py |
遍历 coordinator.data 建隧道开关、遍历 clients_data 建客户端开关(NoDisplay 跳过),末尾追加「管理面板访问」开关 |
console_auth.py |
签发/缓存签名地址(24h,缓存 1h)、HMAC 会话票据(12h)、校验 authSig、生成/校验面板密钥(加盐整点轮换 + 一个周期宽限) |
helpers.py |
build_device_info() 统一设备信息与 configuration_url;get_console_url()(受开关门控);set_panel_access() 持久化并同步设备链接 |
panel.py |
后端命令分发 _async_dispatch()(18 种动作)、websocket 命令 nps/action、独立页面 GET /nps/console、HTTP 接口 POST /nps/api/action、静态资源与面板注册 |
panel/nps-panel.js |
面板 UI(纯 Web Component + Shadow DOM);_call() 支持 websocket / HTTP 双通道 |
panel/nps-embed.js |
捕获阶段拦截设备页「访问」链接,弹可拖动/缩放的 iframe 悬浮窗 |
┌─────────────────────┐
│ NPS Server (nps) │
│ HTTP API │
└──────────┬──────────┘
│ POST + form-data
│ 鉴权: md5(auth_key + timestamp)
▼
┌─────────────────────┐
│ NpsApiClient │ api.py
│ - get_tunnels │ (同步, requests.Session)
│ - get_clients │
│ - get_host_list │
│ - start/stop/... │
└──────────┬──────────┘
│ 包装为 async_add_executor_job
▼
┌─────────────────────────────┐
│ NpsDataUpdateCoordinator │ coordinator.py
│ - _async_update_data() │ 默认 30s
│ - data: 隧道列表 │
│ - dashboard_data: 聚合统计 │
│ - clients_data: 客户端 │
│ - hosts_data: Host 映射 │
└──────────┬──────────────────┘
│ listener 通知
┌───────┴────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ sensors │ │ switches │
│ 19 个 │ │ N 隧道 │
│ │ │ + M 客户 │
│ │ │ + 面板访问│
└──────────┘ └────┬─────┘
│ 用户操作 switch
▼
api.start_tunnel / stop_tunnel
api.change_client_status
│ 睡眠 REFRESH_DELAY=3s
▼
coordinator.async_request_refresh()
┌──────────────────────────────┐
│ 设备页「访问」或侧边栏 │
└──────────┬───────────────────┘
│
┌──────────────────────┼─────────────────────────┐
│ HA 前端内 │ 独立页面(iframe / 新标签)│
▼ ▼
websocket nps/action GET /nps/console?entry=&authSig= (或 &key=)
(require_admin) │ 校验 authSig / 面板密钥
│ │ Set-Cookie: nps_panel_sess(12h)
│ ▼
│ POST /nps/api/action(Cookie 鉴权)
│ │
└──────────┬───────────┘
▼
_async_dispatch() → api.* → NPS
│
coordinator.async_refresh()
│
返回最新快照(tunnels/clients/hosts/dashboard)
面板前端双通道:nps-panel.js 的 _call(action, data):
_httpMode === true(独立页面,startHttp(entryId)设置)→fetch("/nps/api/action", {method:"POST", credentials:"same-origin", ...});- 否则(HA 前端内)→
hass.connection.sendMessagePromise({type:"nps/action", ...})。
两者返回同一份快照结构,UI 代码无需区分。
| 约束 | 说明 |
|---|---|
| 签名算法 | sign = md5(auth_key + timestamp).hexdigest(),32 位小写 |
| 鉴权失败 | NPS 返回 302 → /login/index;必须 allow_redirects=False,否则会拿到 HTML 登录页 |
| 业务错误 | 响应 JSON status == 0 表示失败;msg 字段说明原因 |
| 隧道类型 | 支持 tcp / udp / socks5 / httpProxy / secret / p2p / file 共 7 种 |
| 获取隧道 | /index/gettunnel 必须按 type 逐个查询再合并去重(type="" 且 clientId=0 时会过滤掉所有真实隧道) |
| 路由斜杠 | 不同版本/分支的尾部斜杠不一致,api.ROUTES 为每个接口列了候选路径并按序回退 |
| 隧道启停 | /index/start 与 /index/stop,参数 id |
| 客户端启停 | /client/change_status,参数 id + status(字符串 "true" / "false") |
| 服务器时间 | /auth/get_time(鉴权控制器中),用于诊断时钟漂移 |
- 已部署 NPS 服务端,并能在浏览器访问 Web 控制台。
- NPS 服务端的时间与 Home Assistant 主机时间偏差应 ≤ 20 秒(超出后 MD5 签名会被服务端拒绝)。
- 已知 NPS 服务端的
ip:port和nps.conf中的auth_key(不是 Web 登录用户名/密码)。 - HA 必须能确定自己的访问地址(
internal_url/external_url,或 Nabu Casa 云端地址)。否则无法签发面板签名链接,设备页不会出现「访问」按钮,密钥实体为unavailable。
将整个 nps 目录复制到 HA 配置目录的 custom_components/nps/ 即可。重启 Home Assistant。
重启后会自动注册:
- 侧边栏面板
/nps(panel_custom,需要管理员); - 静态资源
/nps_static/nps-panel.js、/nps_static/nps-embed.js; - 全局前端模块(
add_extra_js_url):设备页「访问」的悬浮窗拦截; - 视图
GET /nps/console、POST /nps/api/action; - websocket 命令
nps/action。
设置 → 设备与服务 → 集成 → "+" 添加集成 → 搜索 "NPS 隧道管理"
| 字段 | 示例 | 说明 |
|---|---|---|
| NPS 服务器地址(ip:port) | 192.168.1.100:8080 |
必填,需含冒号 |
| API 认证密钥(nps.conf 中的 auth_key) | your_auth_key_here |
必填,原始值,不是 Web 登录密码 |
提交后集成会立刻调用 test_connection() 拉取一条隧道进行验证:
- ✅ 成功 → 创建配置入口,跳转到设备详情页。
- ❌ 失败 → 提示
connection_error:- 服务器地址和端口是否正确且可访问
- auth_key 是否为 nps.conf 中的原始值
- NPS 服务端时间与本地时间偏差是否超过 20 秒
每个 NPS 服务端用 host 作为唯一标识(unique_id = nps_{host}),可重复添加多个不同 host 的实例。所有实例共用同一套面板/接口注册,面板顶部会自动出现实例下拉框,可切换查看不同服务端。
设置 → 设备与服务 → 集成 → NPS → 配置
| 选项 | 范围 | 默认 | 说明 |
|---|---|---|---|
scan_interval |
5 ~ 3600 秒 | 30 | 轮询间隔;基于 OptionsFlowWithReload,保存后自动重载集成 |
服务器地址与 auth_key 仍不可在选项里修改,如需变更请删除原集成后重新添加。
所有实体归属于同一台虚拟设备 NPS Server ({host}),便于在 HA 设备页中统一查看。设备 configuration_url 指向管理面板的签名地址(受「管理面板访问」开关控制)。
unique_id格式:{entry_id}_nps_{key};entity_id由固定的suggested_object_id决定(见 4.1.4)。
| Key | 名称 | entity_id | 单位 | 图标 | StateClass | Divisor |
|---|---|---|---|---|---|---|
clientCount |
客户端总数 | sensor.client_total |
— | mdi:account-group |
TOTAL | 1 |
clientOnlineCount |
在线客户端 | sensor.clients_online |
— | mdi:account-check |
TOTAL | 1 |
totalConnections |
连接总数 | sensor.total_connections |
— | mdi:connection |
MEASUREMENT | 1 |
inletFlowCount |
入口流量 | sensor.inlet_traffic |
GB | mdi:download-network-outline |
TOTAL_INCREASING | 1/1024³ |
exportFlowCount |
出口流量 | sensor.export_traffic |
GB | mdi:upload-network-outline |
TOTAL_INCREASING | 1/1024³ |
tunnelTotal |
隧道总数 | sensor.tunnel_total |
— | mdi:tunnel-variant |
TOTAL | 1 |
tcpC |
TCP 隧道 | sensor.tcp_tunnels |
— | mdi:tunnel |
TOTAL | 1 |
udpCount |
UDP 隧道 | sensor.udp_tunnels |
— | mdi:tunnel |
TOTAL | 1 |
socks5Count |
SOCKS5 隧道 | sensor.socks5_tunnels |
— | mdi:tunnel |
TOTAL | 1 |
httpProxyCount |
HTTP 代理隧道 | sensor.http_proxy_tunnels |
— | mdi:tunnel |
TOTAL | 1 |
secretCount |
秘密隧道 | sensor.secret_tunnels |
— | mdi:tunnel-lock |
TOTAL | 1 |
p2pCount |
P2P 隧道 | sensor.p2p_tunnels |
— | mdi:tunnel |
TOTAL | 1 |
fileCount |
文件隧道 | sensor.file_tunnels |
— | mdi:file-document-outline |
TOTAL | 1 |
hostCount |
域名映射 | sensor.host_mappings |
— | mdi:dns-outline |
TOTAL | 1 |
nowRateBps |
实时速率 | sensor.realtime_rate |
KB/s | mdi:speedometer |
MEASUREMENT | 1/1024 |
serverIps |
服务器 IP | sensor.server_ips |
— | mdi:server-network |
— | 1(字符串原样返回) |
bridgeTypes |
网桥协议 | sensor.bridge_protocols |
— | mdi:swap-horizontal |
— | 1(字符串原样返回) |
备注:
serverIps与bridgeTypes是逗号连接的多值字符串,不会被divisor缩放(代码里if isinstance(raw_val, (str, bool)): return raw_val已特判)。
隧道类传感器附带明细属性:隧道总数、TCP 隧道、UDP 隧道、SOCKS5 隧道、HTTP 代理隧道、秘密隧道、P2P 隧道、文件隧道 这 8 个实体的状态属性里额外带该类型下每条隧道的信息(隧道总数 汇总全部类型):
| 属性 | 内容 |
|---|---|
tunnels |
隧道摘要列表,最多 MAX_LISTED_TUNNELS = 50 条 |
returned |
本次列出的条数 |
truncated |
仅当条数达到 50 时出现,值为 true |
tunnels 每项字段:
| 字段 | 含义 |
|---|---|
tunnel_id |
隧道 ID |
remark |
备注(没有则 null) |
mode |
原始模式值(tcp / udp / socks5 / httpProxy / secret / p2p / file;旧版 NPS 取 Type) |
port |
服务端端口(0 = 自动分配) |
target |
内网目标(Target.TargetStr 第一行) |
client_id / client_online |
所属 NPC ID / 该 NPC 是否在线 |
running |
NPS RunStatus:隧道当前是否在运行 |
enabled |
NPS Status:是否在配置中启用 |
inlet_flow / export_flow |
已格式化的流量(B / KB / MB / GB / TB) |
-
tunnels属性已声明_unrecorded_attributes,不写入 recorder(避免历史库膨胀与属性大小上限)。 -
用法示例(列出正在运行的 TCP 隧道备注):
{{ state_attr('sensor.tcp_tunnels', 'tunnels') | selectattr('running') | map(attribute='remark') | list }}
| 项目 | 内容 |
|---|---|
| 名称 | 面板访问密钥 |
| entity_id | sensor.panel_access_key |
| unique_id | {entry_id}_nps_access_key |
| 图标 / 单位 | mdi:key-variant / min(state_class: measurement;不声明 device_class: duration,否则 HA 会把分钟按规范单位换算回秒) |
| 状态值 | 当前密钥剩余有效分钟数(不足 1 分钟按 1 分钟),随 coordinator 刷新(默认 30 秒,可在选项中改 5~3600 秒) |
密钥口径与爱快集成的 sensor.ikuai_api_key 完全一致:
key = md5(集成盐 | 整点序号 | 主机地址 | 条目 id)[:16]
- 加盐:盐在集成启动时随机生成(
os.urandom(16).hex()),只存在内存里,不落盘、不写进任何链接;即使知道你连的是哪台 NPS,也算不出密钥。 - 整点轮换:以 UTC 整点为一个周期(
API_KEY_TTL = 3600秒),跨整点自动换新。 - 旧密钥宽限一个周期:上一把密钥在下一个整点内仍然有效,正在使用的人跨整点不会被打断;再过一小时彻底失效。
- 多实例时密钥带条目 id,各台密钥互不相同,也可以只凭密钥反查是哪一台。
状态属性:
| 属性 | 含义 |
|---|---|
密钥 |
当前整点的密钥(16 位 hex) |
剩余时长 |
中文可读时长,分钟级只到分钟:55 分 / 29 分;只剩不到 1 分钟时显示 42 秒 |
访问地址 |
可直接打开的面板地址:http(s)://<HA>/nps/console?entry=<entry_id>&key=<密钥> |
过期时间 |
本周期结束时刻(带时区的 ISO 时间) |
上一把密钥 |
上一个整点的密钥(宽限期内仍有效,首个周期为 null) |
算法 |
密钥算法说明 |
实体形态示例(状态是整数分钟,单位 min):
entity_id: sensor.nps_server_47_108_189_87_8080_panel_access_key
state: "37" # 37 分钟,最长 60
unit_of_measurement: min
attributes:
密钥: ce0fa23a907c260f
剩余时长: 36 分
访问地址: http://<HA>:8123/nps/console?entry=<entry_id>&key=ce0fa23a907c260f
过期时间: "2026-10-22T19:00:00+08:00"
上一把密钥: null
算法: md5(集成盐 | 整点序号 | 主机地址 | 条目id)[:16],整点换新、旧密钥宽限 1 小时行为说明:
- 状态单位就是
min:不要给它加device_class: duration,否则 HA 会按 duration 的规范单位把分钟换算回秒(状态变成分钟 × 60的浮点数、单位显示s)。 - 「访问地址」是一条短链接:不需要签名、不依赖 HA 登录态,有效期为"当前整点剩余时间 + 一个整点宽限",打开后照常下发 12 小时会话 Cookie。
- 密钥相关属性已声明
_unrecorded_attributes,不写入 recorder 历史库。 - 「管理面板访问」开关关闭时,实体为
unavailable。 - 盐只在内存里,HA 重启后密钥会重新生成(即使还在同一整点)。
- 改过单位/设备类后必须重载集成(点"刷新"无效),页面再硬刷一次。
| 项目 | 内容 |
|---|---|
| 名称 | 所有实体 |
| entity_id | sensor.all_entities |
| 状态值 | 该设备下实体注册表条目数量 |
| 属性 | count(数量)、entities(entity_id / name / icon / state 明细)、truncated(超过 200 条时置 true) |
刷新时机:每 60 秒 + 实体注册表变化事件 + 成员实体状态变化。
- 传感器显示名全部为中文;中文名 slugify 后为空,会生成
sensor.xxx_2之类的不稳定 ID,因此每个传感器都用suggested_object_id显式固定 entity_id。 - 固定的 object_id 沿用早期英文名的 slug(如
Client Total→client_total),升级后已有 entity_id 不变;unique_id也未改动,重启后注册表会把显示名刷新为中文(用户手动改过名的保持用户设置)。
-
唯一 ID:
{entry_id}_nps_tunnel_{tunnel_id} -
命名规则:
- 有备注时:
{client_id}_{remark}_{MODE_LABEL}_{target_first_line} - 无备注时:
{client_id}_Tunnel{tunnel_id}_{MODE_LABEL}_{target_first_line} MODE_LABEL取自MODE_LABELS:TCP/UDP/Socks5/HTTPProxy/Secret/P2P/File(另有HostServer、WebServer);目标为空时后缀省略
- 有备注时:
-
图标:
mdi:lan-connect(开) /mdi:lan-disconnect(关) -
动作:
- 开启 →
POST /index/start id={tunnel_id}→ 等 3 秒 → 请求刷新 - 关闭 →
POST /index/stop id={tunnel_id}→ 等 3 秒 → 请求刷新
- 开启 →
-
extra_state_attributes:
字段 来源 备注 tunnel_idId隧道 ID portPort监听端口 modeMode原始枚举值 targetTarget.TargetStr第一行目标地址 status_enabledStatus数据库启用标志 server_ipServerIp服务端 IP portsPorts多端口 target_addrTargetAddr顶层目标 strip_preStripPreHTTP 前缀剥离 inlet_flowFlow.InletFlow格式化(B/KB/MB/GB/TB) export_flowFlow.ExportFlow格式化 flow_limitFlow.FlowLimit格式化(>0 才输出) passwordPassword脱敏(前 2 + 中间最多 8 个 * + 后 2);隧道本身没密码时不输出该字段 health_check_interval_sHealthCheckInterval顶层健康检查周期 health_max_failHealthMaxFail最大失败次数 health_check_timeout_sHealthCheckTimeout健康检查超时 client_idClient.Id所属 NPC ID client_ipClient.Addr客户端 IP client_onlineClient.IsConnect客户端在线状态 serverapi._base_url当前 NPS 服务端地址
-
唯一 ID:
{entry_id}_nps_client_{client_id} -
命名规则:
- 有备注时:
{client_id}_{remark}_{verify_key} - 无备注时:
{client_id}_NPC_{verify_key or 'unknown'}
- 有备注时:
-
图标:
mdi:ethernet(在线) /mdi:ethernet-off(离线) -
动作:
- 开启(启用连接)→
POST /client/change_status id={client_id} status=true→ 等 3 秒 → 刷新 - 关闭(禁用连接 = 断开)→
POST /client/change_status id={client_id} status=false→ 等 3 秒 → 刷新
- 开启(启用连接)→
-
过滤:若客户端
NoDisplay == True,不会创建实体。 -
extra_state_attributes:
字段 来源 备注 client_idIdNPC ID ipAddr客户端 IP onlineIsConnect在线状态 connectionsNowConn当前连接数 inlet_flow/export_flowFlow.*格式化 versionVersionNPC 版本 vkeyVerifyKey仅显示前 16 字符(脱敏) rate_limit_kbpsRateLimit速率限制 max_tunnel_numMaxTunnelNum最大隧道数 max_connectionsMaxConn最大连接数 flow_limitFlow.FlowLimit流量上限 config_allowedConfigConnAllow是否允许配置连接 web_usernameWebUserNameWeb 鉴权用户名 compressionCnf.Compress是否启用压缩 encryptionCnf.Crypt是否启用加密 realtime_rateRate.NowRateKB/s 字符串 bridge_protocol/bridge_portbridgeType/bridgePort桥接信息 nps_public_ipip/IpNPS 公网 IP
- 名称 / entity_id:
管理面板访问/switch.panel_access - 唯一 ID:
{entry_id}_nps_panel_access - 图标:
mdi:shield-lock(开) /mdi:shield-off-outline(关) - 属性:
entry_id、panel_url(开启时为签名面板地址,关闭时不输出) - 持久化:状态写在配置条目
options.panel_access,重启后保持 - 关闭后的效果:
- 设备页「访问」链接被移除(
configuration_url置空); - 已打开的面板页面所有请求被拒(HTTP 403
panel_disabled/ 401unauthorized); - HA 前端内 websocket 调用返回
panel_disabled; - 密钥传感器变为
unavailable。
- 设备页「访问」链接被移除(
- 侧边栏:设置 → 面板 → 「NPS 管理」;路径
/nps,需管理员。 - 设备页「访问」:点击设备信息里的「访问」按钮,弹悬浮窗(不再整页跳转)。
- 链接直开:
sensor.panel_access_key的访问地址(密钥短链接,整点轮换 + 1 小时宽限)或设备实体的panel_url(authSig签名地址,24 小时),都可在任意浏览器(含外网)直接打开。
顶部:标题、状态条(N 条隧道 · 在线/总客户端 · N 个域名映射)、实例下拉框(多实例时出现)。
工具栏:搜索框、隧道类型筛选(仅隧道页)、「新建…」、「刷新」。
三个选项卡:
| 选项卡 | 表格列 | 操作 |
|---|---|---|
| 隧道 | 运行、ID、类型、备注、客户端、服务端端口、内网目标、流量(入/出) | 启停、编辑、复制、删除 |
| 客户端 | 连接、ID、备注、地址、版本、连接数、流量(入/出)、实时速率 | 启用/禁用、编辑、删除 |
| 域名映射 | 状态、ID、域名、内网目标、备注、客户端、协议 | 启停、编辑、删除 |
新建/编辑使用原生 <dialog> 表单,字段与 NPS 官方后台一致:
- 隧道:隧道类型(7 种)、所属客户端、备注、服务端端口(0=自动分配)、服务端绑定 IP、内网目标、隧道密码(仅
secret)、本地文件路径(仅file)、去除 URL 前缀(仅httpProxy)、转发到 NPS 服务器本地。带dependsOn的字段会随类型自动显隐。 - 客户端:备注、唯一验证密钥、Basic 认证用户名/密码、压缩、加密、允许配置文件模式连接、带宽限制 (KB/s)、流量限制 (M)、最大连接数、最大隧道数、Web 登录用户名/密码、IP 白名单(开关/授权密码/列表)、黑名单 IP 列表、到期时间。编辑时留空的敏感字段(Basic 密码、Web 密码、白名单密码、各 IP 列表)不会被提交,避免把服务端已有值清空。
- 域名映射:所属客户端、域名、协议(all/http/https)、URL 路由、内网目标、备注、改写请求 Host、自定义请求头、自动申请 HTTPS、HTTPS 私钥/证书、转发到 NPS 服务器本地。编辑时留空的私钥/证书同样不提交。
面板操作完成后,后端会立即 coordinator.async_refresh() 刷新一次,保证面板与 HA 实体同步:
- 新增隧道/客户端 → 自动补建对应的
switch实体; - 删除隧道/客户端 → 自动移除对应实体(含实体注册表条目);
- 修改备注/端口/目标等 → 实体状态属性与显示名就地更新。
以上都不需要重载集成;switch.py 的 _sync_entities() 挂在 coordinator 监听上做增量对齐。
- 通过
frontend.add_extra_js_url注入,在document捕获阶段拦截点击;用composedPath()穿透ha-button的 Shadow DOM。 - 命中条件:链接 pathname 为
/nps/console且带authSig。 - 命中后
preventDefault + stopPropagation,在 Shadow DOM 宿主里弹 iframe 窗口;Ctrl/Cmd/Shift/Alt点击与中键仍走浏览器原生新标签。 - 同时把链接的 host 原地改写为当前地址栏 origin(
authSig只签 path+query 不签 host),带端口映射/反向代理时不会指向不可达的内部地址。 - 窗口能力:标题栏拖动、右下角缩放、
↻重载、↗新标签打开、✕关闭(关闭时src=about:blank释放页面连接)。 - 非模态:容器
pointer-events: none、窗口本体auto,窗体外仍可正常操作 HA。 - 调试:F12 输入
__npsFloatEmbed可确认模块已加载;__npsFloatOpen(url)/__npsFloatClose()可手动控制。
| 环节 | 机制 |
|---|---|
| 面板地址 | HA 官方 async_sign_path 签发 authSig,默认 24 小时有效;地址缓存 1 小时,到期自动重签 |
| 面板密钥 | ?key=<16 位 hex>,算法 md5(盐 | 整点序号 | 主机地址 | 条目 id);整点轮换、旧密钥宽限一个周期(详见 4.1.2 / 6.11) |
| 首次进入 | GET /nps/console 校验 authSig(或 key)后下发 HttpOnly Cookie |
| 会话 | nps_panel_sess = entry_id.签发时间.HMAC-SHA256,12 小时有效,Path=/nps/、HttpOnly、SameSite=Lax,HTTPS 下自动加 Secure |
| 数据接口 | 独立页面走 POST /nps/api/action + Cookie;HA 前端内走 websocket nps/action(require_admin) |
为什么不用 authSig 直接 POST |
HA 只对 GET/HEAD 接受签名参数,所以页面第一次 GET 用 authSig 换 Cookie,后续 POST 靠 Cookie |
| 总开关 | 「管理面板访问」开关;关闭后签名地址不再签发、已打开页面请求被拒 |
websocket:{type: "nps/action", action, entry_id?, data?}(管理员)
HTTP - 页面:GET /nps/console?entry=<entry_id>,鉴权二选一:authSig=<jwt>(签名链接,24h)或 key=<面板密钥>(整点轮换),校验通过后下发 Cookie 并返回面板 HTML。
HTTP - 数据:POST /nps/api/action,body {action, entry_id?, data?, key?},鉴权三选一:会话 Cookie / authSig / 面板密钥(?key= 或 body 的 key);未指定 entry_id 时取第一个已加载实例。
成功返回最新快照:
{
"success": true,
"entry_id": "…",
"entries": [{ "entry_id": "…", "title": "NPS (host)" }],
"tunnels": [], "clients": [], "hosts": [],
"dashboard": {}, "server_time": 0,
"tunnel_types": ["tcp", "udp", "socks5", "httpProxy", "secret", "p2p", "file"],
"host_schemes": ["all", "http", "https"]
}失败返回 {success: false, code, message},错误码:
| code | 触发条件 |
|---|---|
panel_disabled |
「管理面板访问」开关关闭 |
invalid_format |
请求体不是 JSON / 缺少 action / 参数校验不通过 |
nps_error |
NPS 连接失败、业务错误、未预期异常 |
unauthorized |
(HTTP)Cookie 无效、authSig 校验失败且面板密钥不匹配,401 |
action = console_url 特例:不拉数据,直接返回 {success: true, url, entry_id}。
timestamp = str(int(time.time()))
sign = hashlib.md5((self._auth_key + timestamp).encode("utf-8")).hexdigest()
# 提交:auth_key=sign, timestamp=...注意:HA 这边是用 int(time.time())(秒级),不是毫秒;签名输出 32 位小写 hex。
NPS 源码 base.go 在鉴权失败时返回 302 → /login/index。若允许重定向,客户端会被重定向到登录页并得到 HTML 而非 JSON,导致后续 JSON 解析报错且错误信息误导。代码中强制 allow_redirects=False,遇到 302 时主动抛出 HomeAssistantError。
NPS server.go 的过滤逻辑:当 type="" 且 clientId=0 时,会跳过所有 Client.Id != 0 的真实隧道,因此不能用空 type 一次拉完。代码采用 7 次查询 + 合并去重:
for tunnel_type in TUNNEL_TYPES: # tcp/udp/socks5/httpProxy/secret/p2p/file
rows = _post("/index/gettunnel", type=tunnel_type).get("rows", [])
for t in rows:
if t.get("Id") not in seen_ids:
seen_ids.add(t["Id"])
all_tunnels.append(t)单次失败仅 WARNING 记录,不影响整体更新。
- 核心请求(隧道/客户端):失败时整体 raise,由 HA 标记 coordinator 为 unavailable
- 可选请求(Host / 服务器时间):try/except 吞掉,debug 日志
hostCount 在旧版 NPS 中可能没有 /index/hostlist/ 接口,失败后 sensor 显示 None(而非 unavailable)。
coordinator._build_dashboard(tunnels, clients, hosts) 产出以下键:
| 字段 | 含义 |
|---|---|
clientCount / clientOnlineCount |
客户端总数 / 在线数 |
inletFlowCount / exportFlowCount |
所有客户端流量累计(字节) |
totalConnections |
所有客户端 NowConn 累计 |
tcpC / udpCount / socks5Count / httpProxyCount / secretCount / p2pCount / fileCount |
各类型隧道计数 |
tunnelTotal |
隧道总数 |
hostCount |
Host 映射数量 |
nowRateBps |
所有客户端 Rate.NowRate 累计(B/s) |
serverIps |
全部 NPS 公网 IP,去重后逗号连接 |
bridgeTypes |
全部 bridgeType 去重后逗号连接 |
REFRESH_DELAY = 3 秒:用户在 UI 触发 turn_on/off 后,集成先调用 NPS API,再 await asyncio.sleep(3),最后 coordinator.async_request_refresh() 强制拉取最新数据并广播给所有监听实体。不强制 sleep 直接刷新会拿到 NPS 内部状态尚未更新的旧值(按钮状态短暂错乱)。
- 传感器:内部存原始字节,读取时
round(raw * 1/1024^3, 2)输出 GB;计数类传感器不做换算,直接输出整数。 - Switch
extra_state_attributes:_format_flow(bytes)动态单位(0显示0 B,其余按 KB/MB/GB/TB 保留两位小数)。
_mask_password:长度 ≤4 显示 ****;否则 前2 + 最多8个* + 后2。客户端 vkey 仅显示前 16 字符。面板页面不回填密码、验证密钥、Web 密码、IP 白名单密码与证书内容,编辑时留空即保持服务端原值。
| action | api 方法 | NPS 端点 |
|---|---|---|
list |
—(只刷新) | — |
tunnel_add / tunnel_edit |
add_tunnel / edit_tunnel |
/index/add / /index/edit |
tunnel_del / tunnel_copy |
del_tunnel / copy_tunnel |
/index/del / /index/copy |
tunnel_start / tunnel_stop |
start_tunnel / stop_tunnel |
/index/start / /index/stop |
host_add / host_edit / host_del |
add_host / edit_host / del_host |
/index/addhost / /index/edithost / /index/delhost |
host_start / host_stop |
start_host / stop_host |
/index/hoststart / /index/hoststop |
client_add / client_edit / client_del |
add_client / edit_client / del_client |
/client/add / /client/edit / /client/del |
client_on / client_off |
change_client_status |
/client/change_status |
console_url |
—(签发签名地址) | — |
每个动作执行后统一 coordinator.async_refresh() 并返回最新快照;刷新失败只记 WARNING,不把已成功的操作报成失败。
DASHBOARD_SENSORS 为字典列表(key / name / object_id / unit / icon / state_class / divisor)。NpsDashboardSensor 覆写 suggested_object_id 使用 object_id,原因:中文 name 经 HA slugify 后为空字符串,会退化成 sensor.xxx_2 这类不稳定 ID;固定为早期英文名 slug 可保证升级前后 entity_id 完全一致。
_hour_index()=int(time.time() // API_KEY_TTL)(API_KEY_TTL = 3600),即 UTC 整点序号;- 盐存在
hass.data["nps_panel_keys"]["salt"],首次访问时os.urandom(16).hex()随机生成,只在内存里; _make_key()=md5(f"{盐}|{整点序号}|{host}|{entry_id}")[:16](host取自配置条目的entry.data["host"]);ensure_panel_key():发现整点序号变化就换新,并把旧密钥挪到prev_key;panel_key_matches():{当前密钥, 上一把密钥}命中即通过 —— 这就是"跨整点宽限一个周期",两小时前的密钥彻底失效;resolve_entry_by_panel_key():遍历所有配置条目按密钥反查,多实例时调用方可以只给key不给entry;- 实体状态 =
remaining_minutes = max(1, (到期时刻 - now + 59) // 60),整数、单位min、不设device_class(设了duration会被 HA 换算成秒); 剩余时长属性由_human_duration()生成:"N 天 M 小时" / "N 小时 M 分" / "N 分",只有不足 1 分钟时才用 "N 秒"。
| 关键字 | 含义 |
|---|---|
NPS API 请求(DEBUG) |
每次 POST 的 URL 和参数 keys |
NPS 返回重定向 [...] → ...(ERROR) |
鉴权签名失败(auth_key 错,被 302 到登录页) |
NPS API 业务错误(ERROR) |
服务端返回 status=0 + msg |
NPS API 请求失败(ERROR) |
网络层失败(超时/连接拒绝等) |
NPS 返回内容无法解析为 JSON(ERROR) |
收到了非 JSON 响应(HTML 登录页) |
NPS 更新完成(INFO) |
每次拉取成功的条数汇总 |
正在开启/停止/启用/禁用 ...(INFO) |
开关操作 |
NPS 管理面板已注册(DEBUG) |
面板注册成功 |
NPS 管理面板前端脚本注册失败(WARNING) |
面板前端模块未注入(功能降级,不影响实体) |
NPS 悬浮窗前端模块注册失败(WARNING) |
「访问」仍会在新页面打开 |
NPS 操作已提交,但刷新数据失败(WARNING) |
面板操作已生效,只是快照没刷新 |
启用/禁用 NPS 管理面板访问(INFO/WARNING) |
访问开关切换 |
| 现象 | 排查方向 |
|---|---|
添加集成提示 connection_error |
1) host:port 拼写 2) 防火墙 3) auth_key 是否为 nps.conf 的原始值 4) 时钟偏差 |
| 隧道开关操作后 5~10 秒无反馈 | 正常,REFRESH_DELAY=3s + coordinator 拉取 + 监听回调 |
sensor 一直 unavailable |
coordinator 首次刷新失败,看 ERROR 日志中的根因 |
hostCount 一直为 None |
NPS 版本没有 /index/hostlist/ 接口,集成会安全忽略 |
| 重复添加相同 host 提示 already configured | 通过 host 去重的多实例保护,请到现有实例查看 |
| 设备页没有「访问」按钮 | 1) 未配置 internal_url/external_url(无法签发地址)2) 「管理面板访问」开关被关闭 3) 集成尚未加载完成 |
| 点「访问」仍在新页面打开 | 1) nps-embed.js 未加载(F12 输入 __npsFloatEmbed 验证)2) 浏览器缓存旧前端资源,强刷 3) Ctrl/中键点击本来就是新标签 |
| 点「访问」弹窗白屏 / 显示「链接或密钥无效/已过期」 | 签名已过期(>24h)或 Cookie 过期(>12h),重新点一次「访问」;或 HA 地址在浏览器不可达 |
| 侧边栏「NPS 管理」404 / 空白 | /nps 路径可能已被其他面板占用,看 ERROR 日志「NPS 管理面板注册失败」 |
| 面板提示「会话或密钥已过期,请重新打开管理面板」 | Cookie 失效;从设备页或侧边栏重新进入 |
改了 HA 的 internal_url/external_url 后「访问」链接还是旧的 |
设备链接在实体添加时写入设备注册表,重载集成才会重新签发 |
| 面板提示「管理面板已被「管理面板访问」开关禁用」 | 打开 switch.panel_access |
「面板访问密钥」为 unavailable |
上述开关关闭 |
| 密钥/访问地址整点后变了 | 正常:密钥整点轮换;上一把密钥还有一整个周期的宽限,手上的旧链接不会立刻失效 |
| 访问地址打开提示「链接或密钥无效/已过期」 | 已跨过两个整点(超出宽限),或 HA 重启过(盐重新生成);从实体属性取新地址 |
外部脚本调 /nps/api/action 返回 401 |
把密钥放 ?key=<密钥> 或 body 的 key 字段;entry 可省略(会按密钥反查实例),且密钥必须在有效期内 |
| 剩余时长总是 ≤ 60 分钟 | 设计如此:单位是分钟,密钥按整点轮换,每周期最长 60 分钟 |
-
在
coordinator._build_dashboard()中产出新键。 -
在
sensor.DASHBOARD_SENSORS追加字典:{ "key": "myKey", "name": "我的传感器", # 中文显示名 "object_id": "my_sensor", # 固定 entity_id 后缀(必填,中文名 slugify 为空) "unit": "GB", # 可选 "icon": "mdi:chart-line", # 可选 "state_class": SensorStateClass.MEASUREMENT, # 可选 "device_class": None, # 可选 "divisor": 1 / (1024 ** 3), # 可选 } -
重启 HA。
- 在
NpsApiClient增加对应控制方法(如delete_tunnel)。 - 在
switch.py增加新实体类并追加到async_setup_entry的entities列表。
api.py增加方法(必要时补充ROUTES候选路径)。panel.py的_async_dispatch()增加分支。panel/nps-panel.js调用this._act("your_action", {...})。- websocket 与 HTTP 两条通道会自动复用同一分发逻辑,无需分别改动。
无特殊改动;HA 通过 unique_id 自动隔离,每个实例有自己的 coordinator 和实体。面板/接口注册是全局单例(hass.data 标记),面板内通过 entry_id 切换实例。
| 来源 | 常量 | 值 | 说明 |
|---|---|---|---|
api.py |
TUNNEL_TYPES |
[tcp, udp, socks5, httpProxy, secret, p2p, file] |
7 种隧道类型 |
api.py |
REQUEST_TIMEOUT |
15 秒 |
HTTP 超时 |
coordinator.py |
UPDATE_INTERVAL |
30 秒 |
默认轮询周期(可被选项覆盖) |
switch.py |
REFRESH_DELAY |
3 秒 |
操作后等待服务端状态同步 |
sensor.py |
BYTES_TO_GB |
1 / 1024**3 |
流量转换系数 |
sensor.py |
MAX_LISTED_ENTITIES |
200 |
「所有实体」传感器最多列出的条目 |
helpers.py |
PANEL_URL_PATH |
nps |
侧边栏面板路径 |
panel.py |
PANEL_STATIC_URL |
/nps_static |
前端静态资源前缀 |
panel.py |
PANEL_MODULE_URL / PANEL_EMBED_URL |
/nps_static/nps-panel.js / /nps_static/nps-embed.js |
面板与悬浮窗模块 |
console_auth.py |
CONSOLE_PATH |
/nps/console |
独立面板页(签名入口) |
console_auth.py |
CONSOLE_API_PATH |
/nps/api/action |
面板数据接口 |
console_auth.py |
CONSOLE_COOKIE_NAME |
nps_panel_sess |
会话 Cookie 名 |
console_auth.py |
CONSOLE_COOKIE_MAX_AGE |
12 小时 |
Cookie 有效期 |
console_auth.py |
CONSOLE_SIGN_EXPIRY |
24 小时 |
签名地址有效期 |
console_auth.py |
_URL_CACHE_TTL |
1 小时 |
签名地址内存缓存 |
console_auth.py |
API_KEY_TTL |
3600 秒 |
面板密钥轮换刻度(整点换新,旧密钥再宽限一个周期) |
console_auth.py |
API_KEY_LEN |
16 |
面板密钥长度(md5 前 16 位) |
const.py |
CONF_SCAN_INTERVAL / DEFAULT_SCAN_INTERVAL |
scan_interval / 30 |
轮询间隔选项 |
const.py |
CONF_PANEL_ACCESS |
panel_access |
面板访问开关的持久化键 |
| 方法 | 端点 | 参数 | 用途 |
|---|---|---|---|
| POST | /auth/get_time |
auth_key, timestamp |
服务器时间(调试用) |
| POST | /index/gettunnel |
+ type, offset, limit |
隧道列表 |
| POST | /index/getonetunnel |
+ id |
单条隧道 |
| POST | /index/add / /index/edit / /index/del / /index/copy |
表单字段 / id |
隧道增改删复制 |
| POST | /index/hostlist/ |
offset, limit |
域名映射列表 |
| POST | /index/addhost / /index/edithost / /index/delhost |
表单字段 / id |
域名映射增改删 |
| POST | /index/hoststart / /index/hoststop |
+ id |
域名映射启停 |
| POST | /client/list |
offset, limit |
客户端列表 |
| POST | /client/add / /client/edit / /client/del |
表单字段 / id |
客户端增改删 |
| POST | /client/change_status |
+ id, status |
客户端启停 |
| POST | /index/start |
+ id |
开启隧道 |
| POST | /index/stop |
+ id |
停止隧道 |
上述路径均有「带/不带尾部斜杠」两种候选形式,由
api.ROUTES依次尝试。
| 方法 | 路径 | 鉴权 | 用途 |
|---|---|---|---|
| WS | nps/action |
HA 登录 + 管理员 | 面板所有操作与 console_url |
| GET | /nps/console?entry=<id>&authSig=<jwt> |
HA 签名(24h) | 返回独立面板 HTML,并下发会话 Cookie |
| GET | /nps/console?entry=<id>&key=<密钥> |
面板密钥(整点轮换 + 1 周期宽限) | 同上,短链接入口 |
| POST | /nps/api/action |
会话 Cookie / authSig / ?key= 或 body.key |
面板数据接口 |
| GET | /nps_static/nps-panel.js |
公开静态资源 | 面板 webcomponent(侧边栏 / 悬浮窗共用) |
| GET | /nps_static/nps-embed.js |
公开静态资源 | 设备页「访问」悬浮窗 |
| 版本 | 日期 | 变更 |
|---|---|---|
| 0.1.0 | 2026-08-22 | 初版:隧道/客户端开关 + 16 个 sensor |
| 1.0.0 | 2026-10-22 | 1) 首发可视化管理面板(隧道/客户端/域名映射的增删改启停) 2) 签名链接 + 会话 Cookie 鉴权与独立页面 /nps/console、面板密钥(加盐、整点轮换)3) 侧边栏面板、设备页「访问」悬浮窗 nps-embed.js4) 「管理面板访问」开关与面板访问密钥传感器 5) 传感器全部中文化并固定 entity_id,条目扩充到 19 个(含「所有实体」) 6) 选项流:轮询间隔可配(5~3600 秒) |
| 1.1.0 | 2026-10-22 | 1) 隧道类传感器新增 tunnels 明细属性(备注/端口/目标/客户端/运行状态/流量)2) 面板增删隧道、客户端后自动补建/移除对应实体,无需重载集成 3) 删除后彻底清除实体与注册表条目,并自动清理历史残留的「不可用」实体 4) 适配新版 device_registry API,消除弃用警告5) 修复计数类传感器显示 0.0、以及「所有实体」传感器在部分版本下添加失败 |
文档版本:2.0
维护者:@chjspp520 (https://github.com/chjspp520/nps)