Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
143 commits
Select commit Hold shift + click to select a range
ce837b9
Add HiVi Speaker integration
swansmart Mar 11, 2026
defeaef
Add HiVi Speaker integration for tests
swansmart Mar 11, 2026
2f67448
Refactor config entry removal and device cleanup
swansmart Mar 12, 2026
4bc2bae
Refactor device data cleanup and listener management
swansmart Mar 12, 2026
2221a52
Update post-initialization method in device.py
swansmart Mar 12, 2026
2b30d39
Refactor callback handling and improve operation management
swansmart Mar 12, 2026
0e253ab
Update requirements for hivico to exact version
swansmart Mar 12, 2026
bf15185
Refactor directory scanning and remove unused code
swansmart Mar 12, 2026
85a953b
Refactor service registration for speaker devices
swansmart Mar 12, 2026
5f40524
Refactor device registry handling in HIVI switch
swansmart Mar 12, 2026
85aa187
Update media_player.py
swansmart Mar 12, 2026
2912df4
Implement error handling for missing master device
swansmart Mar 12, 2026
a81487b
Handle missing device_info in discovery scheduler
swansmart Mar 12, 2026
a0157c5
Refactor device data methods for clarity and efficiency
swansmart Mar 12, 2026
cc64cec
Refactor fields to use default factories in device model
swansmart Mar 12, 2026
541852f
Refactor device management and update logging
swansmart Mar 12, 2026
5d94503
Implement persistent storage for device data
swansmart Mar 12, 2026
e00fa02
Refactor options flow tests to use AsyncMock
swansmart Mar 12, 2026
98bf04d
Delete homeassistant/components/hivi_speaker/translations/en.json
swansmart Mar 12, 2026
46ef76f
Delete homeassistant/components/hivi_speaker/translations/zh-Hans.json
swansmart Mar 12, 2026
7766b26
Add files via upload
swansmart Mar 12, 2026
f15f3d6
Add hivico dependency for hivi_speaker component
swansmart Mar 12, 2026
c9102ca
Remove unused switch_entities attribute
swansmart Mar 12, 2026
9dd3630
Implement delayed save for device data registry
swansmart Mar 12, 2026
3a8ed7c
Clean up device_manager.py by removing unused code
swansmart Mar 12, 2026
f2dcb15
Refactor discovery scheduler for improved readability
swansmart Mar 12, 2026
dcb7ca2
Replace asyncio.create_task with hass.async_create_task
swansmart Mar 12, 2026
286da40
Refactor logging and XML handling in media player
swansmart Mar 12, 2026
518ea8f
Improve logging for master and slave device lookups
swansmart Mar 12, 2026
388a023
Refactor exception handling and improve docstrings
swansmart Mar 12, 2026
b9cc3ca
Update docstrings for HIVI speaker config flow
swansmart Mar 12, 2026
e5d896b
Add SIGNAL_DEVICE_STATUS_UPDATED constant
swansmart Mar 12, 2026
ddb7e9d
Refactor device data registry for type hints
swansmart Mar 12, 2026
7ed0ff2
Refactor device manager for HiVi Speaker integration
swansmart Mar 12, 2026
09f84f6
Refactor device.py to use optional type hints
swansmart Mar 12, 2026
b5d7014
Refactor discovery scheduler for HiVi Speaker
swansmart Mar 12, 2026
f4911d3
Refactor type hints and improve docstrings
swansmart Mar 12, 2026
52b6c40
Remove dlna_dms dependency from manifest.json
swansmart Mar 12, 2026
2b50d84
Delete homeassistant/components/hivi_speaker/media_player.py
swansmart Mar 12, 2026
1761815
Refactor ha_device_id assignment for clarity
swansmart Mar 12, 2026
604d0e2
Reformat services.yaml for HiVi Speaker integration
swansmart Mar 12, 2026
f32171a
Refactor HiVi Speaker switch integration code
swansmart Mar 12, 2026
e34c895
Update homeassistant/components/hivi_speaker/manifest.json
swansmart Mar 12, 2026
e1c6101
Refactor device removal process in HIVI speaker
swansmart Mar 12, 2026
b8bca57
Refactor device removal methods for clarity
swansmart Mar 12, 2026
4916a18
Disable debug logging for HivicoClient
swansmart Mar 12, 2026
947e5ee
Refactor online/offline device count calculation
swansmart Mar 12, 2026
3648c60
Add method to get connection status counts
swansmart Mar 12, 2026
14d2cfe
Initialize _attr_is_on to False in switch.py
swansmart Mar 12, 2026
43df0a1
Remove persistent device data storage on entry removal
swansmart Mar 12, 2026
08a5022
Refactor discovery task and update device dict handling
swansmart Mar 12, 2026
38a1e63
Clarify comment for handling all online devices
swansmart Mar 12, 2026
e2c314d
Update group_coordinator.py
swansmart Mar 12, 2026
6adbee8
Update discovery_scheduler.py
swansmart Mar 13, 2026
9616745
Update __init__.py
swansmart Mar 13, 2026
68a0a1b
Update config_flow.py
swansmart Mar 13, 2026
9434023
Update device_data_registry.py
swansmart Mar 13, 2026
6b3e028
Update device_manager.py
swansmart Mar 13, 2026
cb99f06
Update device.py
swansmart Mar 13, 2026
e3a518b
Update discovery_scheduler.py
swansmart Mar 13, 2026
46cfa07
Update group_coordinator.py
swansmart Mar 13, 2026
49c6697
Update switch.py
swansmart Mar 13, 2026
1e24e68
Create quality_scale.yaml
swansmart Mar 13, 2026
fad531f
Update test_config_flow.py
swansmart Mar 16, 2026
c2fc6cc
Remove the use of service functions.
swansmart Mar 18, 2026
9037f3e
Remove the use of service functions
swansmart Mar 18, 2026
9bf6fba
Remove the use of service functions
swansmart Mar 18, 2026
a160a80
Delete services.yaml
swansmart Mar 18, 2026
da97e59
Delete services.py
swansmart Mar 18, 2026
23474de
Add hivico for hivi_speaker
swansmart Mar 18, 2026
b35e6e8
Update test_config_flow.py
swansmart Mar 18, 2026
1fe7e85
Update __init__.py
swansmart Mar 18, 2026
6113a60
Update config_flow.py
swansmart Mar 18, 2026
3d5845b
Update device_data_registry.py
swansmart Mar 18, 2026
8dafe16
Update device_manager.py
swansmart Mar 18, 2026
46ca1a4
Update device_manager.py
swansmart Mar 18, 2026
525af44
Update discovery_scheduler.py
swansmart Mar 18, 2026
8126300
Update device_manager.py
swansmart Mar 18, 2026
e78e623
Update group_coordinator.py
swansmart Mar 18, 2026
f281b41
Update manifest.json
swansmart Mar 18, 2026
c8a288a
Update quality_scale.yaml
swansmart Mar 18, 2026
49aa977
Update strings.json
swansmart Mar 18, 2026
f2d95d5
Update switch.py
swansmart Mar 18, 2026
438b47b
Update config_flow.py
swansmart Mar 18, 2026
b6df665
Update manifest.json
swansmart Mar 18, 2026
802f854
Update switch.py
swansmart Mar 18, 2026
f3b3786
Update manifest.json
swansmart Mar 18, 2026
15e751c
Update switch.py
swansmart Mar 18, 2026
279ee6a
Update __init__.py
swansmart Mar 18, 2026
80fa817
Update device_manager.py
swansmart Mar 18, 2026
4c75005
Update discovery_scheduler.py
swansmart Mar 18, 2026
b614bda
Update group_coordinator.py
swansmart Mar 18, 2026
3b6874d
Update switch.py
swansmart Mar 18, 2026
8231948
Update __init__.py
swansmart Mar 18, 2026
b70ca70
Update discovery_scheduler.py
swansmart Mar 18, 2026
42cfbb9
Update group_coordinator.py
swansmart Mar 18, 2026
f3800a3
Update __init__.py
swansmart Mar 18, 2026
e09f912
Update device_manager.py
swansmart Mar 18, 2026
bd5beaf
Create switch_hub.py
swansmart Mar 18, 2026
68fae9b
Update discovery_scheduler.py
swansmart Mar 18, 2026
5f6d96f
Update group_coordinator.py
swansmart Mar 18, 2026
d10e349
Update switch.py
swansmart Mar 18, 2026
a4e5e2d
Update __init__.py
swansmart Mar 18, 2026
cd0f3f3
Update device_manager.py
swansmart Mar 18, 2026
282ee85
Update group_coordinator.py
swansmart Mar 18, 2026
d696262
Add CODEOWNERS for hivi_speaker component
swansmart Apr 3, 2026
88dfa19
Add HiVi Speaker integration to integrations.json
swansmart Apr 3, 2026
ae7128c
Add 'hivi_speaker' to config flows
swansmart Apr 3, 2026
a6ece9e
Update homeassistant/components/hivi_speaker/discovery_scheduler.py
swansmart Apr 3, 2026
43f55a8
Update tests/components/hivi_speaker/conftest.py
swansmart Apr 3, 2026
61a8228
Remove default parameter in get_device_dict_by_ha_device_id
swansmart Apr 3, 2026
f6e3047
Refactor device data registry and remove listeners
swansmart Apr 3, 2026
130dda8
Remove default=None from get_device_dict_by_ha_device_id calls
swansmart Apr 3, 2026
42d0d3a
Update homeassistant/components/hivi_speaker/switch.py
swansmart Apr 3, 2026
a67f30a
Update tests/components/hivi_speaker/test_config_flow.py
swansmart Apr 3, 2026
699da7d
Fix typo in variable name from 'entiy' to 'entity'
swansmart Apr 3, 2026
bfe6481
Change return type of mock_setup_entry fixture
swansmart Apr 3, 2026
51f2bd6
Update homeassistant/components/hivi_speaker/device_manager.py
swansmart Apr 3, 2026
ce11c96
Merge branch 'dev' into add-hivi-speaker-integration
swansmart Apr 3, 2026
3de4cd0
modify format
swansmart Apr 3, 2026
f0428b8
Update discovery_scheduler.py
swansmart Apr 3, 2026
4f8e115
Update homeassistant/components/hivi_speaker/device.py
swansmart Apr 3, 2026
9f9e2c8
Update homeassistant/components/hivi_speaker/switch.py
swansmart Apr 3, 2026
9339a07
Update homeassistant/components/hivi_speaker/device.py
swansmart Apr 3, 2026
ae4c981
update
swansmart Apr 7, 2026
715a55c
Add already_configured
swansmart Apr 7, 2026
2d14cba
update format
swansmart Apr 7, 2026
d85009c
Merge branch 'home-assistant:dev' into add-hivi-speaker-integration
swansmart Apr 7, 2026
136c776
add tz=UTC
swansmart Apr 7, 2026
1abe0c5
Merge branch 'add-hivi-speaker-integration' of https://github.com/swa…
swansmart Apr 7, 2026
a249e6d
Update device_manager.py
swansmart Apr 7, 2026
a02e313
update device_manager.py format
swansmart Apr 7, 2026
7e87200
update or add test file
swansmart Apr 7, 2026
97e0cd0
update test file
swansmart Apr 7, 2026
338a10b
add or update test file
swansmart Apr 8, 2026
da6078d
update format
swansmart Apr 8, 2026
d2a119e
update format
swansmart Apr 8, 2026
d4ce748
add docstring in public function
swansmart Apr 8, 2026
473a5eb
Merge branch 'home-assistant:dev' into add-hivi-speaker-integration
swansmart Apr 9, 2026
2193ac7
Added verification checks
swansmart Apr 9, 2026
8d8ea03
Merge branch 'add-hivi-speaker-integration' of https://github.com/swa…
swansmart Apr 9, 2026
7d336ba
Merge branch 'dev' into add-hivi-speaker-integration
swansmart Apr 9, 2026
892a21d
Merge branch 'dev' into add-hivi-speaker-integration
swansmart Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

