From 3f698d6e16927d777ea3b020d932b0cb41376dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E6=B1=82=E5=9C=A3=E5=89=91?= Date: Tue, 14 Oct 2025 10:44:00 +0800 Subject: [PATCH] first commit --- .github/workflows/validate.yml | 23 ++ .gitignore | 11 + .python-version | 1 + DATA_FORMAT.md | 145 ++++++++ LICENSE | 22 ++ README.md | 233 +++++++++++++ custom_components/JackeryHome/README.md | 140 ++++++++ custom_components/JackeryHome/__init__.py | 39 +++ custom_components/JackeryHome/config_flow.py | 69 ++++ custom_components/JackeryHome/manifest.json | 16 + custom_components/JackeryHome/sensor.py | 311 ++++++++++++++++++ custom_components/JackeryHome/strings.json | 19 ++ .../JackeryHome/translations/zh-Hans.json | 19 ++ data_transmission_example.py | 172 ++++++++++ delete.py | 54 +++ energy_flow_card_config.yaml | 35 ++ hacs.json | 11 + main.py | 191 +++++++++++ prepare_release.sh | 90 +++++ pyproject.toml | 8 + uv.lock | 23 ++ 21 files changed, 1632 insertions(+) create mode 100644 .github/workflows/validate.yml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 DATA_FORMAT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 custom_components/JackeryHome/README.md create mode 100644 custom_components/JackeryHome/__init__.py create mode 100644 custom_components/JackeryHome/config_flow.py create mode 100644 custom_components/JackeryHome/manifest.json create mode 100644 custom_components/JackeryHome/sensor.py create mode 100644 custom_components/JackeryHome/strings.json create mode 100644 custom_components/JackeryHome/translations/zh-Hans.json create mode 100644 data_transmission_example.py create mode 100644 delete.py create mode 100644 energy_flow_card_config.yaml create mode 100644 hacs.json create mode 100644 main.py create mode 100755 prepare_release.sh create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..f0aa649 --- /dev/null +++ b/.github/workflows/validate.yml @@ -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 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81fe3e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +.DS_Store diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/DATA_FORMAT.md b/DATA_FORMAT.md new file mode 100644 index 0000000..d51ac94 --- /dev/null +++ b/DATA_FORMAT.md @@ -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 格式正确性 +- 必需字段存在性 +- 数值类型正确性 +- 数值范围合理性 + +如果数据格式不正确,会在日志中记录警告信息。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cee65d6 --- /dev/null +++ b/LICENSE @@ -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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5d2e5f --- /dev/null +++ b/README.md @@ -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 + diff --git a/custom_components/JackeryHome/README.md b/custom_components/JackeryHome/README.md new file mode 100644 index 0000000..0ab0bb2 --- /dev/null +++ b/custom_components/JackeryHome/README.md @@ -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 + diff --git a/custom_components/JackeryHome/__init__.py b/custom_components/JackeryHome/__init__.py new file mode 100644 index 0000000..18e4bce --- /dev/null +++ b/custom_components/JackeryHome/__init__.py @@ -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 + diff --git a/custom_components/JackeryHome/config_flow.py b/custom_components/JackeryHome/config_flow.py new file mode 100644 index 0000000..40fe87b --- /dev/null +++ b/custom_components/JackeryHome/config_flow.py @@ -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) + diff --git a/custom_components/JackeryHome/manifest.json b/custom_components/JackeryHome/manifest.json new file mode 100644 index 0000000..3cfe87b --- /dev/null +++ b/custom_components/JackeryHome/manifest.json @@ -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" +} \ No newline at end of file diff --git a/custom_components/JackeryHome/sensor.py b/custom_components/JackeryHome/sensor.py new file mode 100644 index 0000000..47b1a8b --- /dev/null +++ b/custom_components/JackeryHome/sensor.py @@ -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 + diff --git a/custom_components/JackeryHome/strings.json b/custom_components/JackeryHome/strings.json new file mode 100644 index 0000000..b98e6b7 --- /dev/null +++ b/custom_components/JackeryHome/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "title": "配置 JackeryHome", + "description": "设置 MQTT 主题前缀以接收能源监控数据", + "data": { + "topic_prefix": "MQTT 主题前缀" + } + } + }, + "error": { + "already_configured": "该集成已配置" + }, + "abort": { + "already_configured": "该集成已配置" + } + } +} \ No newline at end of file diff --git a/custom_components/JackeryHome/translations/zh-Hans.json b/custom_components/JackeryHome/translations/zh-Hans.json new file mode 100644 index 0000000..b98e6b7 --- /dev/null +++ b/custom_components/JackeryHome/translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "title": "配置 JackeryHome", + "description": "设置 MQTT 主题前缀以接收能源监控数据", + "data": { + "topic_prefix": "MQTT 主题前缀" + } + } + }, + "error": { + "already_configured": "该集成已配置" + }, + "abort": { + "already_configured": "该集成已配置" + } + } +} \ No newline at end of file diff --git a/data_transmission_example.py b/data_transmission_example.py new file mode 100644 index 0000000..e3dea58 --- /dev/null +++ b/data_transmission_example.py @@ -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() diff --git a/delete.py b/delete.py new file mode 100644 index 0000000..7d72591 --- /dev/null +++ b/delete.py @@ -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() diff --git a/energy_flow_card_config.yaml b/energy_flow_card_config.yaml new file mode 100644 index 0000000..9e7b15d --- /dev/null +++ b/energy_flow_card_config.yaml @@ -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 diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..fdcc11a --- /dev/null +++ b/hacs.json @@ -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" +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..03bbd9d --- /dev/null +++ b/main.py @@ -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() diff --git a/prepare_release.sh b/prepare_release.sh new file mode 100755 index 0000000..f343486 --- /dev/null +++ b/prepare_release.sh @@ -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 "" + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c343b5c --- /dev/null +++ b/pyproject.toml @@ -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", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..5cfb9dc --- /dev/null +++ b/uv.lock @@ -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" }, +]