Skip to content
This repository was archived by the owner on Aug 19, 2024. It is now read-only.

Commit 402d1bf

Browse files
committed
Fix watchman doing blocking I/O in the event loop
1 parent 45aad58 commit 402d1bf

File tree

2 files changed

+48
-34
lines changed

2 files changed

+48
-34
lines changed

custom_components/watchman/__init__.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""https://github.com/andreasbrett/thewatchman§"""
2+
23
from datetime import timedelta
34
import logging
45
import os
@@ -133,18 +134,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
133134
await add_event_handlers(hass)
134135
if hass.is_running:
135136
# integration reloaded or options changed via UI
136-
parse_config(hass, reason="changes in watchman configuration")
137+
await parse_config(hass, reason="changes in watchman configuration")
137138
await coordinator.async_config_entry_first_refresh()
138139
else:
139140
# first run, home assistant is loading
140141
# parse_config will be scheduled once HA is fully loaded
141142
_LOGGER.info("Watchman started [%s]", VERSION)
142143

143-
144-
# resources = hass.data["lovelace"]["resources"]
145-
# await resources.async_get_info()
146-
# for itm in resources.async_items():
147-
# _LOGGER.debug(itm)
144+
# resources = hass.data["lovelace"]["resources"]
145+
# await resources.async_get_info()
146+
# for itm in resources.async_items():
147+
# _LOGGER.debug(itm)
148148

149149
return True
150150

@@ -210,7 +210,7 @@ async def async_handle_report(call):
210210
await async_notification(hass, "Watchman error", message, error=True)
211211

212212
if call.data.get(CONF_PARSE_CONFIG, False):
213-
parse_config(hass, reason="service call")
213+
await parse_config(hass, reason="service call")
214214

215215
if send_notification:
216216
chunk_size = call.data.get(CONF_CHUNK_SIZE, config.get(CONF_CHUNK_SIZE))
@@ -270,7 +270,7 @@ async def async_delayed_refresh_states(timedate): # pylint: disable=unused-argu
270270
await coordinator.async_refresh()
271271

272272
async def async_on_home_assistant_started(event): # pylint: disable=unused-argument
273-
parse_config(hass, reason="HA restart")
273+
await parse_config(hass, reason="HA restart")
274274
startup_delay = get_config(hass, CONF_STARTUP_DELAY, 0)
275275
await async_schedule_refresh_states(hass, startup_delay)
276276

@@ -283,12 +283,12 @@ async def async_on_configuration_changed(event):
283283
"reload_core_config",
284284
"reload",
285285
]:
286-
parse_config(hass, reason="configuration changes")
286+
await parse_config(hass, reason="configuration changes")
287287
coordinator = hass.data[DOMAIN]["coordinator"]
288288
await coordinator.async_refresh()
289289

290290
elif typ in [EVENT_AUTOMATION_RELOADED, EVENT_SCENE_RELOADED]:
291-
parse_config(hass, reason="configuration changes")
291+
await parse_config(hass, reason="configuration changes")
292292
coordinator = hass.data[DOMAIN]["coordinator"]
293293
await coordinator.async_refresh()
294294

@@ -340,14 +340,14 @@ def state_or_missing(state_id):
340340
hass.data[DOMAIN]["cancel_handlers"] = hdlr
341341

342342

343-
def parse_config(hass: HomeAssistant, reason=None):
343+
async def parse_config(hass: HomeAssistant, reason=None):
344344
"""parse home assistant configuration files"""
345345
assert hass.data.get(DOMAIN_DATA)
346346
start_time = time.time()
347347
included_folders = get_included_folders(hass)
348348
ignored_files = hass.data[DOMAIN_DATA].get(CONF_IGNORED_FILES, None)
349349

