Home Assistant Holo-Series Integration

  • Home |
  • Home Assistant Holo-Series Integration
微信图片 20251127134942

Home Assistant Holo-Series Integration

Home Assistant Holo-Series Integration (Full Deployable Version)

 

1. Plugin Overview

 

Developed based on the latest Holo-Series Streamer Web API documentation, this integration supports all Holo-Series devices including HoloWhas, HoloOne AMP, and HoloOne Pro Max. It enables core functionalities such as device monitoring, input source management, and zone audio control, fully adapting to all implemented APIs.

2. File Directory Structure

 
plaintext
 
custom_components/
└── holoseries/
    ├── __init__.py          # Initialization & core API interactions
    ├── manifest.json        # Plugin metadata
    ├── sensor.py            # Device status sensor entities
    ├── media_player.py      # Zone audio control entities
    ├── services.yaml        # Service definitions
    ├── config_flow.py       # UI configuration flow
    ├── const.py             # Constant definitions
    └── translations/
        └── en.json          # English translations (optional)

3. Core File Implementations

3.1 const.py (Constant Definitions)

 
python
 
DOMAIN = "holoseries"
DEFAULT_PORT = 80
DEFAULT_SCAN_INTERVAL = 30

# Configuration constants
CONF_HOST = "host"
CONF_PORT = "port"
CONF_SCAN_INTERVAL = "scan_interval"
CONF_DEVICE_FILTERS = "device_filters"
CONF_INPUT_CLASS_FILTER = "input_class_filter"
CONF_ZONE_FILTERS = "zone_filters"

# Input class constants
INPUT_CLASSES = {
    0: "General Input",
    1: "Zone-based (Spotify)",
    2: "Zone-based (Airplay2)"
}

# Supported input types
SUPPORTED_INPUT_TYPES = [
    "Airplay", "DLNA", "Spotify", "USB", "Analog", "RCA", "ARC", "Optical", "Bluetooth"
]

# EQ bands (10 bands)
EQ_BANDS = [
    "31.25Hz", "62.5Hz", "125Hz", "250Hz", "500Hz",
    "1KHz", "2KHz", "4KHz", "8KHz", "16KHz"
]

3.2 manifest.json (Plugin Metadata)

 
json
 
{
  "domain": "holoseries",
  "name": "Holo-Series Streamer Integration",
  "version": "1.0.0",
  "documentation": "https://github.com/example/holoseries-homeassistant",
  "requirements": ["requests>=2.31.0"],
  "dependencies": [],
  "codeowners": ["@your_username"],
  "config_flow": true,
  "iot_class": "local_polling",
  "homeassistant": "2023.1.0"
}

3.3 init.py (Initialization & API Interactions)

 
python
 
import requests
import logging
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL
from .const import (
    DOMAIN, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL,
    CONF_DEVICE_FILTERS, CONF_INPUT_CLASS_FILTER, CONF_ZONE_FILTERS
)

_LOGGER = logging.getLogger(__name__)

async def async_setup(hass: HomeAssistant, config: dict) -> bool:
    hass.data.setdefault(DOMAIN, {})
    if DOMAIN not in config:
        return True
    
    # Read configuration
    conf = config[DOMAIN]
    host = conf[CONF_HOST]
    port = conf.get(CONF_PORT, DEFAULT_PORT)
    scan_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
    device_filters = conf.get(CONF_DEVICE_FILTERS, [])
    input_class_filter = conf.get(CONF_INPUT_CLASS_FILTER)
    zone_filters = conf.get(CONF_ZONE_FILTERS, [])
    
    # Initialize API client
    api = HoloSeriesAPI(host, port)
    hass.data[DOMAIN]["api"] = api
    hass.data[DOMAIN]["config"] = {
        "device_filters": device_filters,
        "input_class_filter": input_class_filter,
        "zone_filters": zone_filters,
        "scan_interval": scan_interval
    }
    
    # Load platforms
    hass.helpers.discovery.load_platforms(config, DOMAIN, ["sensor", "media_player"])
    return True

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    host = entry.data[CONF_HOST]
    port = entry.data.get(CONF_PORT, DEFAULT_PORT)
    api = HoloSeriesAPI(host, port)
    hass.data[DOMAIN]["api"] = api
    hass.data[DOMAIN]["config"] = {
        "device_filters": entry.options.get(CONF_DEVICE_FILTERS, []),
        "input_class_filter": entry.options.get(CONF_INPUT_CLASS_FILTER),
        "zone_filters": entry.options.get(CONF_ZONE_FILTERS, []),
        "scan_interval": entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
    }
    hass.config_entries.async_setup_platforms(entry, ["sensor", "media_player"])
    return True

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    unload_ok = await hass.config_entries.async_unload_platforms(entry, ["sensor", "media_player"])
    if unload_ok:
        hass.data.pop(DOMAIN)
    return unload_ok

