Skip to content

Latest commit

 

History

History
209 lines (177 loc) · 7.19 KB

File metadata and controls

209 lines (177 loc) · 7.19 KB

Architecture

Objective

Provide a maintainable local-control platform for Devialet Phantom volume, with:

  • stable CLI commands for direct use and scripts
  • daemon mode for event-driven inputs (CEC now, keyboard now, IR/HA later)
  • clean separation of domain logic from I/O transports

Design Principles

  • Keep business behavior in domain + application, keep side effects in infrastructure.
  • Preserve CLI compatibility while evolving internals.
  • Treat input methods as adapters that emit normalized events.
  • Prefer async-first I/O paths for runtime resilience and predictable concurrency.

Layered Structure

  • src/devialetctl/domain
    • events.py: canonical input/event types
    • policy.py: dedupe and rate-limit policy
  • src/devialetctl/application
    • service.py: volume use-cases (including +1/-1 relative steps)
    • router.py: maps normalized events to actions
    • daemon.py: async CEC orchestration, watcher polling, and retry behavior
    • ports.py: contracts (VolumeGateway, discovery target models)
  • src/devialetctl/infrastructure
    • devialet_gateway.py: async HTTP calls to Devialet API (httpx.AsyncClient)
    • mdns_gateway.py: mDNS/zeroconf discovery + filtering
    • upnp_gateway.py: SSDP/UPnP discovery (MediaRenderer:2)
    • cec_adapter.py: Linux CEC kernel adapter (/dev/cec0, ioctl, async event stream)
    • keyboard_adapter.py: single-key or line-based keyboard input
    • config.py: typed runtime config (TOML + env overrides)
  • src/devialetctl/interfaces
    • cli.py: argparse and command wiring
    • topology.py: topology tree building/rendering and system-name target selection
  • Compatibility shims
    • src/devialetctl/api.py
    • src/devialetctl/discovery.py
    • src/devialetctl/cli.py

Component Flow

flowchart LR
  subgraph interfacesLayer [Interfaces]
    cliInterface[CLIInterface]
    daemonInterface[DaemonInterface]
  end

  subgraph appLayer [Application]
    eventRouter[EventRouter]
    volumeService[VolumeService]
    daemonRunner[DaemonRunner]
  end

  subgraph domainLayer [Domain]
    inputEvent[InputEvent]
    repeatPolicy[EventPolicy]
  end

  subgraph infraLayer [Infrastructure]
    cecAdapter[CecKernelAdapter]
    keyboardAdapter[KeyboardAdapter]
    httpGateway[DevialetHttpGateway]
    mdnsGateway[MdnsDiscoveryGateway]
    upnpGateway[UpnpDiscoveryGateway]
    configLoader[ConfigLoader]
  end

  devialetSpeaker[DevialetSpeaker]
  tvRemote[TVRemote] --> cecAdapter
  keyboardInput[KeyboardTTY] --> keyboardAdapter
  cecAdapter --> daemonRunner
  keyboardAdapter --> daemonRunner
  daemonInterface --> daemonRunner
  daemonRunner --> eventRouter
  eventRouter --> repeatPolicy
  eventRouter --> inputEvent
  eventRouter --> volumeService
  cliInterface --> volumeService
  volumeService --> httpGateway
  httpGateway --> devialetSpeaker
  devialetSpeaker --> mdnsGateway
  devialetSpeaker --> upnpGateway
  daemonInterface --> mdnsGateway
  daemonInterface --> upnpGateway
  cliInterface --> mdnsGateway
  cliInterface --> upnpGateway
  configLoader --> daemonInterface
  configLoader --> cliInterface
Loading

Command Surface

  • Direct control:
    • list, tree, systems, getvol, setvol, volup, voldown, mute
  • Daemon:
    • daemon --input cec
    • daemon --input keyboard
  • Target selection:
    • global args (--ip, --port, --system, --discover-timeout)
    • daemon-specific CEC args (--cec-device, --cec-osd-name, --cec-vendor-compat)
    • --ip and --system are mutually exclusive
    • list and tree reject --ip / --system because they are discovery-only

