feat: 加入 插座
This commit is contained in:
@@ -265,6 +265,9 @@ class JackeryDataCoordinator:
|
|||||||
self._data_task = None
|
self._data_task = None
|
||||||
self._subscribed = False
|
self._subscribed = False
|
||||||
|
|
||||||
|
self._known_plugs = set() # Set of known plug SNs
|
||||||
|
self.add_entities_callback = None # Callback to add new entities
|
||||||
|
|
||||||
# Topic patterns
|
# Topic patterns
|
||||||
self._topic_status_wildcard = f"{self._topic_root}/device/+/status"
|
self._topic_status_wildcard = f"{self._topic_root}/device/+/status"
|
||||||
|
|
||||||
@@ -344,11 +347,52 @@ class JackeryDataCoordinator:
|
|||||||
|
|
||||||
# Enrich data with calculations
|
# Enrich data with calculations
|
||||||
data = self._calculate_energy_flow(data)
|
data = self._calculate_energy_flow(data)
|
||||||
|
|
||||||
|
# Check for new plugs
|
||||||
|
self._check_for_new_plugs(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 _check_for_new_plugs(self, data: dict) -> None:
|
||||||
|
"""检查并添加新发现的插座."""
|
||||||
|
plugs = data.get("plugs")
|
||||||
|
if not plugs or not isinstance(plugs, list):
|
||||||
|
return
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for plug in plugs:
|
||||||
|
sn = plug.get("sn")
|
||||||
|
if sn and sn not in self._known_plugs:
|
||||||
|
_LOGGER.info(f"Discovered new plug: {sn}")
|
||||||
|
self._known_plugs.add(sn)
|
||||||
|
|
||||||
|
# Create plug sensor
|
||||||
|
# Pass entry_id if needed, but entity init can handle without it for simple cases?
|
||||||
|
# Need config_entry_id. We can store it in coordinator or pass it later.
|
||||||
|
# Actually, standard pattern is to use the config_entry passed in setup.
|
||||||
|
# We'll need to store entry_id in coordinator or make JackeryPlugSensor use it.
|
||||||
|
# Let's attach config_entry_id to coordinator in __init__?
|
||||||
|
# Or just pass it. Coordinator doesn't have it currently.
|
||||||
|
# We'll use a property or modify init.
|
||||||
|
# For now, let's assume async_add_entities callback handles the addition.
|
||||||
|
# But we need to instantiate the entity class.
|
||||||
|
|
||||||
|
# Wait, I need config_entry_id to create proper device_info identifiers.
|
||||||
|
# I'll add config_entry_id to JackeryDataCoordinator.
|
||||||
|
if hasattr(self, "config_entry_id"):
|
||||||
|
entity = JackeryPlugSensor(
|
||||||
|
plug_sn=sn,
|
||||||
|
coordinator=self,
|
||||||
|
config_entry_id=self.config_entry_id
|
||||||
|
)
|
||||||
|
new_entities.append(entity)
|
||||||
|
|
||||||
|
if new_entities and self.add_entities_callback:
|
||||||
|
self.add_entities_callback(new_entities)
|
||||||
|
|
||||||
def _calculate_energy_flow(self, data: dict) -> dict:
|
def _calculate_energy_flow(self, data: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
根据用户需求计算能量流数据.
|
根据用户需求计算能量流数据.
|
||||||
@@ -519,9 +563,8 @@ class JackeryDataCoordinator:
|
|||||||
False
|
False
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Poll Sub-devices (Type 100) - specifically for CTs (devType: 2)
|
# 2. Poll Sub-devices (Type 100) - CTs (2) and Plugs (6)
|
||||||
# type=100 通知设备上报特定类型子设备全量数据
|
for dev_type in [2, 6]:
|
||||||
# devType: 2 (同时获取CT&电表采集头&电表)
|
|
||||||
payload_100 = {
|
payload_100 = {
|
||||||
"type": 100,
|
"type": 100,
|
||||||
"eventId": 0,
|
"eventId": 0,
|
||||||
@@ -529,7 +572,7 @@ class JackeryDataCoordinator:
|
|||||||
"ts": ts,
|
"ts": ts,
|
||||||
"token": self._token,
|
"token": self._token,
|
||||||
"body": {
|
"body": {
|
||||||
"devType": 2
|
"devType": dev_type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,8 +583,9 @@ class JackeryDataCoordinator:
|
|||||||
0,
|
0,
|
||||||
False
|
False
|
||||||
)
|
)
|
||||||
|
await asyncio.sleep(0.5) # Avoid spamming too fast
|
||||||
|
|
||||||
_LOGGER.debug(f"Sent poll requests (25 & 100) to {action_topic}")
|
_LOGGER.debug(f"Sent poll requests (25 & 100 [2,6]) to {action_topic}")
|
||||||
|
|
||||||
await asyncio.sleep(REQUEST_INTERVAL)
|
await asyncio.sleep(REQUEST_INTERVAL)
|
||||||
|
|
||||||
@@ -565,6 +609,13 @@ async def async_setup_entry(
|
|||||||
device_sn = config.get("device_sn")
|
device_sn = config.get("device_sn")
|
||||||
|
|
||||||
coordinator = JackeryDataCoordinator(hass, topic_prefix, token, mqtt_host, device_sn)
|
coordinator = JackeryDataCoordinator(hass, topic_prefix, token, mqtt_host, device_sn)
|
||||||
|
coordinator.config_entry_id = config_entry.entry_id # Assign entry_id
|
||||||
|
|
||||||
|
# Register callback for dynamic entities
|
||||||
|
def add_entities_callback(new_entities):
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
coordinator.add_entities_callback = add_entities_callback
|
||||||
|
|
||||||
hass.data[DOMAIN][config_entry.entry_id]["coordinator"] = coordinator
|
hass.data[DOMAIN][config_entry.entry_id]["coordinator"] = coordinator
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
@@ -585,7 +636,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
class JackerySensor(SensorEntity):
|
class JackerySensor(SensorEntity):
|
||||||
"""Jackery Sensor."""
|
"""Jackery Sensor."""
|
||||||
|
# ... (Existing JackerySensor Code) ...
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
sensor_id: str,
|
sensor_id: str,
|
||||||
@@ -672,3 +723,79 @@ class JackerySensor(SensorEntity):
|
|||||||
"device_sn": self._coordinator._device_sn,
|
"device_sn": self._coordinator._device_sn,
|
||||||
"raw_key": self._config.get("json_key")
|
"raw_key": self._config.get("json_key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class JackeryPlugSensor(SensorEntity):
|
||||||
|
"""Jackery Smart Plug Sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
plug_sn: str,
|
||||||
|
coordinator: JackeryDataCoordinator,
|
||||||
|
config_entry_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
self._plug_sn = plug_sn
|
||||||
|
self._coordinator = coordinator
|
||||||
|
|
||||||
|
self._attr_name = f"Plug {plug_sn} Power"
|
||||||
|
self._attr_native_unit_of_measurement = UnitOfPower.WATT
|
||||||
|
self._attr_icon = "mdi:power-socket-eu"
|
||||||
|
self._attr_device_class = SensorDeviceClass.POWER
|
||||||
|
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||||
|
self._attr_unique_id = f"jackery_plug_{plug_sn}_power"
|
||||||
|
self._attr_has_entity_name = True
|
||||||
|
|
||||||
|
self._attr_device_info = {
|
||||||
|
"identifiers": {(DOMAIN, f"plug_{plug_sn}")}, # Unique identifier for this plug device? Or attach to main?
|
||||||
|
# Ideally attach to main device if it's a sub-device, but having separate device in HA is also fine.
|
||||||
|
# Let's attach to the main Jackery device for simplicity.
|
||||||
|
"identifiers": {(DOMAIN, config_entry_id)},
|
||||||
|
"name": "Jackery",
|
||||||
|
"manufacturer": "Jackery",
|
||||||
|
"model": "Energy Monitor",
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
# Register with coordinator using a unique ID format
|
||||||
|
self._coordinator.register_sensor(f"plug_{self._plug_sn}", self)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
self._coordinator.unregister_sensor(f"plug_{self._plug_sn}")
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
|
|
||||||
|
def _update_from_coordinator(self, data: dict) -> None:
|
||||||
|
"""Receive data from coordinator."""
|
||||||
|
plugs = data.get("plugs")
|
||||||
|
if not plugs or not isinstance(plugs, list):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find my plug data
|
||||||
|
my_plug = next((p for p in plugs if p.get("sn") == self._plug_sn), None)
|
||||||
|
if not my_plug:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update state (outPw)
|
||||||
|
try:
|
||||||
|
self._attr_native_value = float(my_plug.get("outPw", 0))
|
||||||
|
|
||||||
|
# Update name if available and changed?
|
||||||
|
# name = my_plug.get("name")
|
||||||
|
# if name and name != self._attr_name:
|
||||||
|
# self._attr_name = name # Changing name dynamically might be tricky for entity registry.
|
||||||
|
|
||||||
|
self._attr_available = True
|
||||||
|
self.async_write_ha_state()
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"plug_sn": self._plug_sn
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user