154 changes: 154 additions & 0 deletions homeassistant/components/hivi_speaker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""The hivi_speaker integration."""

from __future__ import annotations

import logging
from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.storage import Store

from .const import DOMAIN
from .device import HIVIDevice
from .device_manager import HIVIDeviceManager

_LOGGER = logging.getLogger(__name__)


PLATFORMS = ["switch"]


async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the config entry."""
device_manager = HIVIDeviceManager(hass, config_entry)
await device_manager.async_setup()

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(config_entry.entry_id, {})
hass.data[DOMAIN][config_entry.entry_id]["device_manager"] = device_manager

await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload config entry and clean up related resources."""
_LOGGER.debug("Starting to unload config entry %s", entry.entry_id)

data = hass.data.get(DOMAIN, {}).get(entry.entry_id, {})

device_manager = data.get("device_manager")
if device_manager:
try:
await device_manager.async_cleanup()
except Exception:
_LOGGER.exception("Exception occurred while cleaning up device_manager")

try:
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
except Exception:
_LOGGER.exception("Failed to call async_unload_platforms")
unload_ok = False

try:
if DOMAIN in hass.data and entry.entry_id in hass.data[DOMAIN]:
hass.data[DOMAIN].pop(entry.entry_id, None)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN, None)
except Exception:
_LOGGER.exception("Exception occurred while cleaning up hass.data")

if not unload_ok:
_LOGGER.error(
"Platform unloading partially failed (unload_ok=False), there may be residual entities or platforms"
)
else:
_LOGGER.debug("Config entry %s unloaded successfully", entry.entry_id)

return unload_ok


async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove config entry and clean up devices and persistent storage."""
_LOGGER.debug("Removing config entry %s – cleaning up devices", entry.entry_id)

