Advanced Patterns

These examples show complex patterns for sophisticated automations.

Using Mixins

Share functionality across apps:

from domovoy.applications import AppBase, AppBaseWithoutConfig, AppConfigBase

# Define a mixin
class AlertsMixin(AppBaseWithoutConfig):
    class AlertsImpl:
        def __init__(self, hass):
            self.hass = hass

        async def send_alert(self, title: str, message: str) -> None:
            await self.hass.fire_event(
                "domovoy_alert",
                {"title": title, "message": message},
            )

    alerts: AlertsImpl

    async def initialize(self) -> None:
        await super().initialize()
        self.alerts = AlertsMixin.AlertsImpl(self.hass)

# Use the mixin
class MyApp(AlertsMixin, AppBase[MyConfig]):
    async def initialize(self) -> None:
        await super().initialize()

        # Access mixin functionality
        await self.alerts.send_alert("Started", "App initialized")

Mixin Composition

Mixins can inherit from other mixins:

class NotificationMixin(PartyModeMixin, AppBaseWithoutConfig):
    """Notifications with party mode awareness."""

    async def initialize(self) -> None:
        await super().initialize()
        # PartyModeMixin is also initialized

    async def notify(self, message: str) -> None:
        if self.is_silent_mode_on():  # From PartyModeMixin
            return

        await self.hass.services.notify.mobile_app(
            message=message,
        )

# App gets both mixins
class MyApp(NotificationMixin, AppBase[Config]):
    async def initialize(self) -> None:
        await super().initialize()

        # From NotificationMixin
        await self.notify("Hello!")

        # From PartyModeMixin (via NotificationMixin)
        if self.is_party_mode_on():
            pass

Concurrent Operations

Use asyncio for parallel operations:

import asyncio

class MultiZoneController(AppBase[ZoneConfig]):
    async def initialize(self) -> None:
        # Create many entities in parallel
        self.zones = await asyncio.gather(*[
            self.create_zone(zone)
            for zone in self.config.zones
        ])

    async def create_zone(self, zone: ZoneInfo):
        sensor, switch, button = await asyncio.gather(
            self.servents.create_sensor(
                servent_id=f"{zone.id}_temp",
                name=f"{zone.name} Temperature",
            ),
            self.servents.create_switch(
                servent_id=f"{zone.id}_enabled",
                name=f"{zone.name} Enabled",
            ),
            self.servents.listen_button_press(
                self.on_zone_reset,
                button_name=f"{zone.name} Reset",
                event_name_to_fire=f"{zone.id}_reset",
                event_data={"zone_id": zone.id},
            ),
        )
        return {"sensor": sensor, "switch": switch, "button": button}

State Machine Pattern

Manage complex states:

from enum import Enum
from typing import Literal

class SystemState(Enum):
    IDLE = "idle"
    WARMING_UP = "warming_up"
    RUNNING = "running"
    COOLING_DOWN = "cooling_down"
    ERROR = "error"

class StateMachineApp(AppBase[StateMachineConfig]):
    state: SystemState = SystemState.IDLE

    async def initialize(self) -> None:
        self.state_sensor = await self.servents.create_sensor(
            servent_id="system_state",
            name="System State",
        )
        await self.update_state_sensor()

        # Start button
        await self.servents.listen_button_press(
            self.start_system,
            button_name="Start System",
            event_name_to_fire="system_start",
        )

    async def transition_to(self, new_state: SystemState) -> None:
        old_state = self.state
        self.state = new_state

        self.log.info(
            "State transition: {old} -> {new}",
            old=old_state.value,
            new=new_state.value,
        )

        await self.update_state_sensor()
        await self.on_state_enter(new_state)

    async def on_state_enter(self, state: SystemState) -> None:
        if state == SystemState.WARMING_UP:
            await self.time.sleep_for(Interval(minutes=5))
            await self.transition_to(SystemState.RUNNING)

        elif state == SystemState.COOLING_DOWN:
            await self.time.sleep_for(Interval(minutes=2))
            await self.transition_to(SystemState.IDLE)

    async def start_system(self) -> None:
        if self.state != SystemState.IDLE:
            self.log.warning("Cannot start, not idle")
            return

        await self.transition_to(SystemState.WARMING_UP)

    async def update_state_sensor(self) -> None:
        await self.state_sensor.set_to(self.state.value)

