feat:加入 能流图计算逻辑
https://alidocs.dingtalk.com/i/nodes/ydxXB52LJq75R0a0SpdyZ7MAWqjMp697?cid=69072404141&utm_source=im&utm_scene=team_space&iframeQuery=utm_medium%3Dim_card%26utm_source%3Dim&utm_medium=im_card&corpId=ding01d381724bff4144bc961a6cb783455b
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user