160 lines
6.0 KiB
Python
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}
|