class HoloSeriesAPI:
    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port
        self.base_url = f"http://{host}:{port}/api/v3"
        self.session = requests.Session()

    def _request(self, method: str, endpoint: str, json=None, params=None) -> requests.Response:
        """Generic request method"""
        url = f"{self.base_url}{endpoint}"
        try:
            response = self.session.request(
                method=method, url=url, json=json, params=params, timeout=10
            )
            response.raise_for_status()
            return response
        except requests.exceptions.RequestException as e:
            _LOGGER.error(f"API request failed {method} {url}: {str(e)}")
            raise

    # Device-related APIs
    def get_devices(self) -> list:
        """Get all device IDs"""
        response = self._request("GET", "/devices/")
        return response.json().get("device_ids", [])

    def get_device_info(self, device_id: str) -> dict:
        """Get single device details"""
        response = self._request("GET", f"/devices/{device_id}/attributes")
        return response.json()

    def get_device_metrics(self, device_id: str) -> dict:
        """Get device performance metrics"""
        response = self._request("GET", f"/devices/{device_id}/metrics")
        return response.json()

    def get_device_connection(self, device_id: str) -> dict:
        """Get device connection status"""
        response = self._request("GET", f"/devices/{device_id}/connection")
        return response.json()

    def reboot_device(self, device_id: str) -> None:
        """Reboot device"""
        self._request("POST", f"/devices/{device_id}/reboot")

    # Input-related APIs
    def get_inputs(self, class_filter: int = None) -> list:
        """Get all input IDs"""
        params = {"class_filter": class_filter} if class_filter is not None else None
        response = self._request("GET", "/inputs/", params=params)
        return response.json().get("input_ids", [])

    def get_input_details(self, input_id: str) -> dict:
        """Get input details"""
        response = self._request("GET", f"/inputs/{input_id}")
        return response.json()

    def set_input_type(self, input_id: str, input_type: str) -> None:
        """Set input type"""
        self._request("PUT", f"/inputs/{input_id}/type", json={"type": input_type})

    def set_input_volume(self, input_id: str, volume: int) -> None:
        """Set input volume"""
        self._request("PUT", f"/inputs/{input_id}/volume", json={"volume": volume})

    # Zone-related APIs
    def get_zones(self) -> list:
        """Get all zone IDs"""
        response = self._request("GET", "/zones/")
        return response.json().get("zone_ids", [])

    def get_zone_details(self, zone_id: str) -> dict:
        """Get zone details"""
        response = self._request("GET", f"/zones/{zone_id}")
        return response.json()

    def set_zone_volume(self, zone_id: str, volume: int) -> None:
        """Set zone volume"""
        self._request("PUT", f"/zones/{zone_id}/volume", json={"volume": volume})

    def set_zone_mute(self, zone_id: str, mute: bool) -> None:
        """Set zone mute"""
        self._request("PUT", f"/zones/{zone_id}/mute", json={"enable": mute})

    def set_zone_active_input(self, zone_id: str, input_id: str) -> None:
        """Set zone active input"""
        self._request("PUT", f"/zones/{zone_id}/input/active", json={"input_id": input_id})

    def set_zone_eq(self, zone_id: str, eq_values: list) -> None:
        """Set zone equalizer"""
        self._request("PUT", f"/zones/{zone_id}/volume_eq", json={"volume_eq": eq_values})

