Skip to content

Commit 4a11c08

Browse files
committed
Improve logging and add repairs and diagnostics
1 parent b278e11 commit 4a11c08

8 files changed

Lines changed: 335 additions & 25 deletions

File tree

custom_components/power_load_balancer/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ async def _handle_turn_off_appliance_service(call: ServiceCall) -> None:
203203
entity_id = call.data[ATTR_ENTITY_ID]
204204
reason = call.data.get(ATTR_REASON, "")
205205

206-
logger.info(
206+
logger.debug(
207207
"Turn off appliance service called",
208208
entity_id=entity_id,
209209
reason=reason,
@@ -234,7 +234,7 @@ async def _handle_turn_on_appliance_service(call: ServiceCall) -> None:
234234
entity_id = call.data[ATTR_ENTITY_ID]
235235
reason = call.data.get(ATTR_REASON, "")
236236

237-
logger.info(
237+
logger.debug(
238238
"Turn on appliance service called",
239239
entity_id=entity_id,
240240
reason=reason,

custom_components/power_load_balancer/appliance_controller.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ async def auto_turn_on_task(
465465
)
466466
await asyncio.sleep(delay_seconds)
467467

468-
logger.info(
468+
logger.debug(
469469
"Auto turn-on timer expired", entity_id=entity_to_restore
470470
)
471471

@@ -493,7 +493,7 @@ async def auto_turn_on_task(
493493
)
494494

495495
if available_budget < expected_power_value:
496-
logger.warning(
496+
logger.debug(
497497
"Cannot auto turn-on: Insufficient power headroom",
498498
entity_id=entity_to_restore,
499499
available_budget=available_budget,
@@ -560,7 +560,7 @@ async def turn_off_appliance(self, entity_id: str, reason: str = "") -> None:
560560
appliance_state = validate_entity_state(self.hass, entity_id)
561561

562562
if not self._is_appliance_active(entity_id, appliance_state.state):
563-
logger.warning(
563+
logger.debug(
564564
"Appliance is not in an active state",
565565
entity_id=entity_id,
566566
current_state=appliance_state.state,
@@ -692,7 +692,7 @@ async def turn_off_appliance_service(
692692
)
693693

694694
try:
695-
logger.info(
695+
logger.debug(
696696
"Service turn_off_appliance called",
697697
entity_id=entity_id,
698698
reason=reason,
@@ -703,7 +703,7 @@ async def turn_off_appliance_service(
703703
appliance_state = validate_entity_state(self.hass, entity_id)
704704

705705
if not self._is_appliance_active(entity_id, appliance_state.state):
706-
logger.warning(
706+
logger.debug(
707707
"Appliance is not in an active state",
708708
entity_id=entity_id,
709709
current_state=appliance_state.state,
@@ -851,7 +851,7 @@ async def turn_on_appliance_service(
851851
)
852852

853853
try:
854-
logger.info(
854+
logger.debug(
855855
"Service turn_on_appliance called",
856856
entity_id=entity_id,
857857
reason=reason,
@@ -862,7 +862,7 @@ async def turn_on_appliance_service(
862862
appliance_state = validate_entity_state(self.hass, entity_id)
863863

864864
if not self._is_appliance_off(entity_id, appliance_state.state):
865-
logger.warning(
865+
logger.debug(
866866
"Appliance is not in 'off' state",
867867
entity_id=entity_id,
868868
current_state=appliance_state.state,
@@ -968,3 +968,20 @@ def cleanup(self) -> None:
968968
self._expected_power_restoration.clear()
969969
self._balanced_off_appliances.clear()
970970
self._previous_hvac_modes.clear()
971+
972+
def get_diagnostics_snapshot(self) -> dict[str, Any]:
973+
"""Return runtime diagnostics data for troubleshooting."""
974+
scheduled_tasks = {
975+
entity_id: {
976+
"done": task.done(),
977+
"cancelled": task.cancelled(),
978+
}
979+
for entity_id, task in self._scheduled_auto_turn_ons.items()
980+
}
981+
982+
return {
983+
"balanced_off_appliances": dict(self._balanced_off_appliances),
984+
"expected_power_restoration_watt": dict(self._expected_power_restoration),
985+
"scheduled_auto_turn_on_tasks": scheduled_tasks,
986+
"stored_previous_hvac_modes": dict(self._previous_hvac_modes),
987+
}

custom_components/power_load_balancer/balancing_engine.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def __init__(
6262
self.hass = hass
6363
self._monitored_sensors = monitored_sensors
6464
self._power_budget = power_budget
65+
self._reported_balance_down_failure = False
6566

6667
def _is_climate_entity(self, entity_id: str) -> bool:
6768
"""Check if an entity is a climate entity."""
@@ -207,15 +208,19 @@ def balance_down(
207208
expected_power_reduction,
208209
)
209210

211+
optimistic_reduction = 0.0
210212
if expected_power_reduction > 0:
211-
reduce_estimated_power_callback(expected_power_reduction)
213+
optimistic_reduction = expected_power_reduction
214+
reduce_estimated_power_callback(optimistic_reduction)
212215
_LOGGER.debug(
213216
"Optimistically reduced estimated power by %s W for %s",
214-
expected_power_reduction,
217+
optimistic_reduction,
215218
appliance_entity_id,
216219
)
217220

218-
async def turn_off_task(inner_appliance_id: str) -> None:
221+
async def turn_off_task(
222+
inner_appliance_id: str, reduced_power: float
223+
) -> None:
219224
try:
220225
appliance_state_before = self.hass.states.get(
221226
inner_appliance_id
@@ -236,14 +241,31 @@ async def turn_off_task(inner_appliance_id: str) -> None:
236241
f"{self._power_budget} W",
237242
)
238243
except Exception:
244+
if reduced_power > 0:
245+
reduce_estimated_power_callback(-reduced_power)
246+
_LOGGER.debug(
247+
"Restored estimated power by %s W for %s after "
248+
"turn-off failure",
249+
reduced_power,
250+
inner_appliance_id,
251+
)
239252
_LOGGER.exception(
240253
"Failed to turn off appliance %s", inner_appliance_id
241254
)
242255

243-
self.hass.async_create_task(turn_off_task(appliance_entity_id))
256+
self.hass.async_create_task(
257+
turn_off_task(appliance_entity_id, optimistic_reduction)
258+
)
259+
self._reported_balance_down_failure = False
244260
return
245261

246-
_LOGGER.warning(
247-
"Could not balance power below budget by turning off non-last-resort "
248-
"appliances."
249-
)
262+
if self._reported_balance_down_failure:
263+
_LOGGER.debug(
264+
"Still unable to balance power using non-last-resort appliances"
265+
)
266+
else:
267+
_LOGGER.warning(
268+
"Could not balance power below budget by turning off non-last-resort "
269+
"appliances."
270+
)
271+
self._reported_balance_down_failure = True
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Diagnostics support for the Power Load Balancer integration."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Any
6+
7+
from homeassistant.components.diagnostics import async_redact_data
8+
from homeassistant.const import CONF_ENTITY_ID
9+
10+
from .const import CONF_APPLIANCE, CONF_MAIN_POWER_SENSOR, CONF_POWER_SENSORS, DOMAIN
11+
from .power_balancer import PowerLoadBalancer
12+
13+
if TYPE_CHECKING:
14+
from homeassistant.config_entries import ConfigEntry
15+
from homeassistant.core import HomeAssistant
16+
17+
TO_REDACT = {
18+
CONF_APPLIANCE,
19+
CONF_ENTITY_ID,
20+
CONF_MAIN_POWER_SENSOR,
21+
CONF_POWER_SENSORS,
22+
"context_id",
23+
"entry_id",
24+
"main_power_sensor_entity_id",
25+
"monitored_sensors",
26+
}
27+
28+
29+
async def async_get_config_entry_diagnostics(
30+
hass: HomeAssistant, entry: ConfigEntry
31+
) -> dict[str, Any]:
32+
"""Return diagnostics for a config entry."""
33+
domain_data = hass.data.get(DOMAIN, {})
34+
power_balancer = domain_data.get(entry.entry_id)
35+
36+
runtime_data: dict[str, Any] = {"available": False}
37+
if isinstance(power_balancer, PowerLoadBalancer):
38+
runtime_data = {
39+
"available": True,
40+
"snapshot": power_balancer.get_diagnostics_snapshot(),
41+
}
42+
43+
diagnostics_data: dict[str, Any] = {
44+
"entry": entry.as_dict(),
45+
"runtime": runtime_data,
46+
}
47+
48+
return async_redact_data(diagnostics_data, TO_REDACT)

0 commit comments

Comments
 (0)