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: 13.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
- Restart Home Assistant, go to “Settings > Devices & Services > Add Integration”;
- Search for “Holo-Series Streamer”, enter server IP and port to complete initial configuration;
- To adjust filters, go to the integration’s “Options” page.
4.3 Deployment Verification
- Copy the
holoseriesfolder to Home Assistant’scustom_componentsdirectory; - Restart Home Assistant service;
- Test plugin services (e.g.,
holoseries.reboot_device) in “Developer Tools > Services”; - Add “Media Player” and “Sensor” cards to the dashboard to view device status and control interface.





