Skip to content

Commit eb26a21

Browse files
authored
Adjust remote ESPHome log subscription level on logging change (#139308)
1 parent 4530fe4 commit eb26a21

File tree

3 files changed

+79
-11
lines changed

3 files changed

+79
-11
lines changed

homeassistant/components/esphome/manager.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
Platform,
3636
)
3737
from homeassistant.core import (
38+
CALLBACK_TYPE,
3839
Event,
3940
EventStateChangedData,
4041
HomeAssistant,
@@ -95,6 +96,14 @@
9596
LogLevel.LOG_LEVEL_VERBOSE: logging.DEBUG,
9697
LogLevel.LOG_LEVEL_VERY_VERBOSE: logging.DEBUG,
9798
}
99+
LOGGER_TO_LOG_LEVEL = {
100+
logging.NOTSET: LogLevel.LOG_LEVEL_VERY_VERBOSE,
101+
logging.DEBUG: LogLevel.LOG_LEVEL_VERY_VERBOSE,
102+
logging.INFO: LogLevel.LOG_LEVEL_CONFIG,
103+
logging.WARNING: LogLevel.LOG_LEVEL_WARN,
104+
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
105+
logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR,
106+
}
98107
# 7-bit and 8-bit C1 ANSI sequences
99108
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
100109
ANSI_ESCAPE_78BIT = re.compile(
@@ -161,6 +170,8 @@ class ESPHomeManager:
161170
"""Class to manage an ESPHome connection."""
162171

163172
__slots__ = (
173+
"_cancel_subscribe_logs",
174+
"_log_level",
164175
"cli",
165176
"device_id",
166177
"domain_data",
@@ -194,6 +205,8 @@ def __init__(
194205
self.reconnect_logic: ReconnectLogic | None = None
195206
self.zeroconf_instance = zeroconf_instance
196207
self.entry_data = entry.runtime_data
208+
self._cancel_subscribe_logs: CALLBACK_TYPE | None = None
209+
self._log_level = LogLevel.LOG_LEVEL_NONE
197210

198211
async def on_stop(self, event: Event) -> None:
199212
"""Cleanup the socket client on HA close."""
@@ -368,15 +381,31 @@ async def on_connect(self) -> None:
368381

369382
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
370383
"""Handle a log message from the API."""
371-
logger_level = LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG)
372-
if _LOGGER.isEnabledFor(logger_level):
373-
log: bytes = msg.message
374-
_LOGGER.log(
375-
logger_level,
376-
"%s: %s",
377-
self.entry.title,
378-
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
379-
)
384+
log: bytes = msg.message
385+
_LOGGER.log(
386+
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
387+
"%s: %s",
388+
self.entry.title,
389+
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
390+
)
391+
392+
@callback
393+
def _async_get_equivalent_log_level(self) -> LogLevel:
394+
"""Get the equivalent ESPHome log level for the current logger."""
395+
return LOGGER_TO_LOG_LEVEL.get(
396+
_LOGGER.getEffectiveLevel(), LogLevel.LOG_LEVEL_VERY_VERBOSE
397+
)
398+
399+
@callback
400+
def _async_subscribe_logs(self, log_level: LogLevel) -> None:
401+
"""Subscribe to logs."""
402+
if self._cancel_subscribe_logs is not None:
403+
self._cancel_subscribe_logs()
404+
self._cancel_subscribe_logs = None
405+
self._log_level = log_level
406+
self._cancel_subscribe_logs = self.cli.subscribe_logs(
407+
self._async_on_log, self._log_level
408+
)
380409

381410
async def _on_connnect(self) -> None:
382411
"""Subscribe to states and list entities on successful API login."""
@@ -390,7 +419,7 @@ async def _on_connnect(self) -> None:
390419
stored_device_name = entry.data.get(CONF_DEVICE_NAME)
391420
unique_id_is_mac_address = unique_id and ":" in unique_id
392421
if entry.options.get(CONF_SUBSCRIBE_LOGS):
393-
cli.subscribe_logs(self._async_on_log, LogLevel.LOG_LEVEL_VERY_VERBOSE)
422+
self._async_subscribe_logs(self._async_get_equivalent_log_level())
394423
results = await asyncio.gather(
395424
create_eager_task(cli.device_info()),
396425
create_eager_task(cli.list_entities_services()),
@@ -542,6 +571,10 @@ async def on_connect_error(self, err: Exception) -> None:
542571
def _async_handle_logging_changed(self, _event: Event) -> None:
543572
"""Handle when the logging level changes."""
544573
self.cli.set_debug(_LOGGER.isEnabledFor(logging.DEBUG))
574+
if self.entry.options.get(CONF_SUBSCRIBE_LOGS) and self._log_level != (
575+
new_log_level := self._async_get_equivalent_log_level()
576+
):
577+
self._async_subscribe_logs(new_log_level)
545578

546579
async def async_start(self) -> None:
547580
"""Start the esphome connection manager."""

tests/components/esphome/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ def __init__(
230230
)
231231
self.on_log_message: Callable[[SubscribeLogsResponse], None]
232232
self.device_info = device_info
233+
self.current_log_level = LogLevel.LOG_LEVEL_NONE
233234

234235
def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None:
235236
"""Set the state callback."""
@@ -432,9 +433,11 @@ def _subscribe_home_assistant_states(
432433

433434
def _subscribe_logs(
434435
on_log_message: Callable[[SubscribeLogsResponse], None], log_level: LogLevel
435-
) -> None:
436+
) -> Callable[[], None]:
436437
"""Subscribe to log messages."""
437438
mock_device.set_on_log_message(on_log_message)
439+
mock_device.current_log_level = log_level
440+
return lambda: None
438441

439442
def _subscribe_voice_assistant(
440443
*,

tests/components/esphome/test_manager.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async def test_esphome_device_subscribe_logs(
5757
caplog: pytest.LogCaptureFixture,
5858
) -> None:
5959
"""Test configuring a device to subscribe to logs."""
60+
assert await async_setup_component(hass, "logger", {"logger": {}})
6061
entry = MockConfigEntry(
6162
domain=DOMAIN,
6263
data={
@@ -76,6 +77,15 @@ async def test_esphome_device_subscribe_logs(
7677
states=[],
7778
)
7879
await hass.async_block_till_done()
80+
81+
await hass.services.async_call(
82+
"logger",
83+
"set_level",
84+
{"homeassistant.components.esphome": "DEBUG"},
85+
blocking=True,
86+
)
87+
assert device.current_log_level == LogLevel.LOG_LEVEL_VERY_VERBOSE
88+
7989
caplog.set_level(logging.DEBUG)
8090
device.mock_on_log_message(
8191
Mock(level=LogLevel.LOG_LEVEL_INFO, message=b"test_log_message")
@@ -103,6 +113,28 @@ async def test_esphome_device_subscribe_logs(
103113
await hass.async_block_till_done()
104114
assert "test_debug_log_message" in caplog.text
105115

116+
await hass.services.async_call(
117+
"logger",
118+
"set_level",
119+
{"homeassistant.components.esphome": "WARNING"},
120+
blocking=True,
121+
)
122+
assert device.current_log_level == LogLevel.LOG_LEVEL_WARN
123+
await hass.services.async_call(
124+
"logger",
125+
"set_level",
126+
{"homeassistant.components.esphome": "ERROR"},
127+
blocking=True,
128+
)
129+
assert device.current_log_level == LogLevel.LOG_LEVEL_ERROR
130+
await hass.services.async_call(
131+
"logger",
132+
"set_level",
133+
{"homeassistant.components.esphome": "INFO"},
134+
blocking=True,
135+
)
136+
assert device.current_log_level == LogLevel.LOG_LEVEL_CONFIG
137+
106138

107139
async def test_esphome_device_service_calls_not_allowed(
108140
hass: HomeAssistant,

0 commit comments

Comments
 (0)