Satellite-Management mit Gruppen, Load-Balancing, Health-Monitoring und Failover.
from trixy_core.satellite import (
# Core
Satellite, SatelliteManager, ConnectionState, SatelliteSocket,
# Registration
RegistrationManager,
# API
SatelliteAPI, SatelliteProxy, SatelliteCollection,
get_satellite_api, TTSRequest, StreamSession, StreamState,
# Groups
SatelliteGroup, GroupManager, GroupType, GroupConfig,
Zone, ZoneManager, ZoneConfig,
# Load Balancing
LoadBalancer, LoadBalancingStrategy, SelectionResult,
RoundRobinStrategy, LeastConnectionsStrategy, WeightedStrategy,
# Health
HealthScorer, HealthScore, HealthMetrics, HealthState, HealthThresholds,
# Failover
FailoverManager, FailoverConfig, FailoverState,
BackupAssignment, BackupRegistry,
# Capability Discovery
CapabilityRegistry, Capability, CapabilityInfo, CapabilityState,
CapabilityDiscovery, DiscoveryConfig, CapabilityProbe,
# Metrics
SatelliteMetricsCollector, SatelliteMetrics, MetricValue, MetricType,
MetricsAggregator, AggregationType, TimeWindow, AggregatedMetric,
)
Repräsentiert ein verbundenes Satellite-Gerät.
from trixy_core.satellite import Satellite, ConnectionState
# Satellite-Eigenschaften
satellite = manager.get("living_room")
print(satellite.id) # "living_room"
print(satellite.room_id) # "wohnzimmer"
print(satellite.mac_address) # "AA:BB:CC:DD:EE:FF"
print(satellite.alias) # "Wohnzimmer"
print(satellite.ip_address) # "192.168.1.100"
print(satellite.state) # ConnectionState.CONNECTED
print(satellite.is_connected) # True
print(satellite.version) # "1.0.0"
# Konversation
print(satellite.conversation_id) # "conv_123" oder None
print(satellite.has_active_conversation) # True/False
# Zeitstempel
print(satellite.connected_at) # datetime
print(satellite.last_heartbeat) # datetime
# TTS (Text-to-Speech)
await satellite.speak(
text="Hallo Welt",
voice="default",
speed=1.0,
volume=1.0,
)
# Raw Audio
await satellite.say(audio_bytes)
# Musik/Streams
await satellite.play(source="music/song.mp3", volume=0.8)
await satellite.stop_playback()
# Verbindung trennen
await satellite.disconnect(reason="Manuell getrennt")
from trixy_core.satellite import ConnectionState
class ConnectionState(Enum):
DISCONNECTED = "disconnected" # Nicht verbunden
CONNECTING = "connecting" # Verbindung wird aufgebaut
CONNECTED = "connected" # Verbunden
AUTHENTICATED = "authenticated" # Authentifiziert
DISCONNECTING = "disconnecting" # Verbindung wird getrennt
ERROR = "error" # Fehler
Verwaltet alle registrierten und verbundenen Satellites.
from trixy_core.satellite import SatelliteManager
manager = SatelliteManager(application)
# Statistiken
print(manager.count) # Gesamt registriert
print(manager.connected_count) # Aktuell verbunden
# Nach ID
satellite = manager.get("living_room")
# Nach MAC-Adresse
satellite = manager.get_by_mac("AA:BB:CC:DD:EE:FF")
# Nach Raum
satellites = manager.get_by_room("wohnzimmer")
# Gefiltert
connected = manager.get_connected()
disconnected = manager.get_disconnected()
all_satellites = manager.get_all()
# Suche mit Kriterien
results = manager.find(
room="wohnzimmer",
alias="Wohnzimmer",
connected=True,
)
# Index-Zugriff
satellite = manager[0] # Nach Position
satellite = manager["living_room"] # Nach ID
satellite = manager["room:wohnzimmer"] # Nach Raum
satellite = manager["mac:AA:BB:CC:DD:EE:FF"] # Nach MAC
# Einzelner Satellite
await manager.disconnect("living_room", reason="Wartung")
await manager.reconnect("living_room")
# Alle Satellites
count = await manager.disconnect_all(reason="Server Shutdown")
count = await manager.reconnect_all()
# TTS an alle/bestimmte
await manager.say("Hallo", room="wohnzimmer", voice="default")
# Raw Audio Broadcast
count = await manager.say_raw(audio_bytes, room="wohnzimmer")
# Musik-Stream
session = await manager.play(
source="music/playlist.m3u",
room="wohnzimmer",
volume=0.7,
)
Verwaltet Satellite-Registrierungen (60-Sekunden-Fenster).
from trixy_core.satellite import RegistrationManager
reg_manager = RegistrationManager(application)
# Registrierungsmodus aktivieren
reg_manager.enable_registration_mode(timeout_seconds=60)
print(reg_manager.registration_mode) # True
# Registrierung starten
success = reg_manager.begin_registration(
mac_address="AA:BB:CC:DD:EE:FF",
room="wohnzimmer",
alias="Wohnzimmer",
)
# Registrierung abschließen
satellite = reg_manager.complete_registration("AA:BB:CC:DD:EE:FF")
# Registrierung abbrechen
reg_manager.cancel_registration("AA:BB:CC:DD:EE:FF")
# Registrierungsmodus deaktivieren
reg_manager.disable_registration_mode()
# Prüfen und Laden
is_reg = reg_manager.is_registered("AA:BB:CC:DD:EE:FF")
satellite = reg_manager.load_registered("AA:BB:CC:DD:EE:FF")
all_registered = reg_manager.load_all()
# Löschen
reg_manager.delete_registration("AA:BB:CC:DD:EE:FF")
High-Level API für Plugins.
from trixy_core.satellite import get_satellite_api, SatelliteAPI
api = get_satellite_api(application)
# Collection
satellites = api.satellites
# Abruf
satellite = satellites["living_room"] # Nach ID
satellite = satellites["room:wohnzimmer"] # Nach Raum-Selektor
satellite = satellites[0] # Nach Index
# Listen
all_proxies = satellites.all
connected = satellites.connected
disconnected = satellites.disconnected
# Nach Raum
room_satellites = satellites.room("wohnzimmer")
# Suche
results = satellites.find(room="wohnzimmer", connected=True)
# Text-to-Speech
await api.say(
text="Hallo Welt",
satellites=["living_room", "bedroom"], # oder None für alle
room="wohnzimmer", # oder nach Raum
voice="default",
speed=1.0,
volume=1.0,
wait=True, # Warten auf Fertigstellung
priority=0, # Höher = wichtiger
)
# Raw Audio
count = await api.say_raw(
audio_data=audio_bytes,
satellites=["living_room"],
room=None,
)
# Stream starten
session = await api.play(
source="music/song.mp3",
satellites=["living_room"],
room=None,
volume=0.8,
)
# Stream-Management
await api.add_to_stream(session.id, satellite)
await api.remove_from_stream(session.id, satellite)
await api.stop_stream(session.id)
# Stoppen
await api.stop_satellite("living_room")
count = await api.stop_all()
# Lautstärke
await api.set_volume(satellite="living_room", volume=0.5)
Wrapper für einzelne Satellites.
from trixy_core.satellite import SatelliteProxy
proxy = api.satellites["living_room"]
# Eigenschaften
print(proxy.id) # "living_room"
print(proxy.alias) # "Wohnzimmer"
print(proxy.room) # "wohnzimmer"
print(proxy.is_connected) # True
# Zugriff auf rohes Satellite
raw = proxy.raw
# Audio
await proxy.say("Hallo", voice="default", wait=True)
await proxy.say_raw(audio_bytes)
session = await proxy.play("music/song.mp3", volume=0.8)
await proxy.stop()
await proxy.set_volume(0.5)
Logische Gruppierung von Satellites.
from trixy_core.satellite import (
SatelliteGroup, GroupManager, GroupType, GroupConfig
)
# Konfiguration
config = GroupConfig(
auto_add=True, # Automatisch hinzufügen
auto_remove=True, # Automatisch entfernen
max_members=10, # Maximum Mitglieder
priority=0, # Gruppen-Priorität
)
# Gruppe erstellen
group = SatelliteGroup(
id="living_area",
name="Wohnbereich",
group_type=GroupType.ZONE,
config=config,
)
# Mitglieder verwalten
group.add(satellite)
group.remove("living_room")
has_member = group.has("living_room")
# Mitglieder abrufen
members = group.get_members()
filtered = group.filter(lambda s: s.is_connected)
# Iteration
for member in group:
print(member.alias)
print(len(group)) # Mitglieder-Anzahl
from trixy_core.satellite import GroupManager
manager = GroupManager(application)
# Gruppe erstellen
group = manager.create(
group_id="living_area",
name="Wohnbereich",
group_type=GroupType.ZONE,
config=config,
)
# Abrufen
group = manager.get("living_area")
all_groups = manager.get_all()
# Suchen
by_name = manager.find_by_name("Wohnbereich")
containing = manager.find_containing("living_room")
# Löschen
manager.delete("living_area")
from trixy_core.satellite import GroupType
class GroupType(Enum):
ROOM = "room" # Nach Raum
ZONE = "zone" # Geografische Zone
CUSTOM = "custom" # Benutzerdefiniert
DYNAMIC = "dynamic" # Dynamisch basierend auf Regeln
from trixy_core.satellite import Zone, ZoneManager, ZoneConfig
config = ZoneConfig(
location="Erdgeschoss",
boundary={"x_min": 0, "x_max": 10, "y_min": 0, "y_max": 10},
)
zone = Zone(
id="ground_floor",
name="Erdgeschoss",
config=config,
)
# ZoneManager wie GroupManager verwenden
zone_manager = ZoneManager(application)
from trixy_core.satellite import (
LoadBalancer, LoadBalancingStrategy, SelectionResult,
RoundRobinStrategy, LeastConnectionsStrategy, WeightedStrategy,
)
# Round-Robin
strategy = RoundRobinStrategy()
# Least-Connections
strategy = LeastConnectionsStrategy()
# Weighted
from trixy_core.satellite import WeightConfig
strategy = WeightedStrategy(weights={
"living_room": WeightConfig(weight=2.0),
"bedroom": WeightConfig(weight=1.0),
})
from trixy_core.satellite import LoadBalancer
balancer = LoadBalancer(strategy=RoundRobinStrategy())
# Satellite auswählen
satellites = manager.get_connected()
result = balancer.select(satellites, context={"task": "tts"})
if result:
print(result.satellite.id) # Ausgewählter Satellite
print(result.reason) # Auswahlgrund
print(result.score) # Score (falls berechnet)
# Request-Tracking
balancer.on_request_start(result.satellite)
try:
await process(result.satellite)
finally:
balancer.on_request_end(result.satellite, success=True)
# Zurücksetzen
balancer.reset()
from trixy_core.satellite import LoadBalancingStrategy, SelectionResult
class LatencyBasedStrategy(LoadBalancingStrategy):
@property
def name(self) -> str:
return "latency_based"
def select(
self,
satellites: list,
context: dict = None,
) -> SelectionResult | None:
if not satellites:
return None
# Niedrigste Latenz wählen
best = min(satellites, key=lambda s: s.metadata.get("latency", 999))
return SelectionResult(
satellite=best,
reason="Lowest latency",
score=1.0 / (best.metadata.get("latency", 1) + 1),
)
from trixy_core.satellite import (
HealthScorer, HealthScore, HealthMetrics, HealthState,
)
scorer = HealthScorer()
# Metriken sammeln
metrics = HealthMetrics(
latency_ms=50.0,
packet_loss=0.01,
uptime=86400.0,
error_rate=0.001,
memory_usage=0.6,
cpu_usage=0.3,
last_heartbeat_age=5.0,
connection_quality=0.95,
custom_metrics={"audio_quality": 0.9},
)
# Score berechnen
score = scorer.calculate(satellite, metrics)
print(score.overall) # 0-100
print(score.state) # HealthState.HEALTHY
print(score.is_healthy) # True
print(score.is_usable) # True
print(score.components) # Dict mit Komponenten-Scores
print(score.warnings) # Liste von Warnungen
from trixy_core.satellite import HealthState
class HealthState(Enum):
HEALTHY = "healthy" # Alles OK
DEGRADED = "degraded" # Eingeschränkt
UNHEALTHY = "unhealthy" # Nicht OK
CRITICAL = "critical" # Kritisch
UNKNOWN = "unknown" # Unbekannt
from trixy_core.satellite import HealthScorer
scorer = HealthScorer()
def audio_quality_scorer(satellite, metrics) -> float:
quality = metrics.custom_metrics.get("audio_quality", 1.0)
return quality * 100 # Normalisiert auf 0-100
scorer.add_custom_scorer("audio", audio_quality_scorer)
scorer.remove_custom_scorer("audio")
from trixy_core.satellite import HealthThresholds, ThresholdConfig
thresholds = HealthThresholds(
latency=ThresholdConfig(warning=100, critical=500),
packet_loss=ThresholdConfig(warning=0.05, critical=0.2),
error_rate=ThresholdConfig(warning=0.01, critical=0.1),
)
from trixy_core.satellite import (
FailoverManager, FailoverConfig, FailoverState,
)
config = FailoverConfig(
enabled=True,
auto_failover=True,
failback_delay=60.0, # Sekunden bis Failback
health_threshold=30.0, # Health-Score für Failover
)
failover = FailoverManager(config)
# Primary und Backup registrieren
failover.register_primary(primary_satellite)
failover.register_backup("living_room", backup_satellite)
# Manuelles Failover
backup = failover.trigger_failover("living_room")
if backup:
print(f"Failover zu {backup.id}")
# Backup abrufen
backup = failover.get_backup("living_room")
from trixy_core.satellite import BackupAssignment, BackupRegistry
registry = BackupRegistry()
# Zuweisung erstellen
assignment = BackupAssignment(
primary_id="living_room",
backup_id="bedroom",
priority=1,
)
registry.add(assignment)
backup_id = registry.get_backup("living_room")
from trixy_core.satellite import FailoverState
class FailoverState(Enum):
NORMAL = "normal" # Normalbetrieb
FAILOVER_PENDING = "pending" # Failover wird vorbereitet
FAILED_OVER = "failed_over" # Failover aktiv
FAILBACK_PENDING = "failback" # Failback wird vorbereitet
Dynamische Fähigkeitserkennung für Satellites.
from trixy_core.satellite.capability import (
CapabilityInfo,
Capability,
CapabilityState,
CapabilityRegistry,
CapabilityDiscovery,
DiscoveryConfig,
)
# Capability definieren
info = CapabilityInfo(
id="audio.input",
name="Mikrofon",
description="Audio-Eingang für Sprachaufnahme",
category="audio",
required_features=["pyaudio"],
)
from trixy_core.satellite.capability import Capability, CapabilityState
cap = Capability(
info=info,
state=CapabilityState.AVAILABLE,
satellite_id="sat-001",
)
# Eigenschaften
print(cap.id) # "audio.input"
print(cap.state) # CapabilityState.AVAILABLE
print(cap.is_available) # True
print(cap.is_degraded) # False
# State aktualisieren
cap.update_state(
CapabilityState.UNAVAILABLE,
error_message="Gerät nicht gefunden",
)
# Als Dictionary
d = cap.to_dict()
from trixy_core.satellite.capability import CapabilityState
class CapabilityState(Enum):
UNKNOWN = "unknown" # Nicht geprüft
AVAILABLE = "available" # Verfügbar
UNAVAILABLE = "unavailable" # Nicht verfügbar
DEGRADED = "degraded" # Eingeschränkt
CHECKING = "checking" # Wird geprüft
from trixy_core.satellite.capability import CapabilityRegistry
registry = CapabilityRegistry()
# Definition registrieren
registry.define_capability(info)
# Capability für Satellite registrieren
cap = Capability(info=info, state=CapabilityState.AVAILABLE)
registry.register("sat-001", cap)
# Abrufen
capability = registry.get("sat-001", "audio.input")
all_caps = registry.get_all("sat-001")
available = registry.get_available("sat-001")
by_category = registry.get_by_category("sat-001", "audio")
# Prüfen
has = registry.has_capability("sat-001", "audio.input")
has_available = registry.has_capability("sat-001", "audio.input", require_available=True)
has_all = registry.has_all_capabilities("sat-001", ["audio.input", "audio.output"])
has_any = registry.has_any_capability("sat-001", ["audio.input", "display"])
# Satellites finden
satellites = registry.find_satellites_with("audio.input")
satellites = registry.find_satellites_with_all(["audio.input", "audio.output"])
# State aktualisieren
registry.update_state(
"sat-001",
"audio.input",
CapabilityState.DEGRADED,
"Niedrige Qualität",
)
# Entfernen
registry.unregister("sat-001", "audio.input") # Eine Capability
registry.unregister("sat-001") # Alle Capabilities
# Callback
def on_change(sat_id: str, cap: Capability):
print(f"{sat_id}: {cap.id} -> {cap.state}")
registry.on_change(on_change)
# Zusammenfassung
summary = registry.get_summary()
print(f"Satellites: {summary['satellite_count']}")
print(f"Capabilities: {summary['total_capabilities']}")
print(f"Verfügbar: {summary['available_capabilities']}")
from trixy_core.satellite.capability import (
CapabilityDiscovery,
DiscoveryConfig,
SimpleProbe,
)
config = DiscoveryConfig(
probe_timeout=10.0, # Probe-Timeout
probe_concurrency=5, # Parallele Probes
auto_discover=True, # Auto-Discovery bei Connect
rediscovery_interval=300, # Sekunden bis Re-Discovery
)
# Discovery erstellen
async def send_capability_check(sat_id: str, message: dict) -> dict:
return await satellite.send_and_wait(message)
discovery = CapabilityDiscovery(
registry=registry,
send_func=send_capability_check,
config=config,
)
# Probe registrieren
probe = SimpleProbe(info)
discovery.register_probe(probe)
# Einzelne Capability prüfen
result = await discovery.discover_capability("sat-001", "audio.input")
if result.success:
print(f"State: {result.state}")
print(f"Parameters: {result.parameters}")
# Alle Capabilities eines Satellites
results = await discovery.discover_satellite("sat-001")
# Spezifische Capabilities
results = await discovery.discover_satellite(
"sat-001",
capability_ids=["audio.input", "display"],
)
# Mehrere Satellites
all_results = await discovery.discover_all(["sat-001", "sat-002"])
# Rediscovery prüfen
needs_check = discovery.needs_rediscovery("sat-001")
# Statistiken
stats = discovery.get_stats()
print(f"Probes: {stats['probe_count']}")
from trixy_core.satellite.capability import CapabilityProbe, ProbeResult
class AudioProbe(CapabilityProbe):
def __init__(self):
super().__init__(CapabilityInfo(
id="audio.input",
name="Mikrofon",
category="audio",
))
async def probe(
self,
satellite_id: str,
send_func,
) -> ProbeResult:
try:
response = await send_func(satellite_id, {
"type": "capability_check",
"capability": "audio.input",
})
if response.get("available"):
return ProbeResult(
capability_id=self.info.id,
success=True,
state=CapabilityState.AVAILABLE,
parameters=response.get("parameters", {}),
)
else:
return ProbeResult(
capability_id=self.info.id,
success=True,
state=CapabilityState.UNAVAILABLE,
error_message=response.get("error"),
)
except Exception as e:
return ProbeResult(
capability_id=self.info.id,
success=False,
state=CapabilityState.UNKNOWN,
error_message=str(e),
)
discovery.register_probe(AudioProbe())
Umfassende Metriken-Sammlung und Aggregation.
from trixy_core.satellite.metrics import (
SatelliteMetricsCollector,
SatelliteMetrics,
MetricValue,
MetricType,
MetricsAggregator,
AggregationType,
TimeWindow,
AggregatedMetric,
)
class MetricType(Enum):
COUNTER = "counter" # Zähler (nur steigend)
GAUGE = "gauge" # Momentanwert
HISTOGRAM = "histogram" # Verteilung
TIMING = "timing" # Zeitdauer
from trixy_core.satellite.metrics import SatelliteMetricsCollector
collector = SatelliteMetricsCollector()
# Metriken aufzeichnen
collector.record_latency("sat-001", 15.5)
collector.record_message_sent("sat-001", bytes_count=256)
collector.record_message_received("sat-001", bytes_count=512)
collector.record_heartbeat("sat-001")
collector.record_reconnect("sat-001")
collector.record_error("sat-001", "Connection lost")
collector.record_packet_loss("sat-001", percent=2.5)
collector.record_bandwidth("sat-001", kbps=1000.0)
# Benutzerdefinierte Metrik
collector.record_custom(
"sat-001",
name="cpu_usage",
value=45.5,
metric_type=MetricType.GAUGE,
unit="%",
)
# Metriken abrufen
metrics = collector.get_metrics("sat-001")
print(f"Latenz: {metrics.latency_ms}ms")
print(f"Gesendet: {metrics.messages_sent}")
print(f"Fehler: {metrics.error_count}")
# Custom Metriken
cpu_history = collector.get_custom_metric("sat-001", "cpu_usage")
# Alle Satellites
all_metrics = collector.get_all_metrics()
# Zusammenfassung
summary = collector.get_summary()
print(f"Satellites: {summary['satellite_count']}")
print(f"Avg Latenz: {summary['avg_latency_ms']}ms")
print(f"Fehler: {summary['total_errors']}")
# Callback für neue Metriken
def on_metric(sat_id: str, value: MetricValue):
print(f"{sat_id}: {value.name} = {value.value}")
collector.on_metric(on_metric)
# Aufräumen
collector.remove_satellite("sat-001")
collector.clear_all()
from trixy_core.satellite.metrics import SatelliteMetrics
metrics = SatelliteMetrics(satellite_id="sat-001")
# Latenz aufzeichnen
metrics.record_latency(15.5)
metrics.record_latency(20.0)
metrics.record_latency(10.0)
# Durchschnitt berechnen
avg = metrics.get_avg_latency(window_seconds=300)
print(f"Durchschnitt (5 Min): {avg}ms")
# Fehler aufzeichnen
metrics.record_error("Connection timeout")
print(f"Fehler: {metrics.error_count}")
print(f"Letzter Fehler: {metrics.last_error}")
# Als Dictionary
d = metrics.to_dict()
from trixy_core.satellite.metrics import (
MetricsAggregator,
AggregationType,
TimeWindow,
)
aggregator = MetricsAggregator(collector)
# Einzelne Aggregation
result = aggregator.aggregate(
metric_name="latency",
aggregation=AggregationType.AVG,
window=TimeWindow.FIVE_MINUTES,
)
print(f"Wert: {result.value}")
print(f"Samples: {result.sample_count}")
print(f"Zeitraum: {result.start_time} - {result.end_time}")
# AggregationType
class AggregationType(Enum):
SUM = "sum" # Summe
AVG = "avg" # Durchschnitt
MIN = "min" # Minimum
MAX = "max" # Maximum
COUNT = "count" # Anzahl
MEDIAN = "median" # Median
P95 = "p95" # 95. Perzentil
P99 = "p99" # 99. Perzentil
# TimeWindow
class TimeWindow(Enum):
MINUTE = 60
FIVE_MINUTES = 300
HOUR = 3600
DAY = 86400
# Mehrfache Aggregation
results = aggregator.aggregate_multiple(
metrics=["latency", "error_count"],
aggregations=[AggregationType.AVG, AggregationType.SUM],
)
print(results["latency"]["AVG"])
print(results["error_count"]["SUM"])
# Top Satellites
top = aggregator.get_top_satellites(
metric="latency",
limit=5,
ascending=False, # Höchste zuerst
)
for sat_id, value in top:
print(f"{sat_id}: {value}ms")
# Verteilung
dist = aggregator.get_metric_distribution(
metric="latency",
buckets=10,
)
print(f"Min: {dist['min']}")
print(f"Max: {dist['max']}")
print(f"Count: {dist['count']}")
print(f"Buckets: {dist['buckets']}")
# Satellite-Vergleich
comparison = aggregator.get_satellite_comparison(
metric="latency",
satellite_ids=["sat-001", "sat-002"],
)
for sat_id, data in comparison.items():
print(f"{sat_id}: current={data['current']}, avg={data['avg']}")
# Health Summary
summary = aggregator.get_health_summary()
print(f"Status: {summary['status']}")
print(f"Satellites: {summary['satellite_count']}")
print(f"Metriken: {summary['metrics']}")
# Cache
aggregator.clear_cache()
from trixy_core.satellite.metrics import MetricValue
value = MetricValue(
name="latency",
value=15.5,
metric_type=MetricType.TIMING,
unit="ms",
labels={"endpoint": "/api/data"},
)
print(value.name) # "latency"
print(value.value) # 15.5
print(value.metric_type) # MetricType.TIMING
print(value.unit) # "ms"
print(value.labels) # {"endpoint": "/api/data"}
d = value.to_dict()