3.4 sensor.py (Device Status Sensors)

python
 
 
import logging
from homeassistant.helpers.entity import Entity
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
from .const import DOMAIN, EQ_BANDS

_LOGGER = logging.getLogger(__name__)

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities):
    api = hass.data[DOMAIN]["api"]
    config = hass.data[DOMAIN]["config"]
    device_filters = config["device_filters"]
    
    # Get all devices
    devices = await hass.async_add_executor_job(api.get_devices)
    # Apply device filters
    if device_filters:
        devices = [dev for dev in devices if dev in device_filters]
    
    sensors = []
    for device_id in devices:
        # Performance metric sensors
        sensors.extend([
            HoloDeviceMetricSensor(api, device_id, "cpu_usage", "CPU Usage", PERCENTAGE),
            HoloDeviceMetricSensor(api, device_id, "ram_usage", "RAM Usage", PERCENTAGE),
            HoloDeviceMetricSensor(api, device_id, "disk_usage", "Disk Usage", PERCENTAGE),
            HoloDeviceMetricSensor(api, device_id, "internal_temp", "Internal Temperature", TEMP_CELSIUS)
        ])
        # Connection status sensor
        sensors.append(HoloDeviceConnectionSensor(api, device_id))
    
    async_add_entities(sensors, True)

class HoloDeviceMetricSensor(Entity):
    def __init__(self, api, device_id: str, metric_type: str, name: str, unit: str):
        self.api = api
        self.device_id = device_id
        self.metric_type = metric_type
        self._name = f"Holo-Series {device_id} {name}"
        self._unit_of_measurement = unit
        self._state = None

    @property
    def unique_id(self) -> str:
        return f"holoseries_{self.device_id}_{self.metric_type}"

    @property
    def name(self) -> str:
        return self._name

    @property
    def state(self):
        return self._state

    @property
    def unit_of_measurement(self) -> str:
        return self._unit_of_measurement

    def update(self):
        try:
            metrics = self.api.get_device_metrics(self.device_id)
            self._state = metrics.get(self.metric_type, 0)
        except Exception as e:
            self._state = None
            _LOGGER.error(f"Failed to update {self.name}: {str(e)}")

class HoloDeviceConnectionSensor(Entity):
    def __init__(self, api, device_id: str):
        self.api = api
        self.device_id = device_id
        self._name = f"Holo-Series {device_id} Connection Status"
        self._state = None
        self._extra_state_attributes = {}

    @property
    def unique_id(self) -> str:
        return f"holoseries_{self.device_id}_connection"

    @property
    def name(self) -> str:
        return self._name

    @property
    def state(self):
        return self._state

    @property
    def extra_state_attributes(self) -> dict:
        return self._extra_state_attributes

    def update(self):
        try:
            conn_data = self.api.get_device_connection(self.device_id)
            conn_type = conn_data.get("type", "none")
            self._state = "connected" if conn_type != "none" else "disconnected"
            self._extra_state_attributes = {
                "ip_address": conn_data.get("ip_address", "Unknown"),
                "signal_strength": conn_data.get("signal_strength", 0),
                "ssid": conn_data.get("ssid", "Unknown"),
                "uptime": conn_data.get("uptime", 0)
            }
        except Exception as e:
            self._state = "unavailable"
            self._extra_state_attributes = {}
            _LOGGER.error(f"Failed to update {self.name}: {str(e)}")
 

3.5 media_player.py (Zone Audio Control)

 python
 
 
import logging
from homeassistant.components.media_player import (
    MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState
)
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, SUPPORTED_INPUT_TYPES, EQ_BANDS

_LOGGER = logging.getLogger(__name__)

