Updated apps
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user