try:
dev_reg = dr.async_get(hass)
for device in list(dev_reg.devices.values()):
config_entries = getattr(device, "config_entries", None)
if (config_entries and entry.entry_id in config_entries) or getattr(
device, "config_entry_id", None
) == entry.entry_id:
dev_reg.async_remove_device(device.id)
except Exception:
_LOGGER.exception("Error cleaning up device registry on entry removal")

try:
store: Store[Any] = Store(hass, 1, "hivi_speaker_device_data")
await store.async_remove()
_LOGGER.debug("Persistent device data storage removed")
except Exception:
_LOGGER.exception("Error removing persistent storage on entry removal")


async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
) -> bool:
"""Handle removal of a device from the UI/device registry.

Return True to allow Home Assistant to remove the device entry.
Only devices belonging to this integration (DOMAIN in identifiers) are cleaned up.
"""
if not any(identifier[0] == DOMAIN for identifier in device_entry.identifiers):
return False

try:
domain_data = hass.data.get(DOMAIN, {}).get(config_entry.entry_id, {})
device_manager = domain_data.get("device_manager")
if device_manager is None:
_LOGGER.debug(
"Device manager not found for entry %s, allowing device deletion %s",
config_entry.entry_id,
device_entry.id,
)
return True

ha_device_id = device_entry.id
speaker_device_id = None
device_dict = (
device_manager.device_data_registry.get_device_dict_by_ha_device_id(
ha_device_id
)
)
if device_dict is not None:
device_obj = HIVIDevice(**device_dict)
speaker_device_id = device_obj.speaker_device_id