class HoloSeriesZoneMediaPlayer(MediaPlayerEntity):
    _attr_supported_features = (
        MediaPlayerEntityFeature.VOLUME_SET
        | MediaPlayerEntityFeature.VOLUME_MUTE
        | MediaPlayerEntityFeature.SELECT_SOURCE
    )

    def __init__(self, api, zone_id: str):
        self.api = api
        self.zone_id = zone_id
        self._name = None
        self._state = MediaPlayerState.OFF
        self._volume = 0
        self._muted = False
        self._active_input = ""
        self._available_inputs = []
        self._eq_values = [0] * 10

    @property
    def unique_id(self) -> str:
        return f"holoseries_zone_{self.zone_id}"

    @property
    def name(self) -> str:
        return self._name

    @property
    def state(self) -> MediaPlayerState:
        return self._state

    @property
    def volume_level(self) -> float:
        return self._volume / 100

    @property
    def is_volume_muted(self) -> bool:
        return self._muted

    @property
    def source(self) -> str:
        return self._active_input

    @property
    def source_list(self) -> list:
        return self._available_inputs

    @property
    def extra_state_attributes(self) -> dict:
        return {f"eq_{EQ_BANDS[i]}": self._eq_values[i] for i in range(10)}

    def set_volume_level(self, volume: float) -> None:
        """Set volume (convert 0.0-1.0 to 0-100)"""
        volume_int = int(volume * 100)
        self.api.set_zone_volume(self.zone_id, volume_int)
        self._volume = volume_int

    def mute_volume(self, mute: bool) -> None:
        """Mute/unmute"""
        self.api.set_zone_mute(self.zone_id, mute)
        self._muted = mute

    def select_source(self, source: str) -> None:
        """Select active input source"""
        if source in self._available_inputs:
            self.api.set_zone_active_input(self.zone_id, source)
            self._active_input = source
            self._state = MediaPlayerState.ON

    def update(self) -> None:
        """Update zone status"""
        try:
            zone_data = self.api.get_zone_details(self.zone_id)
            self._name = zone_data.get("name", self.zone_id)
            self._volume = zone_data.get("volume", 0)
            self._muted = zone_data.get("muted", False)
            self._active_input = zone_data.get("active_input", "")
            self._available_inputs = zone_data.get("input", [])
            self._eq_values = zone_data.get("volume_eq", [0] * 10)
            
            # Determine device state
            self._state = MediaPlayerState.ON if zone_data.get("enabled", True) else MediaPlayerState.OFF
            if self._active_input:
                self._state = MediaPlayerState.ON
        except Exception as e:
            self._state = MediaPlayerState.UNAVAILABLE
            _LOGGER.error(f"Failed to update zone {self.zone_id} status: {str(e)}")

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities):
    api = hass.data[DOMAIN]["api"]
    config = hass.data[DOMAIN]["config"]
    zone_filters = config["zone_filters"]
    
    # Get all zones
    zones = await hass.async_add_executor_job(api.get_zones)
    # Apply zone filters
    if zone_filters:
        zones = [zone for zone in zones if zone in zone_filters]
    
    async_add_entities([HoloSeriesZoneMediaPlayer(api, zone_id) for zone_id in zones], True)
 

3.6 services.yaml (Service Definitions)

yaml
 
 
# Device-related services
reboot_device:
  name: Reboot Holo-Series Device
  description: Reboot a specific Holo-Series device
  fields:
    device_id:
      name: Device ID
      required: true
      example: "DynamoAmp-DEV99"
      selector:
        text:

# Input-related services
set_input_type:
  name: Set Input Type
  description: Set input type for a general class input
  fields:
    input_id:
      name: Input ID
      required: true
      example: "1"
      selector:
        text:
    type:
      name: Input Type
      required: true
      example: "USB"
      selector:
        select:
          options:
            - "Airplay"
            - "DLNA"
            - "Spotify"
            - "USB"
            - "Analog"
            - "RCA"
            - "ARC"
            - "Optical"
            - "Bluetooth"

set_input_volume:
  name: Set Input Volume
  description: Set volume for USB/RCA/Optical inputs (0-100)
  fields:
    input_id:
      name: Input ID
      required: true
      example: "1"
      selector:
        text:
    volume:
      name: Volume
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1

