Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/hassette/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,11 @@ class Api(Resource):
_api_service: "ApiResource"
"""Internal API service instance."""

@classmethod
def create(cls, hassette: "Hassette", parent: "Resource"):
inst = cls(hassette=hassette, parent=parent)
inst._api_service = inst.hassette._api_service
inst.sync = inst.add_child(ApiSyncFacade, api=inst)
inst.mark_ready(reason="API initialized")
return inst
def __init__(self, hassette: "Hassette", *, parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)
self._api_service = self.hassette._api_service
self.sync = self.add_child(ApiSyncFacade, api=self)
self.mark_ready(reason="API initialized")

@property
def config_log_level(self):
Expand Down
10 changes: 4 additions & 6 deletions src/hassette/api/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,10 @@ class ApiSyncFacade(Resource):

_api: "Api"

@classmethod
def create(cls, hassette: "Hassette", api: "Api"):
inst = cls(hassette, parent=api)
inst._api = api
inst.mark_ready(reason="Synchronous API facade initialized")
return inst
def __init__(self, hassette: "Hassette", *, api: "Api", parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)
self._api = api
self.mark_ready(reason="Synchronous API facade initialized")

def ws_send_and_wait(self, **data: Any) -> Any:
"""Send a WebSocket message and wait for a response."""
Expand Down
25 changes: 10 additions & 15 deletions src/hassette/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,23 +83,18 @@ class App(Generic[AppConfigT], Resource, metaclass=FinalMeta):
index: int
"""Index of this app instance, used for unique naming."""

def __init__(self, *args, app_config: AppConfigT, index: int, **kwargs):
# unlike most classes, this one does take additional init args
# this is because the unique name we use for the logger depends on the app config
def __init__(
self, hassette: "Hassette", *, app_config: AppConfigT, index: int, parent: Resource | None = None
) -> None:
# app_config and index must be set before super().__init__ because
# unique_name (used by the logger) depends on app_config
self.app_config = app_config
self.index = index
super().__init__(*args, **kwargs)

@classmethod
def create(cls, hassette: "Hassette", app_config: AppConfigT, index: int):
inst = cls(hassette=hassette, app_config=app_config, index=index)
inst.app_config = app_config
inst.index = index
inst.api = inst.add_child(Api)
inst.scheduler = inst.add_child(Scheduler)
inst.bus = inst.add_child(Bus, priority=0)
inst.states = inst.add_child(StateManager)
return inst
super().__init__(hassette, parent=parent)
self.api = self.add_child(Api)
self.scheduler = self.add_child(Scheduler)
self.bus = self.add_child(Bus, priority=0)
self.states = self.add_child(StateManager)

@property
def unique_name(self) -> str:
Expand Down
16 changes: 7 additions & 9 deletions src/hassette/bus/bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,13 @@ class Bus(Resource):
priority: int = 0
"""Priority level for event handlers created by this bus."""

@classmethod
def create(cls, hassette: "Hassette", parent: "Resource", priority: int = 0):
inst = cls(hassette=hassette, parent=parent)
inst.bus_service = inst.hassette._bus_service
inst.priority = priority

assert inst.bus_service is not None, "Bus service not initialized"
inst.mark_ready(reason="Bus initialized")
return inst
def __init__(self, hassette: "Hassette", *, priority: int = 0, parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)
self.bus_service = self.hassette._bus_service
self.priority = priority

assert self.bus_service is not None, "Bus service not initialized"
self.mark_ready(reason="Bus initialized")

async def on_shutdown(self) -> None:
"""Cleanup all listeners owned by this bus's owner on shutdown."""
Expand Down
10 changes: 4 additions & 6 deletions src/hassette/core/api_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@ class ApiResource(Resource):
_session: aiohttp.ClientSession | None
"""HTTP client session for making requests."""

@classmethod
def create(cls, hassette: "Hassette"):
inst = cls(hassette, parent=hassette)
inst._stack = AsyncExitStack()
inst._session = None
return inst
def __init__(self, hassette: "Hassette", *, parent: "Resource | None" = None) -> None:
super().__init__(hassette, parent=parent)
self._stack = AsyncExitStack()
self._session = None

