Files
Home-Assistant/custom_components/watchman/hub.py
T
2026-07-20 22:52:35 -04:00

160 lines
6.0 KiB
Python

"""Watchman Hub - Asynchronous wrapper for WatchmanParser."""
from collections.abc import Callable
import fnmatch
import sqlite3
from typing import Any
from homeassistant.core import HomeAssistant
from .const import BUNDLED_IGNORED_ITEMS, CONF_IGNORED_ITEMS
from .utils.logger import _LOGGER
from .utils.parser_core import ParseResult, WatchmanParser, get_domains
from .utils.utils import get_config
class WatchmanHub:
"""Asynchronous wrapper (Adapter) for the synchronous parser."""
def __init__(self, hass: HomeAssistant, db_path: str) -> None:
self.hass = hass
self.db_path = db_path
self._is_scanning = False
# inject Home Assistant's executor to run process config files asynchronously
async def ha_executor(func: Callable, *args: Any) -> Any:
return await self.hass.async_add_executor_job(func, *args)
self._parser = WatchmanParser(db_path, executor=ha_executor)
self.cached_items = {}
self._monitored_entities = None
self._monitored_services = None
@property
def is_scanning(self) -> bool:
"""Return True if a scan is currently in progress."""
return self._is_scanning
def is_monitored_service(self, service_id: str) -> bool:
"""Check if service is monitored (fast cache check)."""
if self._monitored_services is None:
return False
return service_id in self._monitored_services
async def async_get_all_items(self) -> dict[str, dict[str, Any]]:
"""Return all parsed items (entities and services) in one pass."""
return await self.hass.async_add_executor_job(self._get_all_items_sync)
def _get_all_items_sync(self) -> dict[str, dict[str, Any]]:
"""Fetch ALL items in one go and split them in memory."""
ignored_items = get_config(self.hass, CONF_IGNORED_ITEMS, [])
final_ignored_items = list(set((ignored_items or []) + BUNDLED_IGNORED_ITEMS))
entities = {}
services = {}
try:
raw_items = self._parser.get_found_items(item_type='all')
except sqlite3.OperationalError as e:
_LOGGER.warning(f"Database busy during read, returning cached/empty data: {e}")
# Try to return cached data if available, else empty
# Note: cached_items is now structured differently?
# For simplicity in this refactor, if DB fails, return empty structure.
# Ideally we should cache the full 'entities'/'services' structure.
return {"entities": {}, "services": {}}
for item in raw_items:
entity_id, path, line, item_type = item[0], item[1], item[2], item[3]
# Filter ignored items
is_ignored = False
for pattern in final_ignored_items:
if fnmatch.fnmatch(entity_id, pattern):
is_ignored = True
break
if is_ignored:
continue
target_dict = entities if item_type == 'entity' else services
if entity_id not in target_dict:
target_dict[entity_id] = {"locations": {}, "automations": set(), "occurrences": []}
if path not in target_dict[entity_id]["locations"]:
target_dict[entity_id]["locations"][path] = []
target_dict[entity_id]["locations"][path].append(line)
parent_type = item[4]
parent_alias = item[5]
parent_id = item[6]
context = None
if parent_type or parent_alias or parent_id:
context = {
"parent_type": parent_type,
"parent_alias": parent_alias,
"parent_id": parent_id
}
target_dict[entity_id]["occurrences"].append({
"path": path,
"line": line,
"context": context
})
if parent_id and parent_type in ("automation", "script"):
target_dict[entity_id]["automations"].add(parent_id)
# Update fast lookups
self._monitored_entities = set(entities.keys())
self._monitored_services = set(services.keys())
return {"entities": entities, "services": services}
async def async_parse(
self, ignored_files: list[str], *, force: bool = False
) -> ParseResult | None:
"""Asynchronous wrapper for the parse method."""
if self._is_scanning:
_LOGGER.debug("Scan already in progress, skipping request.")
return None
self._is_scanning = True
try:
custom_domains = get_domains(self.hass)
(
_entities,
_services,
_files_parsed,
_files_ignored,
_ent_to_auto,
parse_result,
) = await self._parser.async_parse(
self.hass.config.config_dir,
ignored_files,
force=force,
custom_domains=custom_domains,
base_path=self.hass.config.config_dir,
)
self.cached_items = {}
# Reset fast cache so it's rebuilt on next access
self._monitored_entities = None
self._monitored_services = None
return parse_result
finally:
self._is_scanning = False
async def async_get_last_parse_info(self) -> dict[str, Any]:
"""Return the duration and timestamp of the last successful scan."""
return await self.hass.async_add_executor_job(self._get_last_parse_info_sync)
def _get_last_parse_info_sync(self) -> dict[str, Any]:
"""Return the duration and timestamp of the last successful scan."""
try:
return self._parser.get_last_parse_info()
except sqlite3.OperationalError as e:
_LOGGER.warning(f"Database busy (get_last_parse_info), returning default: {e}")
return {"duration": 0.0, "timestamp": None, "ignored_files_count": 0, "processed_files_count": 0}