# Zone-related services
set_zone_eq:
  name: Set Zone Equalizer
  description: Set 10-band EQ for a zone (0-100 per band)
  fields:
    zone_id:
      name: Zone ID
      required: true
      example: "DynamoAmp-DEV99-Z1"
      selector:
        text:
    eq_31.25Hz:
      name: 31.25Hz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_62.5Hz:
      name: 62.5Hz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_125Hz:
      name: 125Hz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_250Hz:
      name: 250Hz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_500Hz:
      name: 500Hz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_1KHz:
      name: 1KHz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_2KHz:
      name: 2KHz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_4KHz:
      name: 4KHz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_8KHz:
      name: 8KHz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
    eq_16KHz:
      name: 16KHz
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          step: 1
 
 

3.7 config_flow.py (UI Configuration Flow)

 python
 
 
import voluptuous as vol
import requests
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from .const import (
    DOMAIN, CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL,
    CONF_DEVICE_FILTERS, CONF_INPUT_CLASS_FILTER, CONF_ZONE_FILTERS,
    DEFAULT_PORT, DEFAULT_SCAN_INTERVAL
)

class HoloSeriesConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    VERSION = 1

    async def async_step_user(self, user_input=None) -> FlowResult:
        errors = {}
        
        if user_input is not None:
            # Verify connection
            try:
                host = user_input[CONF_HOST]
                port = user_input[CONF_PORT]
                # Simple connection test
                response = await self.hass.async_add_executor_job(
                    requests.get, f"http://{host}:{port}/api/v3/devices/", timeout=5
                )
                response.raise_for_status()
            except Exception:
                errors["base"] = "cannot_connect"
            else:
                await self.async_set_unique_id(f"holoseries_{host}_{port}")
                self._abort_if_unique_id_configured()
                return self.async_create_entry(title=f"Holo-Series ({host})", data=user_input)
        
        # Configuration form
        data_schema = vol.Schema({
            vol.Required(CONF_HOST): str,
            vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
            vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int,
        })
        
        return self.async_show_form(
            step_id="user", data_schema=data_schema, errors=errors
        )

    async def async_step_options(self, user_input=None) -> FlowResult:
        """Configuration options"""
        if user_input is not None:
            return self.async_create_entry(title="", data=user_input)
        
        # Get current configuration
        config = self.config_entry.options or {}
        data_schema = vol.Schema({
            vol.Optional(CONF_DEVICE_FILTERS, default=config.get(CONF_DEVICE_FILTERS, [])): vol.All(
                vol.Split(","), [str]
            ),
            vol.Optional(CONF_INPUT_CLASS_FILTER, default=config.get(CONF_INPUT_CLASS_FILTER)): vol.In([None, 0, 1, 2]),
            vol.Optional(CONF_ZONE_FILTERS, default=config.get(CONF_ZONE_FILTERS, [])): vol.All(
                vol.Split(","), [str]
            ),
        })
        
        return self.async_show_form(step_id="options", data_schema=data_schema)
 

4. Configuration & Deployment Steps

4.1 Basic Configuration (YAML Method)

 
Add to configuration.yaml:
 
yaml
 
 
holoseries:
  host: "192.168.0.115"  # Holo-Series server IP
  port: 80
  scan_interval: 30
  device_filters: ["DynamoAmp-DEV99"]  # Optional, specify devices
  input_class_filter: 0  # Optional, filter input class
  zone_filters: ["DynamoAmp-DEV99-Z1", "DynamoAmp-DEV99-Z2"]  # Optional, specify zones
 

4.2 UI Configuration Method

 
  1. Restart Home Assistant, go to “Settings > Devices & Services > Add Integration”;
  2. Search for “Holo-Series Streamer”, enter server IP and port to complete initial configuration;
  3. To adjust filters, go to the integration’s “Options” page.

4.3 Deployment Verification

 
  1. Copy the holoseries folder to Home Assistant’s custom_components directory;
  2. Restart Home Assistant service;
  3. Test plugin services (e.g., holoseries.reboot_device) in “Developer Tools > Services”;
  4. Add “Media Player” and “Sensor” cards to the dashboard to view device status and control interface.
 
0
YOUR CART
  • No products in the cart.