await device_manager.async_remove_entities_for_device(ha_device_id)
await device_manager.device_data_registry.async_remove_device_data(ha_device_id)
_LOGGER.debug(
"Cleaned up entities and data for device %s, HA will remove the device entry",
ha_device_id,
)

if speaker_device_id:
await device_manager.remove_control_entities_by_speaker_device_id(
speaker_device_id
)
_LOGGER.debug(
"Requested device manager to delete control entities for speaker device ID %s",
speaker_device_id,
)

except Exception:
_LOGGER.exception(
"Error in async_remove_config_entry_device for device %s",
device_entry.id,
)
return True
else:
return True
92 changes: 92 additions & 0 deletions homeassistant/components/hivi_speaker/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Config flow for HiVi Speaker integration."""

from __future__ import annotations

import logging
from typing import Any

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.core import callback

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


class HIVISpeakerConfigFlow(ConfigFlow, domain=DOMAIN):
"""HIVI speaker configuration flow."""

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""User step - configure integration."""
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="HiVi Speaker", data={})

@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get options flow."""
return HIVISpeakerOptionsFlow()


class HIVISpeakerOptionsFlow(config_entries.OptionsFlow):
"""Options flow - using confirmation switch."""

def __init__(self) -> None:
"""Initialize the options flow."""
super().__init__()
self.open_num = 0

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show initial step of the options flow."""
_LOGGER.debug("Entering initial step of options flow")
self.open_num = 0
if user_input is not None:
if user_input.get("confirm_refresh"):
domain_data = self.hass.data.get(DOMAIN, {}).get(
self.config_entry.entry_id, {}
)
device_manager = domain_data.get("device_manager")
if device_manager is not None:
await device_manager.refresh_discovery()
else:
_LOGGER.warning(
"Device manager not available; skipping refresh from options"
)
return await self.async_step_success()