350-
entity_list, service_list, files_parsed, files_ignored = parse(
350+
entity_list, service_list, files_parsed, files_ignored = await parse(
351351
hass, included_folders, ignored_files, hass.config.config_dir
352352
)
353353
hass.data[DOMAIN]["entity_list"] = entity_list
@@ -387,11 +387,16 @@ async def async_report_to_file(hass, path, test_mode):
387387
"""save report to a file"""
388388
coordinator = hass.data[DOMAIN]["coordinator"]
389389
await coordinator.async_refresh()
390-
report_chunks = report(hass, table_renderer, chunk_size=0, test_mode=test_mode)
391-
# OSError exception is handled in async_handle_report
392-
with open(path, "w", encoding="utf-8") as report_file:
393-
for chunk in report_chunks:
394-
report_file.write(chunk)
390+
report_chunks = await report(
391+
hass, table_renderer, chunk_size=0, test_mode=test_mode
392+
)
393+
394+
def write(path):
395+
with open(path, "w", encoding="utf-8") as report_file:
396+
for chunk in report_chunks:
397+
report_file.write(chunk)
398+
399+
await hass.async_add_executor_job(write, path)
395400

396401

397402
async def async_report_to_notification(hass, service_str, service_data, chunk_size):
@@ -422,7 +427,7 @@ async def async_report_to_notification(hass, service_str, service_data, chunk_si
422427

423428
coordinator = hass.data[DOMAIN]["coordinator"]
424429
await coordinator.async_refresh()
425-
report_chunks = report(hass, text_renderer, chunk_size)
430+
report_chunks = await report(hass, text_renderer, chunk_size)
426431
for chunk in report_chunks:
427432
data["message"] = chunk
428433
# blocking=True ensures execution order

custom_components/watchman/utils.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""Miscellaneous support functions for watchman"""
2+
3+
import aiofiles
24
import glob
35
import re
46
import fnmatch
@@ -231,7 +233,7 @@ def check_entitites(hass):
231233
return entities_missing
232234

233235

234-
def parse(hass, folders, ignored_files, root=None):
236+
async def parse(hass, folders, ignored_files, root=None):
235237
"""Parse a yaml or json file for entities/services"""
236238
files_parsed = 0
237239
entity_pattern = re.compile(
@@ -255,19 +257,22 @@ def parse(hass, folders, ignored_files, root=None):
255257
continue
256258

257259
try:
258-
for i, line in enumerate(open(yaml_file, encoding="utf-8")):
259-
line = re.sub(comment_pattern, "", line)
260-
for match in re.finditer(entity_pattern, line):
261-
typ, val = match.group(1), match.group(2)
262-
if (
263-
typ != "service:"
264-
and "*" not in val
265-
and not val.endswith(".yaml")
266-
):
267-
add_entry(entity_list, val, short_path, i + 1)
268-
for match in re.finditer(service_pattern, line):
269-
val = match.group(1)
270-
add_entry(service_list, val, short_path, i + 1)
260+
lineno = 1
261+
async with aiofiles.open(yaml_file, mode="r", encoding="utf-8") as f:
262+
async for line in f:
263+
line = re.sub(comment_pattern, "", line)
264+
for match in re.finditer(entity_pattern, line):
265+
typ, val = match.group(1), match.group(2)
266+
if (
267+
typ != "service:"
268+
and "*" not in val
269+
and not val.endswith(".yaml")
270+
):
271+
add_entry(entity_list, val, short_path, lineno)
272+
for match in re.finditer(service_pattern, line):
273+
val = match.group(1)
274+
add_entry(service_list, val, short_path, lineno)
275+
lineno += 1
271276
files_parsed += 1
272277
_LOGGER.debug("%s parsed", yaml_file)
273278
except OSError as exception:
@@ -312,7 +317,7 @@ def fill(data, width, extra=None):
312317
)
313318

314319

315-
def report(hass, render, chunk_size, test_mode=False):
320+
async def report(hass, render, chunk_size, test_mode=False):
316321
"""generates watchman report either as a table or as a list"""
317322
if not DOMAIN in hass.data:
318323
raise HomeAssistantError("No data for report, refresh required.")
@@ -354,7 +359,11 @@ def report(hass, render, chunk_size, test_mode=False):
354359
rep += "your config are available!\n"
355360
else:
356361
rep += "\n-== No entities found in configuration files!\n"
357-
timezone = pytz.timezone(hass.config.time_zone)
362+
363+
def get_timezone(hass):
364+
return pytz.timezone(hass.config.time_zone)
365+
366+
timezone = await hass.async_add_executor_job(get_timezone, hass)
358367

359368
if not test_mode:
360369
report_datetime = datetime.now(timezone).strftime("%d %b %Y %H:%M:%S")

0 commit comments

Comments
 (0)