External API Integration

Call external services:

import aiohttp

class WeatherIntegration(AppBase[WeatherConfig]):
    session: aiohttp.ClientSession | None = None

    async def initialize(self) -> None:
        self.session = aiohttp.ClientSession()

        self.weather_sensor = await self.servents.create_sensor(
            servent_id="external_weather",
            name="External Weather",
        )

        self.callbacks.run_every(
            Interval(hours=1),
            self.fetch_weather,
            "now",
        )

    async def finalize(self) -> None:
        if self.session:
            await self.session.close()

    async def fetch_weather(self) -> None:
        if not self.session:
            return

        try:
            async with self.session.get(self.config.api_url) as resp:
                data = await resp.json()
                await self.weather_sensor.set_to(
                    data["temperature"],
                    {"humidity": data["humidity"]},
                )
        except Exception as e:
            self.log.error("Weather fetch failed: {err}", err=e)

TypeGuard Validation

Type-safe runtime validation:

from typing import Literal, get_args
from typing_extensions import TypeGuard

ValidMode = Literal["auto", "manual", "schedule", "off"]
VALID_MODES: list[str] = list(get_args(ValidMode))

def is_valid_mode(value: str) -> TypeGuard[ValidMode]:
    return value in VALID_MODES

class TypeSafeApp(AppBase[Config]):
    async def on_mode_change(self, new) -> None:
        if not isinstance(new, str):
            return

        if is_valid_mode(new):
            # new is now typed as ValidMode
            await self.apply_mode(new)
        else:
            self.log.warning("Invalid mode: {mode}", mode=new)

    async def apply_mode(self, mode: ValidMode) -> None:
        # Type-safe handling
        match mode:
            case "auto":
                pass
            case "manual":
                pass
            case "schedule":
                pass
            case "off":
                pass

Persistent Storage

Save state across restarts:

import json
from pathlib import Path

class PersistentApp(AppBase[PersistentConfig]):
    storage_path: Path

    async def initialize(self) -> None:
        self.storage_path = Path(f"storage/{self.meta.get_app_name()}.json")
        self.storage_path.parent.mkdir(exist_ok=True)

        # Load saved state
        self.data = self.load_data()

        self.counter_sensor = await self.servents.create_sensor(
            servent_id="counter",
            name="Counter",
            default_state=self.data.get("counter", 0),
        )

    def load_data(self) -> dict:
        if self.storage_path.exists():
            return json.loads(self.storage_path.read_text())
        return {}

    def save_data(self) -> None:
        self.storage_path.write_text(json.dumps(self.data))

    async def increment(self) -> None:
        self.data["counter"] = self.data.get("counter", 0) + 1
        self.save_data()
        await self.counter_sensor.set_to(self.data["counter"])

Event-Driven Architecture

React to custom events:

class EventDrivenApp(AppBase[EventConfig]):
    async def initialize(self) -> None:
        # Listen to custom events
        self.callbacks.listen_event(
            "domovoy_command",
            self.on_command,
        )

        # Create command button
        await self.servents.listen_button_press(
            self.send_command,
            button_name="Send Command",
            event_name_to_fire="domovoy_command",
            event_data={"action": "refresh"},
        )

    async def on_command(self, data: dict) -> None:
        action = data.get("action")
        self.log.info("Received command: {action}", action=action)

        if action == "refresh":
            await self.refresh_data()
        elif action == "reset":
            await self.reset_state()

Key Concepts

  • Mixins: Share functionality via multiple inheritance

  • asyncio.gather: Parallel async operations

  • State machines: Manage complex state transitions

  • External APIs: aiohttp for HTTP requests

  • TypeGuard: Runtime type validation

  • Persistence: Save state to disk

  • Events: Loosely coupled communication