return self.async_create_entry(title="", data={})

return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required("confirm_refresh", default=True): bool,
}
),
)

async def async_step_success(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Success page."""
_LOGGER.debug("Displaying success page")
if self.open_num == 0:
self.open_num += 1
return self.async_show_form(
step_id="success",
data_schema=vol.Schema({}),
)
return self.async_create_entry(title="", data={})
13 changes: 13 additions & 0 deletions homeassistant/components/hivi_speaker/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Constants for the HiVi Speaker integration."""

from __future__ import annotations

DOMAIN = "hivi_speaker"

DISCOVERY_UPDATED = "hivi_speaker_discovery_updated"
DEVICE_RELATION_CHANGED = "hivi_speaker_device_relation_changed"
SIGNAL_DEVICE_DISCOVERED = "hivi_speaker_signal_device_discovered"
SIGNAL_DEVICE_STATUS_UPDATED = "hivi_speaker_signal_device_status_updated"

DISCOVERY_BASE_INTERVAL = 300 # seconds
DEVICE_OFFLINE_THRESHOLD = 180 # seconds
115 changes: 115 additions & 0 deletions homeassistant/components/hivi_speaker/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Device models for the HiVi Speaker integration."""

from __future__ import annotations

from datetime import UTC, datetime
from enum import Enum

from pydantic import BaseModel, Field


class SyncGroupStatus(Enum):
"""Sync group status or master/slave relationship status of a device."""

UNKNOWN = "unknown"
MASTER = "master"
SLAVE = "slave"
STANDALONE = "standalone"


class ConnectionStatus(Enum):
"""Device status."""

ONLINE = "online"
OFFLINE = "offline"
UNAVAILABLE = "unavailable"


class SlaveDeviceInfo(BaseModel):
"""Slave speaker information class."""

friendly_name: str
ssid: str
mask: int | None
volume: int
mute: bool
channel: int
battery: int | None
ip_addr: str
version: str
uuid: str


class HIVIDevice(BaseModel):
"""HIVI speaker device base class."""

speaker_device_id: str = ""
unique_id: str = ""
friendly_name: str = ""
model: str = ""
manufacturer: str = ""
ha_device_id: str = ""
hardware: str = ""

ip_addr: str = ""
mac_address: str = ""
hostname: str = ""

supports_dlna: bool = True
supports_private_protocol: bool = True
sync_group_status: SyncGroupStatus = SyncGroupStatus.STANDALONE

connection_status: ConnectionStatus = ConnectionStatus.ONLINE
last_seen: datetime = Field(default_factory=lambda: datetime.now(tz=UTC))

master_speaker_device_id: str | None = None
slave_device_num: int = 0
slave_device_list: list[SlaveDeviceInfo] = Field(default_factory=list)

dlna_udn: str | None = None
dlna_location: str | None = None

private_protocol_version: str | None = None
private_port: int = 9527

entity_id: str | None = None
config_entry_id: str | None = None

wifi_channel: str = "0"
ssid: str | None = None
auth_mode: str | None = None
encryption_mode: str | None = None
psk: str | None = None
uuid: str | None = None

def model_post_init(self, __context, /) -> None:
"""Auto-generate unique_id from mac_address if not provided."""
if not self.unique_id:
mac_address = self.mac_address.replace(":", "")
if mac_address:
self.unique_id = f"hivi_{mac_address}"

@property
def is_available_for_media(self) -> bool:
"""Whether available as media player (non-slave speaker)."""
return (
self.sync_group_status != SyncGroupStatus.SLAVE
and self.connection_status == ConnectionStatus.ONLINE
)

@property
def can_be_master(self) -> bool:
"""Whether can be set as master speaker."""
return (
self.sync_group_status
in {SyncGroupStatus.MASTER, SyncGroupStatus.STANDALONE}
and self.connection_status == ConnectionStatus.ONLINE
)

@property
def can_be_slave(self) -> bool:
"""Whether can be set as slave speaker."""
return (
self.sync_group_status == SyncGroupStatus.STANDALONE
and self.connection_status == ConnectionStatus.ONLINE
)
Loading
Loading