From 1a971c975caf92d33a22c2bc194e0a40c8e6ab9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E6=B1=82=E5=9C=A3=E5=89=91?= Date: Mon, 26 Jan 2026 15:36:49 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=8A=A0=E5=85=A5=20=E8=83=BD?= =?UTF-8?q?=E6=B5=81=E5=9B=BE=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- custom_components/jackery/sensor.py | 215 ++++++++++++++++++++++++++-- energy_flow_card_config.yaml | 9 +- 2 files changed, 211 insertions(+), 13 deletions(-) diff --git a/custom_components/jackery/sensor.py b/custom_components/jackery/sensor.py index f57bd6a..d4f48da 100644 --- a/custom_components/jackery/sensor.py +++ b/custom_components/jackery/sensor.py @@ -203,6 +203,48 @@ SENSORS = { "icon": "mdi:power-sleep", "device_class": None, "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}") return + # Enrich data with calculations + data = self._calculate_energy_flow(data) self._distribute_data(data) except Exception as 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": , "TnphasePw": , "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: """分发数据给传感器.""" for sensor_id, entity in self._sensors.items(): entity._update_from_coordinator(data) 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}...") await asyncio.sleep(2) @@ -324,25 +499,49 @@ class JackeryDataCoordinator: # Construct Action Topic action_topic = f"{self._topic_root}/device/{self._device_sn}/action" - - # Construct Payload - payload = { + ts = int(time.time()) + + # 1. Poll Device Status (Type 25) + payload_25 = { "type": 25, "eventId": 0, "messageId": random.randint(1000, 9999), - "ts": int(time.time()), + "ts": ts, "token": self._token, "body": None } - + await ha_mqtt.async_publish( self.hass, action_topic, - json.dumps(payload), + json.dumps(payload_25), 0, 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) diff --git a/energy_flow_card_config.yaml b/energy_flow_card_config.yaml index a0c446d..462f854 100644 --- a/energy_flow_card_config.yaml +++ b/energy_flow_card_config.yaml @@ -1,21 +1,20 @@ type: custom:power-flow-card-plus entities: grid: - entity: - consumption: sensor.jackery_grid_import_power - production: sensor.jackery_grid_export_power + entity: sensor.jackery_grid_net_power display_state: two_way color_circle: true solar: entity: sensor.jackery_solar_power battery: entity: - consumption: sensor.jackery_battery_charge_power - production: sensor.jackery_battery_discharge_power + consumption: sensor.jackery_calc_battery_charge_power + production: sensor.jackery_calc_battery_discharge_power state_of_charge: sensor.jackery_battery_soc display_state: two_way color_circle: true home: + entity: sensor.jackery_home_power color_icon: true individual: - entity: sensor.jackery_eps_output_power