mirror of
https://github.com/home-assistant/core.git
synced 2026-08-23 22:54:24 +01:00
Add diagnostics platform to Hot Spring (#179778)
Co-authored-by: Moustachauve <2206577+Moustachauve@users.noreply.github.com>
This commit is contained in:
co-authored by
Moustachauve
parent
49259e3426
commit
839bb52aba
@@ -0,0 +1,58 @@
|
||||
"""Diagnostics support for Hot Spring."""
|
||||
|
||||
from dataclasses import asdict
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED, async_redact_data
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import HotSpringConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_HOST,
|
||||
}
|
||||
|
||||
|
||||
def _redact_mac(value: str, patterns: list[str]) -> str:
|
||||
"""Redact MAC address patterns from a string."""
|
||||
for pattern in patterns:
|
||||
value = re.sub(re.escape(pattern), REDACTED, value, flags=re.IGNORECASE)
|
||||
return value
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: HotSpringConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
spa = coordinator.data
|
||||
|
||||
info = asdict(spa.info)
|
||||
if mac_address := spa.info.mac_address:
|
||||
clean_mac = mac_address.replace(":", "")
|
||||
patterns = [mac_address, clean_mac, clean_mac[-6:]]
|
||||
info["root_topic"] = _redact_mac(info["root_topic"], patterns)
|
||||
info["hostname"] = _redact_mac(info["hostname"], patterns)
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.data, TO_REDACT),
|
||||
"data": {
|
||||
"info": info,
|
||||
"heater": asdict(spa.heater),
|
||||
"jets": [asdict(jet) for jet in spa.jets],
|
||||
"blower": asdict(spa.blower),
|
||||
"light_zones": [asdict(zone) for zone in spa.light_zones],
|
||||
"logo_light": asdict(spa.logo_light),
|
||||
"clean_cycle": asdict(spa.clean_cycle),
|
||||
"spa_lock": asdict(spa.spa_lock),
|
||||
"water_care": asdict(spa.water_care),
|
||||
"freshwater_iq": asdict(spa.freshwater_iq),
|
||||
"energy_savings": [asdict(schedule) for schedule in spa.energy_savings],
|
||||
"versions": asdict(spa.versions),
|
||||
"connection_status": asdict(spa.connection_status),
|
||||
"diagnostics": asdict(spa.diagnostics),
|
||||
"test_metrics": asdict(spa.test_metrics),
|
||||
},
|
||||
}
|
||||
@@ -49,7 +49,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
|
||||
@@ -3,7 +3,32 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from hotspring import Heater, Spa, SpaBrand, SpaInfo, Versions, WaterCare
|
||||
from hotspring import (
|
||||
Blower,
|
||||
BrightnessLevel,
|
||||
CleanCycle,
|
||||
ConnectionStatus,
|
||||
Diagnostics,
|
||||
EnergySaving,
|
||||
FreshWaterIQ,
|
||||
Heater,
|
||||
HeatingMode,
|
||||
Jet,
|
||||
JetSpeed,
|
||||
LightColor,
|
||||
LightWheelMode,
|
||||
LightZone,
|
||||
LogoLight,
|
||||
Spa,
|
||||
SpaBrand,
|
||||
SpaFailureState,
|
||||
SpaInfo,
|
||||
SpaLock,
|
||||
TemperatureUnit,
|
||||
Versions,
|
||||
WaterCare,
|
||||
)
|
||||
from hotspring.models import SpaTestData
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hotspring.const import DOMAIN
|
||||
@@ -61,11 +86,17 @@ def device_fixture() -> Spa:
|
||||
dosing="",
|
||||
logolight="",
|
||||
)
|
||||
heater = MagicMock(spec=Heater)
|
||||
heater.current_temperature = 102.0
|
||||
heater.set_temperature = 104.0
|
||||
heater.is_on = True
|
||||
spa.heater = heater
|
||||
spa.heater = Heater(
|
||||
is_on=True,
|
||||
heater_lock=False,
|
||||
heatpump_installed=False,
|
||||
heating_mode=HeatingMode.HEAT_SAVER,
|
||||
heater_current=5.0,
|
||||
heater_on_seconds=3600,
|
||||
set_temperature=104.0,
|
||||
current_temperature=102.0,
|
||||
temperature_unit=TemperatureUnit.FAHRENHEIT,
|
||||
)
|
||||
spa.water_care = WaterCare(
|
||||
cartridge_installed=True,
|
||||
ten_day_timer=0,
|
||||
@@ -76,6 +107,60 @@ def device_fixture() -> Spa:
|
||||
boost_active=False,
|
||||
salt_value=12,
|
||||
)
|
||||
spa.jets = [
|
||||
Jet(jet_id=1, speed=JetSpeed.OFF, is_enabled=True, on_seconds=0),
|
||||
Jet(jet_id=2, speed=JetSpeed.OFF, is_enabled=True, on_seconds=0),
|
||||
]
|
||||
spa.blower = Blower(is_enabled=False, is_on=False)
|
||||
spa.light_zones = [
|
||||
LightZone(
|
||||
zone_id=1,
|
||||
is_enabled=True,
|
||||
is_on=False,
|
||||
color=LightColor.OFF,
|
||||
light_wheel=LightWheelMode.OFF,
|
||||
intensity=0,
|
||||
loop_speed=0,
|
||||
),
|
||||
]
|
||||
spa.logo_light = LogoLight(brightness=BrightnessLevel.LEVEL_1)
|
||||
spa.clean_cycle = CleanCycle(is_enabled=False, vanishing_act=False)
|
||||
spa.spa_lock = SpaLock(is_locked=False)
|
||||
spa.freshwater_iq = FreshWaterIQ(
|
||||
conductivity=0,
|
||||
orp=0,
|
||||
chlorine=0.0,
|
||||
ph=7.2,
|
||||
sensor_life_percentage=100.0,
|
||||
installed=False,
|
||||
)
|
||||
spa.energy_savings = [
|
||||
EnergySaving(schedule_id=1, mode=0, start_hour=0, start_minute=0, duration=0),
|
||||
]
|
||||
spa.connection_status = ConnectionStatus(spa_connected=True)
|
||||
spa.diagnostics = Diagnostics(
|
||||
spa_failure_state=SpaFailureState.OK,
|
||||
heater_error="0",
|
||||
power_frequency="60",
|
||||
pressure_switch_status="0",
|
||||
l1_n_volts=120.0,
|
||||
l2_n_volts=120.0,
|
||||
heater_volts=240.0,
|
||||
jet3_volts=0.0,
|
||||
jet1_jet2_blower_power="0",
|
||||
small_loads_power="0",
|
||||
heater_power="0",
|
||||
jet3_power="0",
|
||||
)
|
||||
spa.test_metrics = SpaTestData(
|
||||
heater_test_status="off",
|
||||
temp_offset=0.0,
|
||||
vsense_cal=0.0,
|
||||
jet1_jet2_blower_current=0.0,
|
||||
small_loads_current=0.0,
|
||||
heater_current=0.0,
|
||||
jet3_current=0.0,
|
||||
)
|
||||
return spa
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'data': dict({
|
||||
'blower': dict({
|
||||
'is_enabled': False,
|
||||
'is_on': False,
|
||||
}),
|
||||
'clean_cycle': dict({
|
||||
'is_enabled': False,
|
||||
'vanishing_act': False,
|
||||
}),
|
||||
'connection_status': dict({
|
||||
'spa_connected': True,
|
||||
}),
|
||||
'diagnostics': dict({
|
||||
'heater_error': '0',
|
||||
'heater_power': '0',
|
||||
'heater_volts': 240.0,
|
||||
'jet1_jet2_blower_power': '0',
|
||||
'jet3_power': '0',
|
||||
'jet3_volts': 0.0,
|
||||
'l1_n_volts': 120.0,
|
||||
'l2_n_volts': 120.0,
|
||||
'power_frequency': '60',
|
||||
'pressure_switch_status': '0',
|
||||
'small_loads_power': '0',
|
||||
'spa_failure_state': dict({
|
||||
'__type': "<enum 'SpaFailureState'>",
|
||||
'repr': "<SpaFailureState.OK: 'Spa_Ok'>",
|
||||
}),
|
||||
}),
|
||||
'energy_savings': list([
|
||||
dict({
|
||||
'duration': 0,
|
||||
'mode': 0,
|
||||
'schedule_id': 1,
|
||||
'start_hour': 0,
|
||||
'start_minute': 0,
|
||||
}),
|
||||
]),
|
||||
'freshwater_iq': dict({
|
||||
'chlorine': 0.0,
|
||||
'conductivity': 0,
|
||||
'installed': False,
|
||||
'orp': 0,
|
||||
'ph': 7.2,
|
||||
'sensor_life_percentage': 100.0,
|
||||
}),
|
||||
'heater': dict({
|
||||
'current_temperature': 102.0,
|
||||
'heater_current': 5.0,
|
||||
'heater_lock': False,
|
||||
'heater_on_seconds': 3600,
|
||||
'heating_mode': dict({
|
||||
'__type': "<enum 'HeatingMode'>",
|
||||
'repr': "<HeatingMode.HEAT_SAVER: 'heatSaver'>",
|
||||
}),
|
||||
'heatpump_installed': False,
|
||||
'is_on': True,
|
||||
'set_temperature': 104.0,
|
||||
'temperature_unit': dict({
|
||||
'__type': "<enum 'TemperatureUnit'>",
|
||||
'repr': "<TemperatureUnit.FAHRENHEIT: 'DegF'>",
|
||||
}),
|
||||
}),
|
||||
'info': dict({
|
||||
'brand': dict({
|
||||
'__type': "<enum 'SpaBrand'>",
|
||||
'repr': "<SpaBrand.HOTSPRING: 'HotSpring'>",
|
||||
}),
|
||||
'brand_id': '1',
|
||||
'brand_name': 'Hot Spring',
|
||||
'collection': 'Highlife',
|
||||
'collection_id': '1',
|
||||
'hostname': 'ConnectedSpa_**REDACTED**',
|
||||
'model_id': '1',
|
||||
'model_name': 'Relay',
|
||||
'root_topic': 'mySpa**REDACTED**',
|
||||
'sna_ready': True,
|
||||
'volume': 335,
|
||||
}),
|
||||
'jets': list([
|
||||
dict({
|
||||
'is_enabled': True,
|
||||
'jet_id': 1,
|
||||
'on_seconds': 0,
|
||||
'speed': dict({
|
||||
'__type': "<enum 'JetSpeed'>",
|
||||
'repr': "<JetSpeed.OFF: 'off'>",
|
||||
}),
|
||||
}),
|
||||
dict({
|
||||
'is_enabled': True,
|
||||
'jet_id': 2,
|
||||
'on_seconds': 0,
|
||||
'speed': dict({
|
||||
'__type': "<enum 'JetSpeed'>",
|
||||
'repr': "<JetSpeed.OFF: 'off'>",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
'light_zones': list([
|
||||
dict({
|
||||
'color': dict({
|
||||
'__type': "<enum 'LightColor'>",
|
||||
'repr': "<LightColor.OFF: 'WHEEL_OFF'>",
|
||||
}),
|
||||
'intensity': 0,
|
||||
'is_enabled': True,
|
||||
'is_on': False,
|
||||
'light_wheel': dict({
|
||||
'__type': "<enum 'LightWheelMode'>",
|
||||
'repr': "<LightWheelMode.OFF: 'off'>",
|
||||
}),
|
||||
'loop_speed': 0,
|
||||
'zone_id': 1,
|
||||
}),
|
||||
]),
|
||||
'logo_light': dict({
|
||||
'brightness': dict({
|
||||
'__type': "<enum 'BrightnessLevel'>",
|
||||
'repr': "<BrightnessLevel.LEVEL_1: 'brightness_level_1'>",
|
||||
}),
|
||||
}),
|
||||
'spa_lock': dict({
|
||||
'is_locked': False,
|
||||
}),
|
||||
'test_metrics': dict({
|
||||
'heater_current': 0.0,
|
||||
'heater_test_status': 'off',
|
||||
'jet1_jet2_blower_current': 0.0,
|
||||
'jet3_current': 0.0,
|
||||
'small_loads_current': 0.0,
|
||||
'temp_offset': 0.0,
|
||||
'vsense_cal': 0.0,
|
||||
}),
|
||||
'versions': dict({
|
||||
'amp': '',
|
||||
'btxr': '',
|
||||
'control_box': '3.0.0',
|
||||
'control_panel': '2.0.0',
|
||||
'cool_zone': '',
|
||||
'dosing': '',
|
||||
'fwiq': '',
|
||||
'fwss': '1.0.0',
|
||||
'logolight': '',
|
||||
'wifi_dongle': '1.0.0',
|
||||
}),
|
||||
'water_care': dict({
|
||||
'ace_mode': 'inactive',
|
||||
'boost_active': False,
|
||||
'cartridge_installed': True,
|
||||
'level': 2,
|
||||
'one_twenty_day_timer': 117,
|
||||
'salt_value': 12,
|
||||
'system_enabled': True,
|
||||
'ten_day_timer': 0,
|
||||
}),
|
||||
}),
|
||||
'entry': dict({
|
||||
'host': '**REDACTED**',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_diagnostics_custom_topic
|
||||
dict({
|
||||
'data': dict({
|
||||
'blower': dict({
|
||||
'is_enabled': False,
|
||||
'is_on': False,
|
||||
}),
|
||||
'clean_cycle': dict({
|
||||
'is_enabled': False,
|
||||
'vanishing_act': False,
|
||||
}),
|
||||
'connection_status': dict({
|
||||
'spa_connected': True,
|
||||
}),
|
||||
'diagnostics': dict({
|
||||
'heater_error': '0',
|
||||
'heater_power': '0',
|
||||
'heater_volts': 240.0,
|
||||
'jet1_jet2_blower_power': '0',
|
||||
'jet3_power': '0',
|
||||
'jet3_volts': 0.0,
|
||||
'l1_n_volts': 120.0,
|
||||
'l2_n_volts': 120.0,
|
||||
'power_frequency': '60',
|
||||
'pressure_switch_status': '0',
|
||||
'small_loads_power': '0',
|
||||
'spa_failure_state': dict({
|
||||
'__type': "<enum 'SpaFailureState'>",
|
||||
'repr': "<SpaFailureState.OK: 'Spa_Ok'>",
|
||||
}),
|
||||
}),
|
||||
'energy_savings': list([
|
||||
dict({
|
||||
'duration': 0,
|
||||
'mode': 0,
|
||||
'schedule_id': 1,
|
||||
'start_hour': 0,
|
||||
'start_minute': 0,
|
||||
}),
|
||||
]),
|
||||
'freshwater_iq': dict({
|
||||
'chlorine': 0.0,
|
||||
'conductivity': 0,
|
||||
'installed': False,
|
||||
'orp': 0,
|
||||
'ph': 7.2,
|
||||
'sensor_life_percentage': 100.0,
|
||||
}),
|
||||
'heater': dict({
|
||||
'current_temperature': 102.0,
|
||||
'heater_current': 5.0,
|
||||
'heater_lock': False,
|
||||
'heater_on_seconds': 3600,
|
||||
'heating_mode': dict({
|
||||
'__type': "<enum 'HeatingMode'>",
|
||||
'repr': "<HeatingMode.HEAT_SAVER: 'heatSaver'>",
|
||||
}),
|
||||
'heatpump_installed': False,
|
||||
'is_on': True,
|
||||
'set_temperature': 104.0,
|
||||
'temperature_unit': dict({
|
||||
'__type': "<enum 'TemperatureUnit'>",
|
||||
'repr': "<TemperatureUnit.FAHRENHEIT: 'DegF'>",
|
||||
}),
|
||||
}),
|
||||
'info': dict({
|
||||
'brand': dict({
|
||||
'__type': "<enum 'SpaBrand'>",
|
||||
'repr': "<SpaBrand.HOTSPRING: 'HotSpring'>",
|
||||
}),
|
||||
'brand_id': '1',
|
||||
'brand_name': 'Hot Spring',
|
||||
'collection': 'Highlife',
|
||||
'collection_id': '1',
|
||||
'hostname': 'customHost',
|
||||
'model_id': '1',
|
||||
'model_name': 'Relay',
|
||||
'root_topic': 'customTopic',
|
||||
'sna_ready': True,
|
||||
'volume': 335,
|
||||
}),
|
||||
'jets': list([
|
||||
dict({
|
||||
'is_enabled': True,
|
||||
'jet_id': 1,
|
||||
'on_seconds': 0,
|
||||
'speed': dict({
|
||||
'__type': "<enum 'JetSpeed'>",
|
||||
'repr': "<JetSpeed.OFF: 'off'>",
|
||||
}),
|
||||
}),
|
||||
dict({
|
||||
'is_enabled': True,
|
||||
'jet_id': 2,
|
||||
'on_seconds': 0,
|
||||
'speed': dict({
|
||||
'__type': "<enum 'JetSpeed'>",
|
||||
'repr': "<JetSpeed.OFF: 'off'>",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
'light_zones': list([
|
||||
dict({
|
||||
'color': dict({
|
||||
'__type': "<enum 'LightColor'>",
|
||||
'repr': "<LightColor.OFF: 'WHEEL_OFF'>",
|
||||
}),
|
||||
'intensity': 0,
|
||||
'is_enabled': True,
|
||||
'is_on': False,
|
||||
'light_wheel': dict({
|
||||
'__type': "<enum 'LightWheelMode'>",
|
||||
'repr': "<LightWheelMode.OFF: 'off'>",
|
||||
}),
|
||||
'loop_speed': 0,
|
||||
'zone_id': 1,
|
||||
}),
|
||||
]),
|
||||
'logo_light': dict({
|
||||
'brightness': dict({
|
||||
'__type': "<enum 'BrightnessLevel'>",
|
||||
'repr': "<BrightnessLevel.LEVEL_1: 'brightness_level_1'>",
|
||||
}),
|
||||
}),
|
||||
'spa_lock': dict({
|
||||
'is_locked': False,
|
||||
}),
|
||||
'test_metrics': dict({
|
||||
'heater_current': 0.0,
|
||||
'heater_test_status': 'off',
|
||||
'jet1_jet2_blower_current': 0.0,
|
||||
'jet3_current': 0.0,
|
||||
'small_loads_current': 0.0,
|
||||
'temp_offset': 0.0,
|
||||
'vsense_cal': 0.0,
|
||||
}),
|
||||
'versions': dict({
|
||||
'amp': '',
|
||||
'btxr': '',
|
||||
'control_box': '3.0.0',
|
||||
'control_panel': '2.0.0',
|
||||
'cool_zone': '',
|
||||
'dosing': '',
|
||||
'fwiq': '',
|
||||
'fwss': '1.0.0',
|
||||
'logolight': '',
|
||||
'wifi_dongle': '1.0.0',
|
||||
}),
|
||||
'water_care': dict({
|
||||
'ace_mode': 'inactive',
|
||||
'boost_active': False,
|
||||
'cartridge_installed': True,
|
||||
'level': 2,
|
||||
'one_twenty_day_timer': 117,
|
||||
'salt_value': 12,
|
||||
'system_enabled': True,
|
||||
'ten_day_timer': 0,
|
||||
}),
|
||||
}),
|
||||
'entry': dict({
|
||||
'host': '**REDACTED**',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Tests for the diagnostics data provided by the Hot Spring integration."""
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
init_integration: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics."""
|
||||
assert (
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
|
||||
== snapshot
|
||||
)
|
||||
|
||||
|
||||
async def test_diagnostics_custom_topic(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
init_integration: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics to ensure root_topic without MAC address is not redacted.
|
||||
|
||||
This preserves diagnosing capabilities in case a spa model acts differently than expected.
|
||||
"""
|
||||
coordinator = init_integration.runtime_data
|
||||
coordinator.data.info.root_topic = "customTopic"
|
||||
coordinator.data.info.hostname = "customHost"
|
||||
|
||||
assert (
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
|
||||
== snapshot
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for the Hot Spring integration."""
|
||||
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from hotspring import HotSpringConnectionError, HotSpringError, Spa
|
||||
@@ -22,7 +23,7 @@ async def test_async_setup_entry(
|
||||
|
||||
assert await hass.config_entries.async_unload(init_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert init_integration.state is ConfigEntryState.NOT_LOADED
|
||||
assert cast(ConfigEntryState, init_integration.state) is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_device_info(
|
||||
|
||||
Reference in New Issue
Block a user