不求圣剑
2026-01-26 15:36:49 +08:00
parent 45d5e5fa80
commit 1a971c975c
2 changed files with 211 additions and 13 deletions

View File

@@ -203,6 +203,48 @@ SENSORS = {
"icon": "mdi:power-sleep", "icon": "mdi:power-sleep",
"device_class": None, "device_class": None,
"state_class": None, # 0-Invalid, 1-Sleep/Off, 2-On "state_class": None, # 0-Invalid, 1-Sleep/Off, 2-On
},
# Calculated Sensors
"home_power": {
"json_key": "calc_home_power",
"name": "Home Power",
"unit": UnitOfPower.WATT,
"icon": "mdi:home-lightning-bolt",
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
},
"battery_net_power": {
"json_key": "calc_batt_net_power",
"name": "Battery Net Power",
"unit": UnitOfPower.WATT,
"icon": "mdi:battery-sync",
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
},
"calc_battery_charge_power": {
"json_key": "calc_battery_charge_power",
"name": "Battery Charge Power (Calc)",
"unit": UnitOfPower.WATT,
"icon": "mdi:battery-charging",
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
},
"calc_battery_discharge_power": {
"json_key": "calc_battery_discharge_power",
"name": "Battery Discharge Power (Calc)",
"unit": UnitOfPower.WATT,
"icon": "mdi:battery-minus",
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
},
"grid_net_power": {
"json_key": "calc_grid_net_power",
"name": "Grid Net Power",
"unit": UnitOfPower.WATT,
"icon": "mdi:transmission-tower",
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
} }
} }
@@ -300,18 +342,151 @@ class JackeryDataCoordinator:
_LOGGER.warning(f"Invalid JSON payload on {topic}") _LOGGER.warning(f"Invalid JSON payload on {topic}")
return return
# Enrich data with calculations
data = self._calculate_energy_flow(data)
self._distribute_data(data) self._distribute_data(data)
except Exception as e: except Exception as e:
_LOGGER.error(f"Error handling message: {e}") _LOGGER.error(f"Error handling message: {e}")
def _calculate_energy_flow(self, data: dict) -> dict:
"""
根据用户需求计算能量流数据.
Variables Mapping:
- PV: pvPw
- OngridCharge: inOngridPw
- OngridSupply: outOngridPw
- ACIn: swEpsInPw
- ACOut: swEpsOutPw
- GridBuy: (Need Key, assuming 'gridBuyPw' or similar, else None)
- GridSell: (Need Key, assuming 'gridSellPw', else None)
"""
try:
# 1. PV
# Handle dict for PV if necessary (copied from sensor logic)
pv_val = data.get("pvPw", 0)
if isinstance(pv_val, dict):
pv = float(pv_val.get("pvPw", 0) or pv_val.get("w", 0) or pv_val.get("power", 0))
else:
pv = float(pv_val)
# 2. Ongrid
ongrid_charge = float(data.get("inOngridPw", 0))
ongrid_supply = float(data.get("outOngridPw", 0))
p_ong = ongrid_charge - ongrid_supply # 流入主机为正
# 3. ACSocket (EPS)
ac_in = float(data.get("swEpsInPw", 0))
ac_out = float(data.get("swEpsOutPw", 0))
p_ac = ac_in - ac_out # 流入主机为正
# 4. Grid (Meter)
# 优先从 'cts' 数组中提取 CT 数据 (Smart CT Meter)
# cts item: { ..., "TphasePw": <Import>, "TnphasePw": <Export>, "commState": 1/0, ... }
grid_available = False
grid_buy = 0.0
grid_sell = 0.0
cts = data.get("cts")
if cts and isinstance(cts, list) and len(cts) > 0:
# 尝试获取第一个 CT 数据
ct_data = cts[0]
# 检查通讯状态 (如果 commState 存在且为 0 可能表示离线,视具体协议而定,这里暂定只要有数据即可)
# TphasePw: 总正向有功 (Grid Buy)
# TnphasePw: 总负向有功 (Grid Sell)
t_phase_pw = ct_data.get("TphasePw")
tn_phase_pw = ct_data.get("TnphasePw")
if t_phase_pw is not None and tn_phase_pw is not None:
grid_buy = float(t_phase_pw)
grid_sell = float(tn_phase_pw)
grid_available = True
# 兼容旧逻辑或直接字段 (如果 cts 不存在)
if not grid_available:
grid_buy_raw = data.get("gridBuyPw") # Hypothetical key
grid_sell_raw = data.get("gridSellPw") # Hypothetical key
if grid_buy_raw is not None and grid_sell_raw is not None:
grid_available = True
grid_buy = float(grid_buy_raw)
grid_sell = float(grid_sell_raw)
# Calculate P_grid
p_grid = None
if grid_available:
p_grid = grid_buy - grid_sell
# 🔴异常流程(仅当电表可用且并网口处于充电态时生效)
# GridAvailable=true 且 GridBuy < OngridCharge 且 (OngridCharge - GridBuy) <= 50W
if grid_buy < ongrid_charge and (ongrid_charge - grid_buy) <= 50:
p_grid = p_ong
# 5. Battery (Calculated)
# P_batt = P_pv + P_ac + P_ong
p_batt = pv + p_ac + p_ong
# 6. Home (Calculated)
p_home = 0.0
if p_grid is not None:
# 电表可用
p_home = p_grid - p_ong
# 🔴 异常分支 1
if grid_buy > 0 and ongrid_charge > 0 and grid_buy < ongrid_charge and (ongrid_charge - grid_buy) <= 50:
# p_grid = p_ong # Already handled in p_grid calc above?
# Note: User spec says "P_grid = P_ong (按异常流程先修正); P_home = 0"
# My P_grid calc above handled P_grid. Now P_home:
p_home = 0.0
# 🔴 异常分支 2
elif grid_buy > 0 and ongrid_charge > 0 and grid_buy < ongrid_charge and (ongrid_charge - grid_buy) > 50:
p_home = ongrid_charge - grid_buy
# 🔴 馈网场景分支 A
elif grid_sell > 0 and ongrid_supply > 0:
p_home = grid_sell - ongrid_supply
# 🔴 馈网场景分支 B
elif grid_sell > 0 and ongrid_charge > 0:
p_home = grid_sell + ongrid_charge
else:
# 电表不可用 (No CT)
if ongrid_supply > 0:
p_home = ongrid_supply
else:
p_home = 0.0
# Store calculated values
data["calc_home_power"] = p_home
data["calc_batt_net_power"] = p_batt
data["calc_battery_charge_power"] = max(0.0, p_batt)
data["calc_battery_discharge_power"] = max(0.0, -p_batt)
data["calc_grid_net_power"] = p_grid if p_grid is not None else 0 # Return 0 if None for sensor safety?
# Note: If p_grid is None, the sensor might show 0 or unavailable.
# Ideally "Grid Net Power" sensor should be unavailable if no CT.
# But let's set it to 0 for now or handle in sensor.
# Additional: We might want to pass 'grid_available' to data for sensor state?
if p_grid is None:
# If we return None, the sensor logic below might error or show Unknown.
# Let's leave it as None in data, and handle in sensor update.
data["calc_grid_net_power"] = None
except Exception as e:
_LOGGER.error(f"Error calculating energy flow: {e}")
return data
def _distribute_data(self, data: dict) -> None: def _distribute_data(self, data: dict) -> None:
"""分发数据给传感器.""" """分发数据给传感器."""
for sensor_id, entity in self._sensors.items(): for sensor_id, entity in self._sensors.items():
entity._update_from_coordinator(data) entity._update_from_coordinator(data)
async def _periodic_data_request(self) -> None: async def _periodic_data_request(self) -> None:
"""定期发送 'type: 25' 指令请求全量数据.""" """定期发送 'type: 25' 'type: 100' 指令."""
_LOGGER.info(f"Starting periodic data polling for {self._device_sn} via {self._mqtt_host}...") _LOGGER.info(f"Starting periodic data polling for {self._device_sn} via {self._mqtt_host}...")
await asyncio.sleep(2) await asyncio.sleep(2)
@@ -324,25 +499,49 @@ class JackeryDataCoordinator:
# Construct Action Topic # Construct Action Topic
action_topic = f"{self._topic_root}/device/{self._device_sn}/action" action_topic = f"{self._topic_root}/device/{self._device_sn}/action"
ts = int(time.time())
# Construct Payload
payload = { # 1. Poll Device Status (Type 25)
payload_25 = {
"type": 25, "type": 25,
"eventId": 0, "eventId": 0,
"messageId": random.randint(1000, 9999), "messageId": random.randint(1000, 9999),
"ts": int(time.time()), "ts": ts,
"token": self._token, "token": self._token,
"body": None "body": None
} }
await ha_mqtt.async_publish( await ha_mqtt.async_publish(
self.hass, self.hass,
action_topic, action_topic,
json.dumps(payload), json.dumps(payload_25),
0, 0,
False False
) )
_LOGGER.debug(f"Sent poll request to {action_topic}")
# 2. Poll Sub-devices (Type 100) - specifically for CTs (devType: 2)
# type=100 通知设备上报特定类型子设备全量数据
# devType: 2 (同时获取CT&电表采集头&电表)
payload_100 = {
"type": 100,
"eventId": 0,
"messageId": random.randint(1000, 9999),
"ts": ts,
"token": self._token,
"body": {
"devType": 2
}
}
await ha_mqtt.async_publish(
self.hass,
action_topic,
json.dumps(payload_100),
0,
False
)
_LOGGER.debug(f"Sent poll requests (25 & 100) to {action_topic}")
await asyncio.sleep(REQUEST_INTERVAL) await asyncio.sleep(REQUEST_INTERVAL)

View File

@@ -1,21 +1,20 @@
type: custom:power-flow-card-plus type: custom:power-flow-card-plus
entities: entities:
grid: grid:
entity: entity: sensor.jackery_grid_net_power
consumption: sensor.jackery_grid_import_power
production: sensor.jackery_grid_export_power
display_state: two_way display_state: two_way
color_circle: true color_circle: true
solar: solar:
entity: sensor.jackery_solar_power entity: sensor.jackery_solar_power
battery: battery:
entity: entity:
consumption: sensor.jackery_battery_charge_power consumption: sensor.jackery_calc_battery_charge_power
production: sensor.jackery_battery_discharge_power production: sensor.jackery_calc_battery_discharge_power
state_of_charge: sensor.jackery_battery_soc state_of_charge: sensor.jackery_battery_soc
display_state: two_way display_state: two_way
color_circle: true color_circle: true
home: home:
entity: sensor.jackery_home_power
color_icon: true color_icon: true
individual: individual:
- entity: sensor.jackery_eps_output_power - entity: sensor.jackery_eps_output_power