first commit
This commit is contained in:
23
.github/workflows/validate.yml
vendored
Normal file
23
.github/workflows/validate.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Validate
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: Validate
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: HACS validation
|
||||
uses: hacs/action@main
|
||||
with:
|
||||
category: integration
|
||||
|
||||
- name: Hassfest validation
|
||||
uses: home-assistant/actions/hassfest@master
|
||||
|
||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.DS_Store
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
145
DATA_FORMAT.md
Normal file
145
DATA_FORMAT.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 数据传输格式说明
|
||||
|
||||
## 概述
|
||||
|
||||
Energy Monitor 系统使用 MQTT 协议进行数据传输,包含两个主要的通信方向:
|
||||
|
||||
1. **Home Assistant → 设备**: 发送数据获取请求
|
||||
2. **设备 → Home Assistant**: 返回设备数据
|
||||
|
||||
## 数据流图
|
||||
|
||||
```
|
||||
Home Assistant 集成
|
||||
│
|
||||
▼
|
||||
/data/data-get (请求)
|
||||
│
|
||||
▼
|
||||
设备端处理
|
||||
│
|
||||
▼
|
||||
/device/data (响应)
|
||||
│
|
||||
▼
|
||||
Home Assistant 集成
|
||||
│
|
||||
▼
|
||||
更新传感器状态
|
||||
```
|
||||
|
||||
## 数据格式
|
||||
|
||||
### 1. 数据获取请求
|
||||
|
||||
**主题**: `/data/data-get`
|
||||
**格式**: 纯文本
|
||||
**内容**: `get_data`
|
||||
**频率**: 每秒5次(每0.2秒一次)
|
||||
|
||||
```bash
|
||||
主题: /data/data-get
|
||||
内容: get_data
|
||||
```
|
||||
|
||||
### 2. 设备数据响应
|
||||
|
||||
**主题**: `/device/data`
|
||||
**格式**: JSON
|
||||
**编码**: UTF-8
|
||||
|
||||
#### 完整数据格式
|
||||
|
||||
```json
|
||||
{
|
||||
"solar_power": 1500.5,
|
||||
"home_power": 1200.0,
|
||||
"grid_import": 300.0,
|
||||
"grid_export": 0.0,
|
||||
"battery_charge": 200.0,
|
||||
"battery_discharge": 0.0,
|
||||
"battery_soc": 85.5
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段名 | 类型 | 单位 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `solar_power` | float | W | 太阳能发电功率 |
|
||||
| `home_power` | float | W | 家庭用电功率 |
|
||||
| `grid_import` | float | W | 从电网购买功率 |
|
||||
| `grid_export` | float | W | 向电网出售功率 |
|
||||
| `battery_charge` | float | W | 电池充电功率 |
|
||||
| `battery_discharge` | float | W | 电池放电功率 |
|
||||
| `battery_soc` | float | % | 电池电量百分比 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### Python 设备端示例
|
||||
|
||||
```python
|
||||
import json
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
if msg.topic == "/data/data-get":
|
||||
# 收到数据请求,发送设备数据
|
||||
data = {
|
||||
"solar_power": 1500.5,
|
||||
"home_power": 1200.0,
|
||||
"grid_import": 300.0,
|
||||
"grid_export": 0.0,
|
||||
"battery_charge": 200.0,
|
||||
"battery_discharge": 0.0,
|
||||
"battery_soc": 85.5
|
||||
}
|
||||
|
||||
# 发送到 /device/data 主题
|
||||
client.publish("/device/data", json.dumps(data))
|
||||
|
||||
# 设置 MQTT 客户端
|
||||
client = mqtt.Client()
|
||||
client.on_message = on_message
|
||||
client.connect("192.168.0.101", 1883, 60)
|
||||
client.subscribe("/data/data-get")
|
||||
client.loop_forever()
|
||||
```
|
||||
|
||||
### 测试命令
|
||||
|
||||
使用 mosquitto 客户端测试:
|
||||
|
||||
```bash
|
||||
# 监听数据获取请求
|
||||
mosquitto_sub -h 192.168.0.101 -t "/data/data-get"
|
||||
|
||||
# 监听设备数据
|
||||
mosquitto_sub -h 192.168.0.101 -t "/device/data"
|
||||
|
||||
# 手动发送数据请求
|
||||
mosquitto_pub -h 192.168.0.101 -t "/data/data-get" -m "get_data"
|
||||
|
||||
# 手动发送设备数据
|
||||
mosquitto_pub -h 192.168.0.101 -t "/device/data" -m '{"solar_power": 1500.5, "home_power": 1200.0}'
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **JSON 格式**: 设备数据必须是有效的 JSON 格式
|
||||
2. **数值类型**: 所有功率值应为数字类型(int 或 float)
|
||||
3. **电量范围**: battery_soc 应在 0-100 之间
|
||||
4. **功率单位**: 所有功率值单位为瓦特 (W)
|
||||
5. **实时性**: 数据应尽可能实时更新
|
||||
6. **错误处理**: 设备端应处理数据请求失败的情况
|
||||
|
||||
## 数据验证
|
||||
|
||||
Home Assistant 集成会验证接收到的数据:
|
||||
|
||||
- JSON 格式正确性
|
||||
- 必需字段存在性
|
||||
- 数值类型正确性
|
||||
- 数值范围合理性
|
||||
|
||||
如果数据格式不正确,会在日志中记录警告信息。
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 JackeryHome
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
233
README.md
Normal file
233
README.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# JackeryHome - Home Assistant 能源监控集成
|
||||
|
||||
[](https://github.com/hacs/integration)
|
||||
[](https://github.com/suyulin/home-assistant-demo-mqtt/releases)
|
||||
[](LICENSE)
|
||||
|
||||
这是一个 Home Assistant 自定义集成,通过 MQTT 监控太阳能、电网、电池和家庭能源数据。
|
||||
|
||||
## 功能
|
||||
|
||||
- 模拟太阳能发电、电网供电、家庭用电和电池充放电数据
|
||||
- 通过 MQTT 自动发现功能将传感器添加到 Home Assistant
|
||||
- **提供 Home Assistant 自定义集成,用于接收和显示 MQTT 数据**
|
||||
- 提供 Energy Flow Card Plus 卡片配置示例
|
||||
|
||||
## 项目结构
|
||||
|
||||
本项目包含两个主要部分:
|
||||
|
||||
1. **MQTT 模拟器** (`main.py`) - 模拟发送能源监控数据到 MQTT broker
|
||||
2. **Home Assistant 自定义集成** (`custom_components/energy_monitor/`) - 接收 MQTT 数据并创建传感器实体
|
||||
|
||||
## 传感器列表
|
||||
|
||||
本项目会创建以下传感器:
|
||||
|
||||
- `sensor.solar_power`: 太阳能发电功率(W)
|
||||
- `sensor.home_power`: 家庭用电功率(W)
|
||||
- `sensor.grid_import`: 从电网购买功率(W)
|
||||
- `sensor.grid_export`: 向电网出售功率(W)
|
||||
- `sensor.battery_charge`: 电池充电功率(W)
|
||||
- `sensor.battery_discharge`: 电池放电功率(W)
|
||||
- `sensor.battery_soc`: 电池电量百分比(%)
|
||||
|
||||
## 安装
|
||||
|
||||
### 方式一:通过 HACS 安装(推荐)
|
||||
|
||||
1. **添加自定义存储库**
|
||||
- 打开 HACS
|
||||
- 点击右上角三个点 → "自定义存储库"
|
||||
- 添加仓库 URL:`https://github.com/suyulin/home-assistant-demo-mqtt`
|
||||
- 类别选择:`Integration`
|
||||
- 点击"添加"
|
||||
|
||||
2. **安装集成**
|
||||
- 在 HACS 中搜索 "JackeryHome"
|
||||
- 点击"安装"
|
||||
- 重启 Home Assistant
|
||||
|
||||
3. **配置集成**
|
||||
- 进入 **设置** → **设备与服务** → **添加集成**
|
||||
- 搜索 "JackeryHome"
|
||||
- 输入 MQTT 主题前缀(默认:`homeassistant/sensor`)
|
||||
- 点击提交完成配置
|
||||
|
||||
### 方式二:手动安装
|
||||
|
||||
1. 下载最新的 [Release](https://github.com/suyulin/home-assistant-demo-mqtt/releases)
|
||||
2. 将 `custom_components/JackeryHome` 文件夹复制到你的 Home Assistant 配置目录的 `custom_components/` 文件夹中
|
||||
3. 重启 Home Assistant
|
||||
4. 按照上述"配置集成"步骤进行配置
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用 MQTT 模拟器
|
||||
|
||||
1. **安装依赖并运行模拟器**
|
||||
```bash
|
||||
# 使用 uv(推荐)
|
||||
uv sync
|
||||
uv run main.py
|
||||
|
||||
# 或使用 pip
|
||||
pip install paho-mqtt
|
||||
python main.py
|
||||
```
|
||||
|
||||
2. **配置 MQTT Broker**
|
||||
|
||||
编辑 `main.py` 中的地址:
|
||||
```python
|
||||
MQTT_BROKER = "192.168.0.101" # 修改为你的 MQTT Broker 地址
|
||||
```
|
||||
|
||||
3. **在 Home Assistant 中查看传感器**
|
||||
|
||||
传感器会自动通过 MQTT Discovery 添加
|
||||
|
||||
### 配置和使用
|
||||
|
||||
1. **确保已安装并配置集成**(参考上面的安装步骤)
|
||||
|
||||
2. **运行模拟器**
|
||||
```bash
|
||||
# 使用 uv(推荐)
|
||||
uv run main.py
|
||||
|
||||
# 或使用 python
|
||||
python main.py
|
||||
```
|
||||
|
||||
3. **查看传感器数据**
|
||||
- 进入 **开发者工具** → **状态**
|
||||
- 搜索 "solar_power"、"home_power" 等传感器
|
||||
|
||||
## Energy Flow Card Plus 配置
|
||||
|
||||
### 安装卡片
|
||||
|
||||
1. **通过 HACS 安装(推荐):**
|
||||
- 打开 HACS
|
||||
- 点击"前端"(Frontend)
|
||||
- 搜索 "Energy Flow Card Plus"
|
||||
- 点击安装
|
||||
- 重启 Home Assistant
|
||||
|
||||
2. **手动安装:**
|
||||
- 从 [GitHub](https://github.com/flixlix/energy-flow-card-plus) 下载最新版本
|
||||
- 将文件放到 `www/community/energy-flow-card-plus/` 目录
|
||||
- 在 Home Assistant 中添加资源:
|
||||
- 设置 -> 仪表板 -> 右上角三点 -> 资源
|
||||
- URL: `/hacsfiles/energy-flow-card-plus/energy-flow-card-plus.js`
|
||||
- 类型: JavaScript 模块
|
||||
|
||||
### 添加卡片到仪表板
|
||||
|
||||
1. 进入仪表板编辑模式
|
||||
2. 点击"添加卡片"
|
||||
3. 选择"手动"(Manual)
|
||||
4. 复制 `energy_flow_card_config.yaml` 中的配置
|
||||
5. 保存
|
||||
|
||||
### 基础配置示例
|
||||
|
||||
```yaml
|
||||
type: custom:energy-flow-card-plus
|
||||
entities:
|
||||
solar:
|
||||
entity: sensor.solar_power
|
||||
name: 太阳能
|
||||
grid:
|
||||
entity:
|
||||
consumption: sensor.grid_import # 从电网购买
|
||||
production: sensor.grid_export # 向电网出售
|
||||
name: 电网
|
||||
battery:
|
||||
entity:
|
||||
consumption: sensor.battery_charge # 充电
|
||||
production: sensor.battery_discharge # 放电
|
||||
state_of_charge: sensor.battery_soc
|
||||
name: 电池
|
||||
home:
|
||||
entity: sensor.home_power
|
||||
name: 家庭用电
|
||||
```
|
||||
|
||||
更多配置选项请查看 `energy_flow_card_config.yaml` 文件。
|
||||
|
||||
## 项目文件说明
|
||||
|
||||
### 核心文件
|
||||
- `main.py`: MQTT 传感器模拟器主程序
|
||||
- `custom_components/energy_monitor/`: Home Assistant 自定义集成
|
||||
- `__init__.py`: 集成入口
|
||||
- `manifest.json`: 集成元数据
|
||||
- `sensor.py`: 传感器平台实现
|
||||
- `config_flow.py`: UI 配置流程
|
||||
- `strings.json`: 本地化字符串
|
||||
- `translations/zh-Hans.json`: 中文翻译
|
||||
- `README.md`: 集成技术文档
|
||||
|
||||
### 文档和工具
|
||||
- `INTEGRATION_GUIDE.md`: 详细的集成使用指南
|
||||
- `energy_flow_card_config.yaml`: Energy Flow Card Plus 配置示例
|
||||
- `install.sh`: Linux/macOS 自动安装脚本
|
||||
- `install.ps1`: Windows PowerShell 自动安装脚本
|
||||
- `README.md`: 项目主文档(本文件)
|
||||
|
||||
## 数据流向逻辑
|
||||
|
||||
1. **太阳能发电**:随机生成 200-3000W
|
||||
2. **家庭用电**:随机生成 500-3500W
|
||||
3. **电网功率**:
|
||||
- grid_import(从电网购买):当家庭用电 > 太阳能发电时的差值
|
||||
- grid_export(向电网出售):当太阳能发电 > 家庭用电时的差值
|
||||
4. **电池功率**:
|
||||
- battery_charge(充电):0-1000W
|
||||
- battery_discharge(放电):0-1000W
|
||||
5. **电池电量**:根据充放电动态变化(20%-100%)
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 确保 Home Assistant 已配置好 MQTT 集成
|
||||
- MQTT Broker 需要在运行此脚本之前启动
|
||||
- 传感器会每 5 秒更新一次数据
|
||||
- 数据为模拟值,用于演示目的
|
||||
|
||||
## 文档
|
||||
|
||||
- [**HACS 发布指南**](HACS_PUBLISHING_GUIDE.md) - 如何发布到 HACS
|
||||
- [自定义集成 README](custom_components/JackeryHome/README.md) - 集成技术文档
|
||||
|
||||
## 开发者
|
||||
|
||||
### 发布新版本
|
||||
|
||||
使用提供的发布脚本:
|
||||
|
||||
```bash
|
||||
./prepare_release.sh
|
||||
```
|
||||
|
||||
或手动发布:
|
||||
|
||||
1. 更新 `custom_components/JackeryHome/manifest.json` 中的版本号
|
||||
2. 提交更改并推送到 GitHub
|
||||
3. 创建新的 Git tag(如 `v1.0.1`)
|
||||
4. 在 GitHub 创建 Release
|
||||
|
||||
详细说明请查看 [HACS 发布指南](HACS_PUBLISHING_GUIDE.md)
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [Energy Flow Card Plus GitHub](https://github.com/flixlix/energy-flow-card-plus)
|
||||
- [Home Assistant MQTT Discovery](https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery)
|
||||
- [Home Assistant 开发文档](https://developers.home-assistant.io/)
|
||||
- [Paho MQTT Python Client](https://github.com/eclipse/paho.mqtt.python)
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
140
custom_components/JackeryHome/README.md
Normal file
140
custom_components/JackeryHome/README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# JackeryHome - Home Assistant 自定义集成
|
||||
|
||||
这是一个 Home Assistant 自定义集成,用于通过 MQTT 接收能源监控数据并创建传感器实体。
|
||||
|
||||
## 功能特性
|
||||
|
||||
该集成会自动创建以下传感器:
|
||||
|
||||
- **Solar Power** (太阳能发电功率) - 单位:W
|
||||
- **Home Power** (家庭负载功率) - 单位:W
|
||||
- **Grid Import** (电网购买功率) - 单位:W
|
||||
- **Grid Export** (电网出售功率) - 单位:W
|
||||
- **Battery Charge** (电池充电功率) - 单位:W
|
||||
- **Battery Discharge** (电池放电功率) - 单位:W
|
||||
- **Battery State of Charge** (电池电量) - 单位:%
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 复制文件到 Home Assistant
|
||||
|
||||
将 `custom_components/energy_monitor` 文件夹复制到 Home Assistant 的 `config/custom_components/` 目录下:
|
||||
|
||||
```
|
||||
config/
|
||||
custom_components/
|
||||
energy_monitor/
|
||||
__init__.py
|
||||
manifest.json
|
||||
sensor.py
|
||||
config_flow.py
|
||||
README.md
|
||||
```
|
||||
|
||||
### 2. 重启 Home Assistant
|
||||
|
||||
复制文件后,重启 Home Assistant 以加载新的集成。
|
||||
|
||||
### 3. 配置集成
|
||||
|
||||
有两种配置方式:
|
||||
|
||||
#### 方式 A:通过 UI 配置(推荐)
|
||||
|
||||
1. 进入 Home Assistant 的 **设置** → **设备与服务**
|
||||
2. 点击右下角的 **添加集成** 按钮
|
||||
3. 搜索 "JackeryHome"
|
||||
4. 输入 MQTT 主题前缀(默认:`homeassistant/sensor`)
|
||||
5. 点击提交完成配置
|
||||
|
||||
#### 方式 B:通过 configuration.yaml 配置
|
||||
|
||||
在 `configuration.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
energy_monitor:
|
||||
topic_prefix: "homeassistant/sensor"
|
||||
```
|
||||
|
||||
然后重启 Home Assistant。
|
||||
|
||||
## MQTT 主题格式
|
||||
|
||||
集成会订阅以下 MQTT 主题(假设 topic_prefix 为 `homeassistant/sensor`):
|
||||
|
||||
- `homeassistant/sensor/solar_power/state`
|
||||
- `homeassistant/sensor/home_power/state`
|
||||
- `homeassistant/sensor/grid_import/state`
|
||||
- `homeassistant/sensor/grid_export/state`
|
||||
- `homeassistant/sensor/battery_charge/state`
|
||||
- `homeassistant/sensor/battery_discharge/state`
|
||||
- `homeassistant/sensor/battery_soc/state`
|
||||
|
||||
每个主题接收的消息格式为纯数字,例如:`1234.56`
|
||||
|
||||
## 与模拟器配合使用
|
||||
|
||||
本集成与 `main.py` 模拟器完美配合:
|
||||
|
||||
1. 确保 Home Assistant 已配置好 MQTT 集成并连接到同一个 MQTT broker
|
||||
2. 运行 `main.py` 模拟器:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
3. 模拟器会自动发布传感器数据到 MQTT
|
||||
4. Home Assistant 的 JackeryHome 集成会自动接收并显示数据
|
||||
|
||||
## 查看传感器
|
||||
|
||||
配置完成后,你可以在以下位置查看传感器:
|
||||
|
||||
- **开发者工具** → **状态** → 搜索 "energy_monitor"
|
||||
- 传感器实体 ID 格式:`sensor.solar_power`、`sensor.home_power` 等
|
||||
|
||||
## 在 Lovelace 中使用
|
||||
|
||||
你可以使用这些传感器创建能源流图表。例如使用 Energy Flow Card:
|
||||
|
||||
```yaml
|
||||
type: custom:energy-flow-card-plus
|
||||
entities:
|
||||
solar:
|
||||
entity: sensor.solar_power
|
||||
grid:
|
||||
entity:
|
||||
consumption: sensor.grid_import
|
||||
production: sensor.grid_export
|
||||
battery:
|
||||
entity:
|
||||
consumption: sensor.battery_charge
|
||||
production: sensor.battery_discharge
|
||||
state_of_charge: sensor.battery_soc
|
||||
home:
|
||||
entity: sensor.home_power
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 传感器不显示数据
|
||||
|
||||
1. 检查 MQTT broker 是否正常运行
|
||||
2. 检查 Home Assistant 的 MQTT 集成是否已配置
|
||||
3. 检查 `main.py` 中的 MQTT broker 地址是否正确
|
||||
4. 查看 Home Assistant 日志:**设置** → **系统** → **日志**
|
||||
|
||||
### 查看 MQTT 消息
|
||||
|
||||
使用 MQTT 客户端工具(如 MQTT Explorer)监听 `homeassistant/sensor/#` 主题,确认消息是否正常发布。
|
||||
|
||||
## 技术细节
|
||||
|
||||
- **依赖**: Home Assistant MQTT 集成
|
||||
- **协议**: MQTT
|
||||
- **更新方式**: Push(实时推送)
|
||||
- **传感器类型**: 功率传感器、电池传感器
|
||||
- **状态类**: Measurement(测量值)
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
39
custom_components/JackeryHome/__init__.py
Normal file
39
custom_components/JackeryHome/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Energy Monitor MQTT Integration for Home Assistant."""
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import Platform
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "energy_monitor"
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Energy Monitor from a config entry."""
|
||||
_LOGGER.info("Setting up Energy Monitor integration")
|
||||
|
||||
# 存储配置数据
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = entry.data
|
||||
|
||||
# 加载传感器平台
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
_LOGGER.info("Unloading Energy Monitor integration")
|
||||
|
||||
# 卸载传感器平台
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
69
custom_components/JackeryHome/config_flow.py
Normal file
69
custom_components/JackeryHome/config_flow.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Config flow for Energy Monitor integration."""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# 配置数据模式
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
"topic_prefix",
|
||||
default="homeassistant/sensor"
|
||||
): str,
|
||||
vol.Required(
|
||||
"mqtt_broker",
|
||||
default="192.168.0.101"
|
||||
): str,
|
||||
vol.Optional(
|
||||
"mqtt_port",
|
||||
default=1883
|
||||
): int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class EnergyMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Energy Monitor."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# 检查是否已经配置
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.info(f"Creating Energy Monitor config entry with topic_prefix: {user_input['topic_prefix']}")
|
||||
|
||||
return self.async_create_entry(
|
||||
title="Energy Monitor",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"topic_prefix": "MQTT topic prefix (e.g., homeassistant/sensor)",
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
return await self.async_step_user(import_config)
|
||||
|
||||
16
custom_components/JackeryHome/manifest.json
Normal file
16
custom_components/JackeryHome/manifest.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"domain": "jackery_home",
|
||||
"name": "jackery_home",
|
||||
"codeowners": [
|
||||
"@suyulin"
|
||||
],
|
||||
"config_flow": true,
|
||||
"dependencies": [
|
||||
"mqtt"
|
||||
],
|
||||
"documentation": "https://github.com/suyulin/home-assistant-demo-mqtt",
|
||||
"issue_tracker": "https://github.com/suyulin/home-assistant-demo-mqtt/issues",
|
||||
"iot_class": "local_push",
|
||||
"requirements": [],
|
||||
"version": "1.0.0"
|
||||
}
|
||||
311
custom_components/JackeryHome/sensor.py
Normal file
311
custom_components/JackeryHome/sensor.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""Energy Monitor Sensor Platform."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from homeassistant.components import mqtt as ha_mqtt
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.const import UnitOfPower, PERCENTAGE
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# 全局 MQTT 客户端和数据处理
|
||||
class MQTTDataManager:
|
||||
"""MQTT 数据管理器,负责订阅设备数据并发送请求"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, topic_prefix: str, mqtt_broker: str = "192.168.0.101", mqtt_port: int = 1883):
|
||||
self.hass = hass
|
||||
self.topic_prefix = topic_prefix
|
||||
self.mqtt_broker = mqtt_broker
|
||||
self.mqtt_port = mqtt_port
|
||||
self.client = None
|
||||
self.data_task = None
|
||||
self.sensors = {}
|
||||
|
||||
async def start(self):
|
||||
"""启动 MQTT 客户端和数据获取任务"""
|
||||
try:
|
||||
# 创建 MQTT 客户端
|
||||
self.client = mqtt.Client(client_id="energy_monitor_sensor", callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
|
||||
self.client.on_connect = self._on_connect
|
||||
self.client.on_message = self._on_message
|
||||
|
||||
# 连接到 MQTT 代理
|
||||
await self._connect_mqtt()
|
||||
|
||||
# 启动数据获取任务
|
||||
self.data_task = asyncio.create_task(self._data_fetch_loop())
|
||||
|
||||
_LOGGER.info("MQTT Data Manager started successfully")
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Failed to start MQTT Data Manager: {e}")
|
||||
|
||||
async def _connect_mqtt(self):
|
||||
"""连接到 MQTT 代理"""
|
||||
def _connect():
|
||||
self.client.connect(self.mqtt_broker, self.mqtt_port, 60)
|
||||
self.client.loop_start()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _connect)
|
||||
|
||||
def _on_connect(self, client, userdata, flags, rc, properties):
|
||||
"""MQTT 连接回调"""
|
||||
if rc == 0:
|
||||
_LOGGER.info("Connected to MQTT broker")
|
||||
# 订阅设备数据主题
|
||||
client.subscribe("/device/data")
|
||||
_LOGGER.info("Subscribed to /device/data topic")
|
||||
else:
|
||||
_LOGGER.error(f"Failed to connect to MQTT broker with result code {rc}")
|
||||
|
||||
def _on_message(self, client, userdata, msg):
|
||||
"""MQTT 消息接收回调"""
|
||||
try:
|
||||
topic = msg.topic
|
||||
payload = msg.payload.decode()
|
||||
|
||||
if topic == "/device/data":
|
||||
_LOGGER.debug(f"Received data from /device/data: {payload}")
|
||||
# 处理接收到的数据
|
||||
self._process_device_data(payload)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error processing MQTT message: {e}")
|
||||
|
||||
def _process_device_data(self, payload: str):
|
||||
"""处理设备数据"""
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
_LOGGER.debug(f"Processed device data: {data}")
|
||||
|
||||
# 将处理后的数据发送到相应的传感器
|
||||
for sensor_id, sensor_entity in self.sensors.items():
|
||||
if sensor_id in data:
|
||||
# 在 Home Assistant 主线程中更新传感器状态
|
||||
self.hass.create_task(sensor_entity._update_state(data[sensor_id]))
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
_LOGGER.error(f"Invalid JSON data received: {payload}, error: {e}")
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error processing device data: {e}")
|
||||
|
||||
async def _data_fetch_loop(self):
|
||||
"""数据获取循环,每秒5次发送请求"""
|
||||
while True:
|
||||
try:
|
||||
# 发送数据获取请求
|
||||
if self.client:
|
||||
self.client.publish("/data/data-get", "get_data")
|
||||
_LOGGER.debug("Sent data request to /data/data-get")
|
||||
|
||||
# 等待 0.2 秒(每秒5次)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error in data fetch loop: {e}")
|
||||
await asyncio.sleep(1) # 出错时等待更长时间
|
||||
|
||||
def register_sensor(self, sensor_id: str, sensor_entity):
|
||||
"""注册传感器实体"""
|
||||
self.sensors[sensor_id] = sensor_entity
|
||||
_LOGGER.info(f"Registered sensor: {sensor_id}")
|
||||
|
||||
async def stop(self):
|
||||
"""停止 MQTT 客户端和数据获取任务"""
|
||||
if self.data_task:
|
||||
self.data_task.cancel()
|
||||
try:
|
||||
await self.data_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if self.client:
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
|
||||
_LOGGER.info("MQTT Data Manager stopped")
|
||||
|
||||
# 全局数据管理器实例
|
||||
_data_manager = None
|
||||
|
||||
# 传感器配置(对应 main.py 中的传感器定义)
|
||||
SENSORS = {
|
||||
"solar_power": {
|
||||
"name": "Solar Power",
|
||||
"unit": UnitOfPower.WATT,
|
||||
"icon": "mdi:solar-power",
|
||||
"device_class": SensorDeviceClass.POWER,
|
||||
"state_class": SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
"home_power": {
|
||||
"name": "Home Power",
|
||||
"unit": UnitOfPower.WATT,
|
||||
"icon": "mdi:home-lightning-bolt",
|
||||
"device_class": SensorDeviceClass.POWER,
|
||||
"state_class": SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
"grid_import": {
|
||||
"name": "Grid Import",
|
||||
"unit": UnitOfPower.WATT,
|
||||
"icon": "mdi:transmission-tower-import",
|
||||
"device_class": SensorDeviceClass.POWER,
|
||||
"state_class": SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
"grid_export": {
|
||||
"name": "Grid Export",
|
||||
"unit": UnitOfPower.WATT,
|
||||
"icon": "mdi:transmission-tower-export",
|
||||
"device_class": SensorDeviceClass.POWER,
|
||||
"state_class": SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
"battery_charge": {
|
||||
"name": "Battery Charge",
|
||||
"unit": UnitOfPower.WATT,
|
||||
"icon": "mdi:battery-charging",
|
||||
"device_class": SensorDeviceClass.POWER,
|
||||
"state_class": SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
"battery_discharge": {
|
||||
"name": "Battery Discharge",
|
||||
"unit": UnitOfPower.WATT,
|
||||
"icon": "mdi:battery-minus",
|
||||
"device_class": SensorDeviceClass.POWER,
|
||||
"state_class": SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
"battery_soc": {
|
||||
"name": "Battery State of Charge",
|
||||
"unit": PERCENTAGE,
|
||||
"icon": "mdi:battery-70",
|
||||
"device_class": SensorDeviceClass.BATTERY,
|
||||
"state_class": SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Energy Monitor sensors from a config entry."""
|
||||
global _data_manager
|
||||
|
||||
_LOGGER.info("Setting up Energy Monitor sensors")
|
||||
|
||||
# 获取配置数据
|
||||
config = hass.data[DOMAIN][config_entry.entry_id]
|
||||
topic_prefix = config.get("topic_prefix", "homeassistant/sensor")
|
||||
mqtt_broker = config.get("mqtt_broker", "192.168.0.101")
|
||||
mqtt_port = config.get("mqtt_port", 1883)
|
||||
|
||||
# 创建全局数据管理器(如果还没有创建)
|
||||
if _data_manager is None:
|
||||
_data_manager = MQTTDataManager(hass, topic_prefix, mqtt_broker, mqtt_port)
|
||||
await _data_manager.start()
|
||||
|
||||
# 创建所有传感器实体
|
||||
entities = []
|
||||
for sensor_id, sensor_config in SENSORS.items():
|
||||
entity = EnergyMonitorSensor(
|
||||
sensor_id=sensor_id,
|
||||
name=sensor_config["name"],
|
||||
unit=sensor_config["unit"],
|
||||
icon=sensor_config["icon"],
|
||||
device_class=sensor_config["device_class"],
|
||||
state_class=sensor_config["state_class"],
|
||||
topic_prefix=topic_prefix,
|
||||
)
|
||||
entities.append(entity)
|
||||
|
||||
# 将传感器注册到数据管理器
|
||||
_data_manager.register_sensor(sensor_id, entity)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
_LOGGER.info(f"Added {len(entities)} Energy Monitor sensors")
|
||||
|
||||
|
||||
class EnergyMonitorSensor(SensorEntity):
|
||||
"""Representation of an Energy Monitor Sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sensor_id: str,
|
||||
name: str,
|
||||
unit: str,
|
||||
icon: str,
|
||||
device_class: SensorDeviceClass,
|
||||
state_class: SensorStateClass,
|
||||
topic_prefix: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._sensor_id = sensor_id
|
||||
self._attr_name = name
|
||||
self._attr_native_unit_of_measurement = unit
|
||||
self._attr_icon = icon
|
||||
self._attr_device_class = device_class
|
||||
self._attr_state_class = state_class
|
||||
self._attr_unique_id = f"energy_monitor_{sensor_id}"
|
||||
self._topic = f"{topic_prefix}/{sensor_id}/state"
|
||||
self._attr_native_value = None
|
||||
self._attr_available = False
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up the sensor."""
|
||||
_LOGGER.info(f"Energy Monitor sensor {self._sensor_id} added to Home Assistant")
|
||||
# 注意:现在数据通过 MQTTDataManager 处理,不再直接订阅 MQTT topic
|
||||
|
||||
async def _update_state(self, value: Any) -> None:
|
||||
"""更新传感器状态(由 MQTTDataManager 调用)"""
|
||||
try:
|
||||
# 确保值是数字类型
|
||||
if isinstance(value, (int, float)):
|
||||
self._attr_native_value = value
|
||||
self._attr_available = True
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(f"Updated {self._sensor_id} with value: {value}")
|
||||
else:
|
||||
_LOGGER.warning(f"Invalid value type for {self._sensor_id}: {type(value)} - {value}")
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error updating sensor {self._sensor_id}: {e}")
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
"sensor_id": self._sensor_id,
|
||||
"mqtt_topic": self._topic,
|
||||
}
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload the sensor platform."""
|
||||
global _data_manager
|
||||
|
||||
_LOGGER.info("Unloading Energy Monitor sensors")
|
||||
|
||||
# 停止数据管理器
|
||||
if _data_manager is not None:
|
||||
await _data_manager.stop()
|
||||
_data_manager = None
|
||||
|
||||
return True
|
||||
|
||||
19
custom_components/JackeryHome/strings.json
Normal file
19
custom_components/JackeryHome/strings.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "配置 JackeryHome",
|
||||
"description": "设置 MQTT 主题前缀以接收能源监控数据",
|
||||
"data": {
|
||||
"topic_prefix": "MQTT 主题前缀"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "该集成已配置"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "该集成已配置"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
custom_components/JackeryHome/translations/zh-Hans.json
Normal file
19
custom_components/JackeryHome/translations/zh-Hans.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "配置 JackeryHome",
|
||||
"description": "设置 MQTT 主题前缀以接收能源监控数据",
|
||||
"data": {
|
||||
"topic_prefix": "MQTT 主题前缀"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "该集成已配置"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "该集成已配置"
|
||||
}
|
||||
}
|
||||
}
|
||||
172
data_transmission_example.py
Normal file
172
data_transmission_example.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据传输示例
|
||||
演示如何使用修改后的 Energy Monitor 系统进行数据传输
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
class DataTransmissionExample:
|
||||
"""数据传输示例类"""
|
||||
|
||||
def __init__(self, broker="192.168.0.101", port=1883):
|
||||
self.broker = broker
|
||||
self.port = port
|
||||
self.client = None
|
||||
self.running = False
|
||||
|
||||
def setup_mqtt(self):
|
||||
"""设置 MQTT 客户端"""
|
||||
self.client = mqtt.Client(client_id="energy_device_simulator", callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_message = self.on_message
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc, properties):
|
||||
"""MQTT 连接回调"""
|
||||
if rc == 0:
|
||||
print("✅ 连接到 MQTT 代理成功")
|
||||
# 订阅数据获取请求主题
|
||||
client.subscribe("/data/data-get")
|
||||
print("✅ 订阅 /data/data-get 主题成功")
|
||||
else:
|
||||
print(f"❌ 连接 MQTT 代理失败,错误码: {rc}")
|
||||
|
||||
def on_message(self, client, userdata, msg):
|
||||
"""MQTT 消息接收回调"""
|
||||
if msg.topic == "/data/data-get":
|
||||
print(f"📨 收到数据请求: {msg.payload.decode()}")
|
||||
# 模拟处理时间
|
||||
time.sleep(0.1)
|
||||
# 发送模拟数据
|
||||
self.send_device_data()
|
||||
|
||||
def generate_sample_data(self):
|
||||
"""生成模拟的设备数据"""
|
||||
# 模拟太阳能发电(白天较高,夜晚较低)
|
||||
hour = time.localtime().tm_hour
|
||||
if 6 <= hour <= 18: # 白天
|
||||
solar_power = random.uniform(500, 3000)
|
||||
else: # 夜晚
|
||||
solar_power = random.uniform(0, 100)
|
||||
|
||||
# 模拟家庭用电
|
||||
home_power = random.uniform(800, 2500)
|
||||
|
||||
# 计算电网功率(家庭用电 - 太阳能发电)
|
||||
grid_power = home_power - solar_power
|
||||
|
||||
# 分离电网功率为购买和出售
|
||||
grid_import = max(0, grid_power) # 从电网购买
|
||||
grid_export = max(0, -grid_power) # 向电网出售
|
||||
|
||||
# 模拟电池充放电
|
||||
battery_power = random.uniform(-800, 800)
|
||||
battery_charge = max(0, -battery_power) # 充电
|
||||
battery_discharge = max(0, battery_power) # 放电
|
||||
|
||||
# 模拟电池电量(根据充放电状态变化)
|
||||
if not hasattr(self, 'battery_soc'):
|
||||
self.battery_soc = random.uniform(30, 90)
|
||||
|
||||
# 根据充放电更新电量
|
||||
if battery_power > 0: # 放电
|
||||
self.battery_soc = max(0, self.battery_soc - 0.5)
|
||||
elif battery_power < 0: # 充电
|
||||
self.battery_soc = min(100, self.battery_soc + 0.3)
|
||||
|
||||
return {
|
||||
"solar_power": round(solar_power, 2),
|
||||
"home_power": round(home_power, 2),
|
||||
"grid_import": round(grid_import, 2),
|
||||
"grid_export": round(grid_export, 2),
|
||||
"battery_charge": round(battery_charge, 2),
|
||||
"battery_discharge": round(battery_discharge, 2),
|
||||
"battery_soc": round(self.battery_soc, 1)
|
||||
}
|
||||
|
||||
def send_device_data(self):
|
||||
"""发送设备数据到 /device/data 主题"""
|
||||
data = self.generate_sample_data()
|
||||
|
||||
# 转换为 JSON 格式
|
||||
json_data = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
# 发布到 /device/data 主题
|
||||
result = self.client.publish("/device/data", json_data)
|
||||
|
||||
if result.rc == mqtt.MQTT_ERR_SUCCESS:
|
||||
print("📤 发送设备数据:")
|
||||
print(f" 主题: /device/data")
|
||||
print(f" 数据: {json_data}")
|
||||
print()
|
||||
else:
|
||||
print(f"❌ 发送数据失败,错误码: {result.rc}")
|
||||
|
||||
def start_simulation(self, duration=60):
|
||||
"""启动数据模拟"""
|
||||
print("🚀 启动数据传输模拟")
|
||||
print(f"📡 MQTT 代理: {self.broker}:{self.port}")
|
||||
print(f"⏱️ 运行时长: {duration} 秒")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
# 连接 MQTT 代理
|
||||
self.client.connect(self.broker, self.port, 60)
|
||||
self.client.loop_start()
|
||||
self.running = True
|
||||
|
||||
# 等待连接建立
|
||||
time.sleep(2)
|
||||
|
||||
# 发送初始数据
|
||||
print("📤 发送初始数据...")
|
||||
self.send_device_data()
|
||||
|
||||
# 运行指定时间
|
||||
start_time = time.time()
|
||||
while self.running and (time.time() - start_time) < duration:
|
||||
time.sleep(1)
|
||||
|
||||
# 每5秒发送一次数据(模拟设备主动发送)
|
||||
if int(time.time() - start_time) % 5 == 0:
|
||||
print("📤 设备主动发送数据...")
|
||||
self.send_device_data()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⏹️ 用户中断模拟")
|
||||
except Exception as e:
|
||||
print(f"❌ 模拟出错: {e}")
|
||||
finally:
|
||||
self.stop_simulation()
|
||||
|
||||
def stop_simulation(self):
|
||||
"""停止模拟"""
|
||||
self.running = False
|
||||
if self.client:
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
print("✅ 模拟已停止")
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("🏠 Energy Monitor 数据传输示例")
|
||||
print("=" * 50)
|
||||
print("这个示例演示了以下功能:")
|
||||
print("1. 监听 /data/data-get 请求")
|
||||
print("2. 响应请求并发送设备数据到 /device/data")
|
||||
print("3. 模拟真实的能源监控数据")
|
||||
print("4. 每秒5次的数据获取频率(由 Home Assistant 集成触发)")
|
||||
print()
|
||||
|
||||
# 创建示例实例
|
||||
example = DataTransmissionExample()
|
||||
example.setup_mqtt()
|
||||
|
||||
# 启动模拟(运行60秒)
|
||||
example.start_simulation(duration=60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
54
delete.py
Normal file
54
delete.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import json
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
MQTT_BROKER = "192.168.0.101"
|
||||
MQTT_PORT = 1883
|
||||
MQTT_USERNAME = ""
|
||||
MQTT_PASSWORD = ""
|
||||
MQTT_CLIENT_ID = "ha_delete_discovery"
|
||||
|
||||
SENSOR_IDS = [
|
||||
"solar_power",
|
||||
"home_power",
|
||||
"grid_import",
|
||||
"grid_export",
|
||||
"battery_charge",
|
||||
"battery_discharge",
|
||||
"battery_soc",
|
||||
"battery_power",
|
||||
"grid_power"
|
||||
]
|
||||
|
||||
# ==== 修改后的回调 ====
|
||||
def on_connect(client, userdata, flags, reason_code, properties=None):
|
||||
if reason_code == 0:
|
||||
print("✅ 已连接到 MQTT Broker")
|
||||
else:
|
||||
print(f"❌ 连接失败,原因码: {reason_code}")
|
||||
|
||||
def on_publish(client, userdata, mid, reason_code, properties=None):
|
||||
print(f"🧹 已发送删除命令 (mid={mid})")
|
||||
|
||||
def delete_discovery_configs():
|
||||
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, MQTT_CLIENT_ID)
|
||||
if MQTT_USERNAME:
|
||||
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
|
||||
|
||||
client.on_connect = on_connect
|
||||
client.on_publish = on_publish
|
||||
|
||||
print("🚀 正在连接 MQTT Broker ...")
|
||||
client.connect(MQTT_BROKER, MQTT_PORT, 60)
|
||||
client.loop_start()
|
||||
|
||||
for sensor_id in SENSOR_IDS:
|
||||
topic = f"homeassistant/sensor/{sensor_id}/config"
|
||||
client.publish(topic, None, retain=True)
|
||||
print(f"🗑️ 已发布空配置以删除实体:{sensor_id}")
|
||||
|
||||
client.loop_stop()
|
||||
client.disconnect()
|
||||
print("✅ 所有 Discovery 配置已删除")
|
||||
|
||||
if __name__ == "__main__":
|
||||
delete_discovery_configs()
|
||||
35
energy_flow_card_config.yaml
Normal file
35
energy_flow_card_config.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
type: custom:energy-flow-card-plus
|
||||
entities:
|
||||
solar:
|
||||
entity: sensor.solar_power
|
||||
name: 太阳能
|
||||
icon: mdi:solar-power
|
||||
grid:
|
||||
entity:
|
||||
consumption: sensor.grid_import
|
||||
production: sensor.grid_export
|
||||
name: 电网
|
||||
icon: mdi:transmission-tower
|
||||
battery:
|
||||
entity:
|
||||
consumption: sensor.battery_charge
|
||||
production: sensor.battery_discharge
|
||||
state_of_charge: sensor.battery_soc
|
||||
name: 电池
|
||||
icon: mdi:battery
|
||||
home:
|
||||
entity: sensor.home_power
|
||||
name: 家庭用电
|
||||
icon: mdi:home-lightning-bolt
|
||||
display_zero_lines:
|
||||
mode: show
|
||||
transparency: 50
|
||||
grey_color:
|
||||
- 189
|
||||
- 189
|
||||
- 189
|
||||
w_decimals: 0
|
||||
kw_decimals: 2
|
||||
color_icons: true
|
||||
animation_speed: 10
|
||||
energy_date_selection: false
|
||||
11
hacs.json
Normal file
11
hacs.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "JackeryHome",
|
||||
"content_in_root": false,
|
||||
"render_readme": true,
|
||||
"domains": [
|
||||
"energy_monitor",
|
||||
"mqtt"
|
||||
],
|
||||
"iot_class": "Local Push",
|
||||
"homeassistant": "2024.1.0"
|
||||
}
|
||||
191
main.py
Normal file
191
main.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
MQTT_BROKER = "192.168.0.101"
|
||||
MQTT_PORT = 1883
|
||||
MQTT_CLIENT_ID = "hem_simulator"
|
||||
|
||||
# 模式控制变量
|
||||
# 0: 自发自用模式 (Self-consumption)
|
||||
# 1: 电池优先模式 (Battery Priority)
|
||||
current_mode = 0
|
||||
|
||||
# 模式名称映射
|
||||
MODE_NAMES = {
|
||||
0: "自发自用模式",
|
||||
1: "电池优先模式"
|
||||
}
|
||||
|
||||
client = mqtt.Client(client_id=MQTT_CLIENT_ID, callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
|
||||
|
||||
# MQTT 连接回调
|
||||
def on_connect(client, userdata, flags, rc, properties):
|
||||
print("Connected to MQTT broker with result code " + str(rc))
|
||||
publish_discovery_configs()
|
||||
# 订阅模式控制命令主题
|
||||
client.subscribe("homeassistant/select/mode_control/set")
|
||||
print("Subscribed to mode control command topic")
|
||||
|
||||
# MQTT 消息接收回调
|
||||
def on_message(client, userdata, msg):
|
||||
global current_mode
|
||||
if msg.topic == "homeassistant/select/mode_control/set":
|
||||
try:
|
||||
payload = msg.payload.decode()
|
||||
# 支持数字或模式名称
|
||||
if payload.isdigit():
|
||||
new_mode = int(payload)
|
||||
elif payload == "自发自用模式":
|
||||
new_mode = 0
|
||||
elif payload == "电池优先模式":
|
||||
new_mode = 1
|
||||
else:
|
||||
print(f"Invalid mode payload: {payload}")
|
||||
return
|
||||
|
||||
if new_mode in [0, 1]:
|
||||
current_mode = new_mode
|
||||
# 发布新的状态(使用模式名称)
|
||||
client.publish("homeassistant/select/mode_control/state", MODE_NAMES[current_mode])
|
||||
print(f"模式已切换到: {MODE_NAMES[current_mode]} ({current_mode})")
|
||||
else:
|
||||
print(f"Invalid mode value: {new_mode}")
|
||||
except ValueError:
|
||||
print(f"Invalid mode payload: {msg.payload.decode()}")
|
||||
|
||||
client.on_connect = on_connect
|
||||
client.on_message = on_message
|
||||
|
||||
|
||||
def publish_discovery_configs():
|
||||
"""发布 Home Assistant MQTT Discovery 配置"""
|
||||
sensors = {
|
||||
"solar_power": {
|
||||
"name": "Solar Power",
|
||||
"unit": "W",
|
||||
"icon": "mdi:solar-power",
|
||||
"device_class": "power",
|
||||
},
|
||||
"home_power": {
|
||||
"name": "Home Power",
|
||||
"unit": "W",
|
||||
"icon": "mdi:home-lightning-bolt",
|
||||
"device_class": "power",
|
||||
},
|
||||
# 电网 - 分为购买和出售
|
||||
"grid_import": {
|
||||
"name": "Grid Import",
|
||||
"unit": "W",
|
||||
"icon": "mdi:transmission-tower-import",
|
||||
"device_class": "power",
|
||||
},
|
||||
"grid_export": {
|
||||
"name": "Grid Export",
|
||||
"unit": "W",
|
||||
"icon": "mdi:transmission-tower-export",
|
||||
"device_class": "power",
|
||||
},
|
||||
# 电池 - 分为充电和放电
|
||||
"battery_charge": {
|
||||
"name": "Battery Charge",
|
||||
"unit": "W",
|
||||
"icon": "mdi:battery-charging",
|
||||
"device_class": "power",
|
||||
},
|
||||
"battery_discharge": {
|
||||
"name": "Battery Discharge",
|
||||
"unit": "W",
|
||||
"icon": "mdi:battery-minus",
|
||||
"device_class": "power",
|
||||
},
|
||||
"battery_soc": {
|
||||
"name": "Battery State of Charge",
|
||||
"unit": "%",
|
||||
"icon": "mdi:battery-70",
|
||||
"device_class": "battery",
|
||||
},
|
||||
}
|
||||
|
||||
for sensor_id, props in sensors.items():
|
||||
topic = f"homeassistant/sensor/{sensor_id}/config"
|
||||
payload = {
|
||||
"name": props["name"],
|
||||
"state_topic": f"homeassistant/sensor/{sensor_id}/state",
|
||||
"unit_of_measurement": props["unit"],
|
||||
"device_class": props["device_class"],
|
||||
"icon": props["icon"],
|
||||
"unique_id": sensor_id,
|
||||
}
|
||||
client.publish(topic, json.dumps(payload), retain=True)
|
||||
print(f"Published discovery config for {sensor_id}")
|
||||
|
||||
# 发布模式控制的 discovery 配置
|
||||
mode_topic = "homeassistant/select/mode_control/config"
|
||||
mode_payload = {
|
||||
"name": "运行模式",
|
||||
"state_topic": "homeassistant/select/mode_control/state",
|
||||
"command_topic": "homeassistant/select/mode_control/set",
|
||||
"options": ["自发自用模式", "电池优先模式"],
|
||||
"icon": "mdi:cog-outline",
|
||||
"unique_id": "mode_control",
|
||||
}
|
||||
client.publish(mode_topic, json.dumps(mode_payload), retain=True)
|
||||
print("Published discovery config for mode_control")
|
||||
|
||||
# 发布初始模式状态(使用模式名称)
|
||||
client.publish("homeassistant/select/mode_control/state", MODE_NAMES[current_mode])
|
||||
print(f"Published initial mode state: {MODE_NAMES[current_mode]} ({current_mode})")
|
||||
|
||||
|
||||
def publish_sensor_data():
|
||||
"""定期发布模拟功率数据"""
|
||||
battery_soc = random.uniform(20, 100) # 初始电池电量
|
||||
|
||||
while True:
|
||||
solar = random.uniform(200, 3000) # 太阳能发电
|
||||
home = random.uniform(500, 3500) # 家庭负载
|
||||
grid = home - solar # 电网供电(可能为负)
|
||||
battery = random.uniform(-1000, 1000) # 电池充/放电
|
||||
|
||||
# 将电网功率分离为购买(import)和出售(export)
|
||||
grid_import = max(0, grid) # 从电网购买(正值)
|
||||
grid_export = max(0, -grid) # 向电网出售(转为正值)
|
||||
|
||||
# 将电池功率分离为充电和放电
|
||||
battery_charge = max(0, -battery) # 充电(转为正值)
|
||||
battery_discharge = max(0, battery) # 放电(正值)
|
||||
|
||||
# 根据电池充放电模拟电量变化
|
||||
if battery < 0: # 充电
|
||||
battery_soc = min(100, battery_soc + 0.5)
|
||||
elif battery > 0: # 放电
|
||||
battery_soc = max(0, battery_soc - 0.3)
|
||||
|
||||
data = {
|
||||
"solar_power": round(solar, 2),
|
||||
"home_power": round(home, 2),
|
||||
"grid_import": round(grid_import, 2),
|
||||
"grid_export": round(grid_export, 2),
|
||||
"battery_charge": round(battery_charge, 2),
|
||||
"battery_discharge": round(battery_discharge, 2),
|
||||
"battery_soc": round(battery_soc, 1),
|
||||
}
|
||||
|
||||
for key, value in data.items():
|
||||
topic = f"homeassistant/sensor/{key}/state"
|
||||
client.publish(topic, value)
|
||||
|
||||
# 发布当前模式状态
|
||||
client.publish("homeassistant/select/mode_control/state", MODE_NAMES[current_mode])
|
||||
|
||||
print("Published:", data, f"| 运行模式: {MODE_NAMES[current_mode]}")
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
client.connect(MQTT_BROKER, MQTT_PORT, 60)
|
||||
client.loop_start()
|
||||
publish_sensor_data()
|
||||
90
prepare_release.sh
Executable file
90
prepare_release.sh
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
|
||||
# JackeryHome HACS 发布准备脚本
|
||||
# 此脚本帮助你准备发布到 HACS
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 准备发布 JackeryHome 到 HACS"
|
||||
echo ""
|
||||
|
||||
# 检查是否在正确的目录
|
||||
if [ ! -f "hacs.json" ]; then
|
||||
echo "❌ 错误:未找到 hacs.json 文件"
|
||||
echo "请确保在项目根目录运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查是否有未提交的更改
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo "⚠️ 检测到未提交的更改"
|
||||
echo ""
|
||||
git status --short
|
||||
echo ""
|
||||
read -p "是否要提交这些更改?(y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
read -p "请输入提交信息: " commit_msg
|
||||
git add .
|
||||
git commit -m "$commit_msg"
|
||||
echo "✅ 更改已提交"
|
||||
else
|
||||
echo "❌ 请先提交或暂存你的更改"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 获取当前版本
|
||||
CURRENT_VERSION=$(grep -o '"version": "[^"]*"' custom_components/JackeryHome/manifest.json | cut -d'"' -f4)
|
||||
echo "📦 当前版本: $CURRENT_VERSION"
|
||||
echo ""
|
||||
|
||||
# 询问新版本
|
||||
read -p "请输入新版本号 (当前: $CURRENT_VERSION): " NEW_VERSION
|
||||
|
||||
if [ -z "$NEW_VERSION" ]; then
|
||||
NEW_VERSION=$CURRENT_VERSION
|
||||
echo "使用当前版本: $NEW_VERSION"
|
||||
fi
|
||||
|
||||
# 更新 manifest.json 中的版本号
|
||||
if [ "$NEW_VERSION" != "$CURRENT_VERSION" ]; then
|
||||
echo "📝 更新 manifest.json 中的版本号..."
|
||||
sed -i.bak "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" custom_components/JackeryHome/manifest.json
|
||||
rm custom_components/JackeryHome/manifest.json.bak
|
||||
git add custom_components/JackeryHome/manifest.json
|
||||
git commit -m "版本更新至 v$NEW_VERSION"
|
||||
echo "✅ 版本号已更新"
|
||||
fi
|
||||
|
||||
# 推送到 GitHub
|
||||
echo ""
|
||||
echo "📤 推送到 GitHub..."
|
||||
git push origin main
|
||||
|
||||
# 创建 tag
|
||||
TAG_NAME="v$NEW_VERSION"
|
||||
echo ""
|
||||
echo "🏷️ 创建 Git tag: $TAG_NAME"
|
||||
git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
|
||||
git push origin "$TAG_NAME"
|
||||
|
||||
echo ""
|
||||
echo "✅ 准备完成!"
|
||||
echo ""
|
||||
echo "📋 下一步操作:"
|
||||
echo "1. 访问 GitHub 创建 Release:"
|
||||
echo " https://github.com/suyulin/home-assistant-demo-mqtt/releases/new?tag=$TAG_NAME"
|
||||
echo ""
|
||||
echo "2. 或者使用以下命令创建 Release (需要 gh CLI):"
|
||||
echo " gh release create $TAG_NAME --title \"$TAG_NAME\" --notes \"Release $TAG_NAME\""
|
||||
echo ""
|
||||
echo "3. 用户可以通过以下方式添加到 HACS:"
|
||||
echo " - 在 HACS 中添加自定义存储库"
|
||||
echo " - URL: https://github.com/suyulin/home-assistant-demo-mqtt"
|
||||
echo " - 类别: Integration"
|
||||
echo ""
|
||||
echo "4. 查看完整发布指南:"
|
||||
echo " cat HACS_PUBLISHING_GUIDE.md"
|
||||
echo ""
|
||||
|
||||
8
pyproject.toml
Normal file
8
pyproject.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[project]
|
||||
name = "home-assistant-demo-mqtt"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"paho-mqtt>=2.1.0",
|
||||
]
|
||||
23
uv.lock
generated
Normal file
23
uv.lock
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "home-assistant-demo-mqtt"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "paho-mqtt" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "paho-mqtt", specifier = ">=2.1.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "paho-mqtt"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user