first commit

This commit is contained in:
不求圣剑
2025-10-14 10:44:00 +08:00
commit 3f698d6e16
21 changed files with 1632 additions and 0 deletions

23
.github/workflows/validate.yml vendored Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
3.13

145
DATA_FORMAT.md Normal file
View 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
View 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
View File

@@ -0,0 +1,233 @@
# JackeryHome - Home Assistant 能源监控集成
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/hacs/integration)
[![GitHub Release](https://img.shields.io/github/release/suyulin/home-assistant-demo-mqtt.svg)](https://github.com/suyulin/home-assistant-demo-mqtt/releases)
[![License](https://img.shields.io/github/license/suyulin/home-assistant-demo-mqtt.svg)](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

View 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

View 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

View 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)

View 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"
}

View 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

View File

@@ -0,0 +1,19 @@
{
"config": {
"step": {
"user": {
"title": "配置 JackeryHome",
"description": "设置 MQTT 主题前缀以接收能源监控数据",
"data": {
"topic_prefix": "MQTT 主题前缀"
}
}
},
"error": {
"already_configured": "该集成已配置"
},
"abort": {
"already_configured": "该集成已配置"
}
}
}

View File

@@ -0,0 +1,19 @@
{
"config": {
"step": {
"user": {
"title": "配置 JackeryHome",
"description": "设置 MQTT 主题前缀以接收能源监控数据",
"data": {
"topic_prefix": "MQTT 主题前缀"
}
}
},
"error": {
"already_configured": "该集成已配置"
},
"abort": {
"already_configured": "该集成已配置"
}
}
}

View 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
View 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()

View 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
View 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
View 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
View 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
View 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
View 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" },
]