async def on_initialize(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion src/hassette/core/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def create_instances(

try:
validated = app_class.app_config_cls.model_validate(config)
app_instance = app_class.create(
app_instance = app_class(
hassette=self.hassette,
app_config=validated,
index=idx,
Expand Down
27 changes: 12 additions & 15 deletions src/hassette/core/app_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,23 @@ class AppHandler(Resource):
bus: Bus
"""Event bus for inter-service communication."""

@classmethod
def create(cls, hassette: "Hassette"):
inst = cls(hassette, parent=hassette)
def __init__(self, hassette: "Hassette", *, parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)

# Initialize components
inst.registry = AppRegistry()
inst.factory = AppFactory(hassette, inst.registry)
inst.change_detector = AppChangeDetector()
inst.set_apps_configs(hassette.config.app_manifests)
inst.lifecycle = AppLifecycleManager(hassette, inst.registry)
self.registry = AppRegistry()
self.factory = AppFactory(hassette, self.registry)
self.change_detector = AppChangeDetector()
self.set_apps_configs(hassette.config.app_manifests)
self.lifecycle = AppLifecycleManager(hassette, self.registry)

inst.registry.logger.setLevel(inst.config_log_level)
inst.factory.logger.setLevel(inst.config_log_level)
inst.lifecycle.logger.setLevel(inst.config_log_level)
inst.change_detector.logger.setLevel(inst.config_log_level)
self.registry.logger.setLevel(self.config_log_level)
self.factory.logger.setLevel(self.config_log_level)
self.lifecycle.logger.setLevel(self.config_log_level)
self.change_detector.logger.setLevel(self.config_log_level)

# Event bus for status events
inst.bus = inst.add_child(Bus)

return inst
self.bus = self.add_child(Bus)

# --- Public API ---

Expand Down
25 changes: 14 additions & 11 deletions src/hassette/core/bus_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from hassette.bus.metrics import ListenerMetrics
from hassette.events import Event, HassPayload
from hassette.exceptions import DependencyError, HassetteError
from hassette.resources.base import Service
from hassette.resources.base import Resource, Service
from hassette.utils.glob_utils import GLOB_CHARS, matches_globs, split_exact_and_glob
from hassette.utils.hass_utils import split_entity_id, valid_entity_id

Expand Down Expand Up @@ -49,16 +49,19 @@ class BusService(Service):
Metrics persist after listener removal (~200 bytes each) to preserve
historical data for the web UI. This is intentional and not a leak."""

@classmethod
def create(cls, hassette: "Hassette", stream: "MemoryObjectReceiveStream[tuple[str, Event[Any]]]"):
inst = cls(hassette, parent=hassette)
inst.stream = stream
inst.listener_seq = itertools.count(1)
inst.router = Router()
inst._listener_metrics = {}
inst._setup_exclusion_filters()

return inst
def __init__(
self,
hassette: "Hassette",
*,
stream: "MemoryObjectReceiveStream[tuple[str, Event[Any]]]",
parent: "Resource | None" = None,
) -> None:
super().__init__(hassette, parent=parent)
self.stream = stream
self.listener_seq = itertools.count(1)
self.router = Router()
self._listener_metrics = {}
self._setup_exclusion_filters()

@property
def config_log_level(self):
Expand Down
2 changes: 1 addition & 1 deletion src/hassette/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(self, config: HassetteConfig) -> None:
self.unique_id = ""
enable_logging(self.config.log_level, log_buffer_size=self.config.web_api_log_buffer_size)

super().__init__(self, task_bucket=TaskBucket.create(self, self), parent=self)
super().__init__(self, task_bucket=TaskBucket(self, parent=self), parent=self)
self.logger.info("Starting Hassette...", stacklevel=2)

# set context variables
Expand Down
18 changes: 8 additions & 10 deletions src/hassette/core/data_sync_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,14 @@ class DataSyncService(Resource):
_start_time: float
_subscriptions: "list[Subscription]"

@classmethod
def create(cls, hassette: "Hassette", parent: Resource):
inst = cls(hassette=hassette, parent=parent)
inst.bus = inst.add_child(Bus)
inst._event_buffer = deque(maxlen=hassette.config.web_api_event_buffer_size)
inst._ws_clients = set()
inst._lock = asyncio.Lock()
inst._start_time = time.time()
inst._subscriptions = []
return inst
def __init__(self, hassette: "Hassette", *, parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)
self.bus = self.add_child(Bus)
self._event_buffer = deque(maxlen=hassette.config.web_api_event_buffer_size)
self._ws_clients: set[asyncio.Queue] = set()
self._lock = asyncio.Lock()
self._start_time = time.time()
self._subscriptions = []

@property
def config_log_level(self):
Expand Down
17 changes: 8 additions & 9 deletions src/hassette/core/database_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

if typing.TYPE_CHECKING:
from hassette import Hassette
from hassette.resources.base import Resource

# Heartbeat interval: 5 minutes
_HEARTBEAT_INTERVAL_SECONDS = 300
Expand Down Expand Up @@ -50,15 +51,13 @@ class DatabaseService(Service):
_session_error: bool
"""Whether a service crash has been recorded for this session."""

@classmethod
def create(cls, hassette: "Hassette") -> "DatabaseService":
inst = cls(hassette, parent=hassette)
inst._db = None
inst._session_id = None
inst._db_path = Path()
inst._consecutive_heartbeat_failures = 0
inst._session_error = False
return inst
def __init__(self, hassette: "Hassette", *, parent: "Resource | None" = None) -> None:
super().__init__(hassette, parent=parent)
self._db = None
self._session_id = None
self._db_path = Path()
self._consecutive_heartbeat_failures = 0
self._session_error = False

@property
def config_log_level(self) -> str:
Expand Down
29 changes: 11 additions & 18 deletions src/hassette/core/scheduler_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,12 @@ class SchedulerService(Service):
_execution_log: "deque[JobExecutionRecord]"
"""Ring buffer of recent job execution records."""

@classmethod
def create(cls, hassette: "Hassette"):
inst = cls(hassette, parent=hassette)
inst._job_queue = inst.add_child(_ScheduledJobQueue)
inst._wakeup_event = asyncio.Event()
inst._exit_event = asyncio.Event()
inst._execution_log = deque(maxlen=hassette.config.web_api_job_history_size)

return inst
def __init__(self, hassette: "Hassette", *, parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)
self._job_queue = self.add_child(_ScheduledJobQueue)
self._wakeup_event = asyncio.Event()
self._exit_event = asyncio.Event()
self._execution_log = deque(maxlen=hassette.config.web_api_job_history_size)

@property
def min_delay(self) -> float:
Expand Down Expand Up @@ -302,15 +299,11 @@ class _ScheduledJobQueue(Resource):
_queue: "HeapQueue[ScheduledJob]"
"""The heap queue of scheduled jobs."""

@classmethod
def create(cls, hassette: "Hassette", parent: Resource):
inst = cls(hassette, parent=parent)
inst._lock = FairAsyncRLock()
inst._queue = HeapQueue()

inst.mark_ready(reason="Queue ready")

return inst
def __init__(self, hassette: "Hassette", *, parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)
self._lock = FairAsyncRLock()
self._queue = HeapQueue()
self.mark_ready(reason="Queue ready")

@property
def config_log_level(self):
Expand Down
10 changes: 4 additions & 6 deletions src/hassette/core/service_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ class ServiceWatcher(Resource):
_restart_attempts: dict[str, int]
"""Tracks restart attempt counts per service, keyed by 'name:role'."""

@classmethod
def create(cls, hassette: "Hassette"):
inst = cls(hassette, parent=hassette)
inst.bus = inst.add_child(Bus)
inst._restart_attempts = {}
return inst
def __init__(self, hassette: "Hassette", *, parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)
self.bus = self.add_child(Bus)
self._restart_attempts = {}

@property
def config_log_level(self):
Expand Down
28 changes: 8 additions & 20 deletions src/hassette/core/state_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,14 @@ class StateProxy(Resource):
state_change_sub: "Subscription | None"
poll_job: "ScheduledJob | None"

@classmethod
def create(cls, hassette: "Hassette", parent: "Resource"):
"""Create a new StateProxy instance.

Args:
hassette: The Hassette instance.
parent: The parent resource (typically the Hassette core).

Returns:
A new StateProxy instance.
"""
inst = cls(hassette=hassette, parent=parent)
inst.states = {}
inst.lock = FairAsyncRLock()
inst.bus = inst.add_child(Bus, priority=100)
inst.scheduler = inst.add_child(Scheduler)
inst.state_change_sub = None
inst.poll_job = None

return inst
def __init__(self, hassette: "Hassette", *, parent: Resource | None = None) -> None:
super().__init__(hassette, parent=parent)
self.states = {}
self.lock = FairAsyncRLock()
self.bus = self.add_child(Bus, priority=100)
self.scheduler = self.add_child(Scheduler)
self.state_change_sub = None
self.poll_job = None

@property
def config_log_level(self):
Expand Down
12 changes: 5 additions & 7 deletions src/hassette/core/web_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,11 @@ class WebApiService(Service):
port: int
_server: uvicorn.Server | None

@classmethod
def create(cls, hassette: "Hassette", parent: "Resource"):
inst = cls(hassette=hassette, parent=parent)
inst.host = hassette.config.web_api_host
inst.port = hassette.config.web_api_port
inst._server = None
return inst
def __init__(self, hassette: "Hassette", *, parent: "Resource | None" = None) -> None:
super().__init__(hassette, parent=parent)
self.host = hassette.config.web_api_host
self.port = hassette.config.web_api_port
self._server = None

@property
def config_log_level(self):
Expand Down
Loading
Loading