Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ The following devices have been tested and confirmed as working. If your device

- **Plugs**: P100, P105, P110, P110M, P115, P125M, P135, TP10, TP15
- **Power Strips**: P210M, P300, P304M, P306, P316M, TP25
- **Wall Switches**: S210, S220, S500, S500D, S505, S505D, TS15
- **Wall Switches**: S210, S220, S500, S500D, S505, S505D, S515D, TS15
- **Bulbs**: L430C, L430P, L510B, L510E, L530B, L530E, L535E, L630
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
- **Cameras**: C100, C101, C110, C210, C220, C225, C325WB, C460, C520WS, C720, TC40, TC65, TC70
Expand Down
2 changes: 2 additions & 0 deletions SUPPORTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
- Hardware: 1.0 (US) / Firmware: 1.0.2
- **S505D**
- Hardware: 1.0 (US) / Firmware: 1.1.0
- **S515D**
- Hardware: 1.6 (US) / Firmware: 1.0.4
- **TS15**
- Hardware: 1.0 (US) / Firmware: 1.2.2

Expand Down
90 changes: 68 additions & 22 deletions kasa/smart/modules/energy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ class Energy(SmartModule, EnergyInterface):
_energy: dict[str, Any]
_current_consumption: float | None

def _get_current_power_mw(
self, data: dict[str, Any], energy: dict[str, Any] | None = None
) -> float | None:
"""Return the best available current power reading in milliwatts."""
energy = self._energy if energy is None else energy

if (power := data.get("get_emeter_data", {}).get("power_mw")) is not None:
return power

# Prefer the higher precision milliwatt readings from the energy usage
# payload. get_current_power is only a lower precision fallback used by
# devices such as P304M whose get_energy_usage omits current_power.
if (power := energy.get("current_power")) is not None:
return power

if (
power := data.get("get_current_power", {}).get("current_power")
) is not None:
return power * 1_000

return None

async def _post_update_hook(self) -> None:
try:
data = self.data
Expand All @@ -34,17 +56,8 @@ async def _post_update_hook(self) -> None:
self._supported | EnergyInterface.ModuleFeature.VOLTAGE_CURRENT
)

if (power := self._energy.get("current_power")) is not None or (
power := data.get("get_emeter_data", {}).get("power_mw")
) is not None:
if (power := self._get_current_power_mw(data)) is not None:
self._current_consumption = power / 1_000
# Fallback if get_energy_usage does not provide current_power,
# which can happen on some newer devices (e.g. P304M).
# This may not be valid scenario as it pre-dates trying get_emeter_data
Comment thread
ZeliardM marked this conversation as resolved.
elif (
power := self.data.get("get_current_power", {}).get("current_power")
) is not None:
self._current_consumption = power
else:
self._current_consumption = None

Expand All @@ -63,7 +76,7 @@ def query(self) -> dict:
def optional_response_keys(self) -> list[str]:
"""Return optional response keys for the module."""
if self.supported_version > 1:
return ["get_energy_usage"]
return ["get_energy_usage", "get_current_power"]
return []

@property
Expand All @@ -76,10 +89,14 @@ def energy(self) -> dict:
"""Return get_energy_usage results."""
return self._energy

def _get_status_from_energy(self, energy: dict) -> EmeterStatus:
def _get_status_from_energy(
self, energy: dict[str, Any], power_mw: float | None = None
) -> EmeterStatus:
return EmeterStatus(
{
"power_mw": energy.get("current_power", 0),
"power_mw": (
power_mw if power_mw is not None else energy.get("current_power")
),
"total": energy.get("today_energy", 0) / 1_000,
}
)
Expand All @@ -88,19 +105,48 @@ def _get_status_from_energy(self, energy: dict) -> EmeterStatus:
@raise_if_update_error
def status(self) -> EmeterStatus:
"""Get the emeter status."""
if "get_emeter_data" in self.data:
return EmeterStatus(self.data["get_emeter_data"])
else:
return self._get_status_from_energy(self.energy)
data = self.data
if "get_emeter_data" in data:
return EmeterStatus(data["get_emeter_data"])

return self._get_status_from_energy(
self.energy, self._get_current_power_mw(data)
)

async def get_status(self) -> EmeterStatus:
"""Return real-time statistics."""
if "get_emeter_data" in self.data:
res = await self.call("get_emeter_data")
return EmeterStatus(res["get_emeter_data"])
else:
if self.supported_version > 1:
try:
res = await self.call("get_emeter_data")
except DeviceError:
pass
else:
return EmeterStatus(res["get_emeter_data"])

energy: dict[str, Any] = {}
try:
res = await self.call("get_energy_usage")
return self._get_status_from_energy(res["get_energy_usage"])
except DeviceError:
if self.supported_version <= 1:
raise
else:
energy = res["get_energy_usage"]
if energy.get("current_power") is not None:
return self._get_status_from_energy(energy)

current_power: dict[str, Any] = {}
if self.supported_version > 1:
try:
res = await self.call("get_current_power")
except DeviceError:
pass
else:
current_power = res["get_current_power"]

return self._get_status_from_energy(
energy,
self._get_current_power_mw({"get_current_power": current_power}, energy),
)

@property
def consumption_this_month(self) -> float | None:
Expand Down
5 changes: 3 additions & 2 deletions tests/device_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"S500D",
"S505",
"S505D",
"S515D",
"TS15",
}
SWITCHES = {*SWITCHES_IOT, *SWITCHES_SMART}
Expand All @@ -119,7 +120,7 @@
STRIPS = {*STRIPS_IOT, *STRIPS_SMART}

DIMMERS_IOT = {"ES20M", "HS220", "KS220", "KS220M", "KS230", "KP405"}
DIMMERS_SMART = {"HS220", "KS225", "S500D", "P135"}
DIMMERS_SMART = {"HS220", "KS225", "S500D", "S505D", "S515D", "P135"}
DIMMERS = {
*DIMMERS_IOT,
*DIMMERS_SMART,
Expand All @@ -143,7 +144,7 @@
VACUUMS_SMART = {"RV20"}

WITH_EMETER_IOT = {"EP25", "HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
WITH_EMETER_SMART = {"P110", "P110M", "P115", "KP125M", "EP25", "P304M"}
WITH_EMETER_SMART = {"P110", "P110M", "P115", "KP125M", "EP25", "P304M", "S515D"}
WITH_EMETER = {*WITH_EMETER_IOT, *WITH_EMETER_SMART}

DIMMABLE = {*BULBS, *DIMMERS}
Expand Down
Loading
Loading