Runtime Behavior

  • Discovery uses merged mDNS + UPnP:
    • mDNS path: _whatsup._tcp.local browsing.
    • UPnP path: SSDP M-SEARCH with target urn:schemas-upnp-org:device:MediaRenderer:2.
    • targets are deduplicated by (address, port, base_path) before selection.
  • tree command builds a topology from "current" endpoints:
    • per discovered dispatcher: /devices/current
    • per inferred system: /systems/current
    • groups are rebuilt from groupId/systemId relationships.
    • system-targeted selection (--system) prefers isSystemLeader when available.
  • Base path is normalized defensively:
    • None, "", / -> /ipcontrol/v1
    • missing leading slash is corrected.
  • Relative volume operations are precise:
    • volup -> current + 1
    • voldown -> current - 1
    • fallback to native async volumeUp/volumeDown endpoint if get/set path fails.
  • CEC daemon path is async-only:
    • CecKernelAdapter.async_events() reads kernel CEC frames
    • external Devialet watcher polls volume/mute and reports changes to TV
    • watcher polling and CEC command handling are serialized with an async lock
    • watcher is temporarily suspended while handling inbound CEC push commands
  • Daemon policy protects API/device from repeated bursts:
    • dedupe window
    • minimum emit interval
    • retry/backoff loop for adapter failures.

Concurrency Model

CEC daemon runtime has two concurrent async producers:

  • CEC receiver stream (async_events) for inbound TV commands
  • external watcher polling Devialet HTTP state

To avoid race conditions, both paths serialize all Devialet I/O and cache mutations with a shared async lock.
Additionally, CEC handling temporarily suspends watcher polling while a push/update is in progress.

sequenceDiagram
  participant TV as Samsung TV (CEC)
  participant CEC as CecKernelAdapter.async_events
  participant D as DaemonRunner
  participant W as External Watcher
  participant API as Devialet HTTP API

  W->>D: tick (every N ms)
  D->>D: acquire io_lock
  D->>API: GET volume + mute
  API-->>D: current state
  D->>D: release io_lock

  TV->>CEC: 0x89/0x44/0x73 command
  CEC-->>D: InputEvent
  D->>D: suspend watcher window
  D->>D: acquire io_lock
  D->>API: POST/GET for command handling
  D->>TV: TX REPORT_AUDIO_STATUS (and optional vendor response)
  D->>D: release io_lock

  W->>D: next tick during suspend
  D-->>W: skip poll (no GET)
Loading

Configuration Model

Config source priority:

  1. CLI global target args (when provided)
  2. Environment variables (DEVIALETCTL_IP, DEVIALETCTL_PORT, DEVIALETCTL_BASE_PATH)
  3. TOML config (~/.config/devialetctl/config.toml)
  4. built-in defaults

Relevant settings:

  • target: ip, port, base_path, discover_timeout
  • daemon: cec_device, cec_osd_name, cec_vendor_compat, reconnect_delay_s, log_level
  • policy: dedupe_window_s, min_interval_s

Compatibility Guarantees

  • Existing imports continue to work:
    • devialetctl.api.DevialetClient
    • devialetctl.discovery.discover
    • devialetctl.cli.main
  • Existing CLI commands remain available with same intent.

Testing Strategy

Current test suite covers:

  • CEC parser behavior
  • keyboard parser behavior
  • event policy dedupe/rate-limit
  • daemon routing in CEC and keyboard modes
  • CLI regressions (including daemon argument handling and tree --json)
  • mDNS + UPnP discovery integration
  • base-path normalization
  • compatibility wrappers

Extension Roadmap

Planned next adapters can reuse the same event pipeline:

  • IR adapter (LIRC/GPIO)
  • Home Assistant adapter (service calls or MQTT/webhook bridge)
  • HDMI-CEC keymap tuning (long press, repeated key policy)
  • optional --dry-run mode for safe observability