Mixins Guide¶
Mixins allow you to share functionality across multiple apps without code duplication.
What Are Mixins?¶
Mixins are classes that provide reusable functionality that can be “mixed in” to your apps through multiple inheritance.
class MyApp(AlertsMixin, NotificationMixin, AppBase[MyAppConfig]):
async def initialize(self) -> None:
# Access mixin functionality
await self.alerts.add_alert(...)
await self.notify_house_tts("Hello!")
Creating a Mixin¶
Basic Mixin Structure¶
from domovoy.applications import AppBaseWithoutConfig
class MyMixin(AppBaseWithoutConfig):
async def initialize(self) -> None:
# Always call super() to chain initialization
await super().initialize()
# Your mixin setup code
self.my_data = {}
def my_helper_method(self) -> str:
return "Hello from mixin!"
Using the Mixin¶
class MyApp(MyMixin, AppBase[MyAppConfig]):
async def initialize(self) -> None:
await super().initialize() # Initializes mixin too
result = self.my_helper_method()
self.log.info("Mixin says: {msg}", msg=result)
Mixin Patterns¶
Inner Implementation Class¶
Encapsulate mixin logic in an inner class for clean separation:
class AlertsMixin(AppBaseWithoutConfig):
class AlertsMixinImpl:
def __init__(self, hass, callbacks, logger):
self.__hass = hass
self.__callbacks = callbacks
self.__log = logger
async def add_alert(self, config: AlertConfiguration) -> None:
await self.__hass.fire_event("add_alert", asdict(config))
async def clear_alert(self, alert_id: str) -> None:
await self.__hass.fire_event("clear_alert", {"id": alert_id})
alerts: AlertsMixinImpl
async def initialize(self) -> None:
await super().initialize()
self.alerts = AlertsMixin.AlertsMixinImpl(
self.hass,
self.callbacks,
self.log,
)
Usage:
class MyApp(AlertsMixin, AppBase[MyAppConfig]):
async def initialize(self) -> None:
await super().initialize()
# Clean, namespaced API
await self.alerts.add_alert(AlertConfiguration(
id="my_alert",
title="Something happened",
))
Mixin Composition¶
Mixins can inherit from other mixins:
class NotificationMixin(PartyModeMixin, AppBaseWithoutConfig):
"""NotificationMixin includes PartyModeMixin functionality."""
async def notify_house_tts(self, message: str) -> None:
# Check party mode from inherited mixin
if self.is_silent_mode_on():
return # Don't announce during silent mode
await self.hass.services.tts.speak(
entity_id=self.tts_entity,
message=message,
)
Apps using NotificationMixin automatically get PartyModeMixin too:
class MyApp(NotificationMixin, AppBase[MyAppConfig]):
async def initialize(self) -> None:
await super().initialize()
# From NotificationMixin
await self.notify_house_tts("Hello!")
# From PartyModeMixin (via NotificationMixin)
if self.is_party_mode_on(backyard=True):
self.log.info("Party time!")
Global Entities in Mixins¶
Create shared entities that persist across apps:
from servents.data_model.entity_configs import DeviceConfig
class PartyModeMixin(AppBaseWithoutConfig):
async def initialize(self) -> None:
await super().initialize()
device_config = DeviceConfig(
device_id="global_party_control",
name="Global Party Controls",
app_name="global_party_controls",
is_global=True, # Shared across all apps
)
# This entity is created once, shared by all apps using this mixin
self.party_mode_select = await self.servents.create_select(
servent_id="party_mode",
name="Party Mode",
options=["None", "Backyard", "Inside", "Full House"],
creation_config=ExtraConfig(device_config=device_config),
)
def is_party_mode_on(self, *, inside: bool = False, backyard: bool = False) -> bool:
state = self.party_mode_select.get_state()
return (
(state == "Full House" and (inside or backyard))
or (inside and state == "Inside")
or (backyard and state == "Backyard")
)
Storage Mixin¶
Persist data across app restarts:
import json
from pathlib import Path
class StorageMixin(AppBaseWithoutConfig):
class StorageMixinImpl:
def __init__(self, app_name: str):
self.storage_path = Path(f"storage/{app_name}.json")
self.storage_path.parent.mkdir(exist_ok=True)
def load(self, key: str = "__default") -> dict | None:
if not self.storage_path.exists():
return None
data = json.loads(self.storage_path.read_text())
return data.get(key)
def save(self, data: dict, key: str = "__default") -> None:
existing = {}
if self.storage_path.exists():
existing = json.loads(self.storage_path.read_text())
existing[key] = data
self.storage_path.write_text(json.dumps(existing))
storage: StorageMixinImpl
async def initialize(self) -> None:
await super().initialize()
self.storage = StorageMixin.StorageMixinImpl(self.meta.get_app_name())
Generic Mixins with TypeVar¶
Create type-safe mixins that work with specific config types:
from typing import TypeVar
TConfig = TypeVar("TConfig", bound=BaseZWaveSwitchConfig)
class AbstractZWaveSwitch(AppBase[TConfig]):
"""Abstract base for Z-Wave switch handlers."""
async def initialize(self) -> None:
await super().initialize()
self.callbacks.listen_event(
"zwave_js_value_notification",
self.on_zwave_event,
)
async def on_zwave_event(self, data: dict) -> None:
# Parse button press
button = self._parse_button(data)
action = self._parse_action(data)
# Dispatch to specific handler
if action == "KeyPressed":
await self.on_key_press(button)
elif action == "KeyPressed2x":
await self.on_2x_key_press(button)
# Override these in subclass
async def on_key_press(self, button: str) -> None:
pass
async def on_2x_key_press(self, button: str) -> None:
pass
Best Practices¶
Always Call super().initialize()¶
async def initialize(self) -> None:
await super().initialize() # Required!
# Your code here
Keep Mixins Focused¶
Each mixin should do one thing well:
# Good: Single responsibility
class AlertsMixin(AppBaseWithoutConfig):
# Only alert-related functionality
pass
class NotificationMixin(AppBaseWithoutConfig):
# Only notification-related functionality
pass
# Bad: Too many responsibilities
class KitchenSinkMixin(AppBaseWithoutConfig):
# Alerts, notifications, storage, logging, etc.
pass
Order Matters¶
Mixins are processed left-to-right:
class MyApp(FirstMixin, SecondMixin, AppBase[Config]):
# FirstMixin's initialize() runs before SecondMixin's
pass
Use Type Hints¶
class AlertsMixin(AppBaseWithoutConfig):
alerts: AlertsMixinImpl # Type hint for IDE support
async def initialize(self) -> None:
await super().initialize()
self.alerts = AlertsMixin.AlertsMixinImpl(...)