Configuration Management¶
Configuration management in the framework is built around dataclasses and provides automatic JSON serialization, CRUD operations, and backup/restore functionality.
Defining Configuration¶
Configuration is just a dataclass:
from dataclasses import dataclass
@dataclass
class MyDeviceConfig:
"""Device configuration."""
identifier: str
name: str
host: str
port: int = 8080
api_key: str = ""
The framework automatically handles:
- ✅ JSON serialization/deserialization
- ✅ Type validation
- ✅ Default values
- ✅ Nested dataclasses
Creating a Config Manager¶
from ucapi_framework import BaseConfigManager
config = BaseConfigManager[MyDeviceConfig](
data_path="./config",
add_handler=driver.on_device_added,
remove_handler=driver.on_device_removed,
)
CRUD Operations¶
Add or Update¶
device_config = MyDeviceConfig(
identifier="device_1",
name="My Device",
host="192.168.1.100",
)
config.add_or_update(device_config)
Get¶
Remove¶
Iterate All¶
Check Existence¶
Clear All¶
Persistence¶
Configuration is automatically persisted to disk:
- Auto-save on add/update/remove
- Auto-load on initialization
- Atomic writes prevent corruption
- Backup on changes (optional)
Backup & Restore¶
Export Configuration¶
Import Configuration¶
Configuration Callbacks¶
The config manager can notify your driver when devices are added or removed:
def on_device_added(device_config: MyDeviceConfig) -> None:
"""Called when device is added."""
print(f"Device added: {device_config.name}")
driver.add_configured_device(device_config)
def on_device_removed(device_config: MyDeviceConfig | None) -> None:
"""Called when device is removed."""
if device_config is None:
print("All devices removed")
driver.clear_devices()
else:
print(f"Device removed: {device_config.name}")
driver.remove_device(driver.get_device_id(device_config))
config = BaseConfigManager[MyDeviceConfig](
data_path="./config",
add_handler=on_device_added,
remove_handler=on_device_removed,
)
Dynamic Configuration Updates¶
Devices can update their own configuration at runtime:
class MyDevice(StatelessHTTPDevice):
async def authenticate(self) -> None:
"""Authenticate and update token."""
new_token = await self._get_auth_token()
# Update config with new token
self.update_config(api_token=new_token)
# Changes are automatically persisted!
Type Safety¶
The config manager is fully typed:
config = BaseConfigManager[MyDeviceConfig](...)
# IDE knows this returns MyDeviceConfig | None
device = config.get("device_1")
# Type checking works
if device:
print(device.host) # ✅ IDE autocomplete works
print(device.invalid) # ❌ Type error
Migration Support¶
The framework supports configuration migration:
class MyConfigManager(BaseConfigManager[MyDeviceConfig]):
def migration_required(self) -> bool:
"""Check if migration is needed."""
# Check for old config format
return os.path.exists("old_config.json")
async def migrate(self) -> bool:
"""Migrate from old format."""
# Load old config
with open("old_config.json") as f:
old_data = json.load(f)
# Convert to new format
for item in old_data:
new_config = MyDeviceConfig(
identifier=item["id"],
name=item["device_name"],
host=item["ip"],
)
self.add_or_update(new_config)
# Clean up old file
os.remove("old_config.json")
return True
Best Practices¶
- Use dataclasses - They're simple, type-safe, and work great with the framework
- Provide defaults - Use default values for optional fields
- Keep it flat - Avoid deep nesting when possible
- Use type hints - Full type safety means fewer bugs
- Validate on load - Use
__post_init__for validation if needed
@dataclass
class MyDeviceConfig:
identifier: str
name: str
host: str
port: int = 8080
def __post_init__(self):
"""Validate configuration."""
if not 1 <= self.port <= 65535:
raise ValueError(f"Invalid port: {self.port}")
# Normalize host
self.host = self.host.strip()
See the API Reference for complete documentation.