From 8d3d4a1b5cae4b0717bba95c511561dbce41f9e6 Mon Sep 17 00:00:00 2001 From: Kurt Chrisford <92524101+kclif9@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:12:56 +1000 Subject: [PATCH] Add diagnostics to Actron Air (#167145) --- .../components/actron_air/diagnostics.py | 35 ++++++++ .../components/actron_air/quality_scale.yaml | 2 +- tests/components/actron_air/conftest.py | 79 ++++++++----------- .../actron_air/fixtures/status.json | 41 ++++++++++ .../actron_air/snapshots/test_climate.ambr | 10 +-- .../snapshots/test_diagnostics.ambr | 55 +++++++++++++ tests/components/actron_air/test_climate.py | 3 - .../components/actron_air/test_diagnostics.py | 28 +++++++ tests/components/actron_air/test_switch.py | 5 +- 9 files changed, 204 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/actron_air/diagnostics.py create mode 100644 tests/components/actron_air/fixtures/status.json create mode 100644 tests/components/actron_air/snapshots/test_diagnostics.ambr create mode 100644 tests/components/actron_air/test_diagnostics.py diff --git a/homeassistant/components/actron_air/diagnostics.py b/homeassistant/components/actron_air/diagnostics.py new file mode 100644 index 000000000000..0cfa668a37c1 --- /dev/null +++ b/homeassistant/components/actron_air/diagnostics.py @@ -0,0 +1,35 @@ +"""Diagnostics support for Actron Air.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_API_TOKEN +from homeassistant.core import HomeAssistant + +from .coordinator import ActronAirConfigEntry + +TO_REDACT = {CONF_API_TOKEN, "master_serial", "serial_number", "serial"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + entry: ActronAirConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinators: dict[int, Any] = {} + for idx, coordinator in enumerate(entry.runtime_data.system_coordinators.values()): + coordinators[idx] = { + "system": async_redact_data( + coordinator.system.model_dump(mode="json"), TO_REDACT + ), + "status": async_redact_data( + coordinator.data.model_dump(mode="json", exclude={"last_known_state"}), + TO_REDACT, + ), + } + return { + "entry_data": async_redact_data(entry.data, TO_REDACT), + "coordinators": coordinators, + } diff --git a/homeassistant/components/actron_air/quality_scale.yaml b/homeassistant/components/actron_air/quality_scale.yaml index 35107d899df0..982050be3bdb 100644 --- a/homeassistant/components/actron_air/quality_scale.yaml +++ b/homeassistant/components/actron_air/quality_scale.yaml @@ -41,7 +41,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: status: exempt comment: This integration uses DHCP discovery, however is cloud polling. Therefore there is no information to update. diff --git a/tests/components/actron_air/conftest.py b/tests/components/actron_air/conftest.py index f17be5782d10..6bd7a6064541 100644 --- a/tests/components/actron_air/conftest.py +++ b/tests/components/actron_air/conftest.py @@ -2,10 +2,13 @@ import asyncio from collections.abc import Generator +import json from unittest.mock import AsyncMock, MagicMock, patch from actron_neo_api.models.auth import ActronAirDeviceCode -from actron_neo_api.models.system import ActronAirSystemInfo +from actron_neo_api.models.settings import ActronAirUserAirconSettings +from actron_neo_api.models.status import ActronAirStatus +from actron_neo_api.models.system import ActronAirACSystem, ActronAirSystemInfo import pytest from homeassistant.components.actron_air.const import DOMAIN @@ -14,7 +17,7 @@ from homeassistant.core import HomeAssistant from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture @pytest.fixture @@ -29,6 +32,27 @@ def mock_actron_api() -> Generator[AsyncMock]: "homeassistant.components.actron_air.config_flow.ActronAirAPI", new=mock_api, ), + patch.object(ActronAirACSystem, "set_system_mode", new_callable=AsyncMock), + patch.object( + ActronAirUserAirconSettings, "set_away_mode", new_callable=AsyncMock + ), + patch.object( + ActronAirUserAirconSettings, + "set_continuous_mode", + new_callable=AsyncMock, + ), + patch.object( + ActronAirUserAirconSettings, "set_quiet_mode", new_callable=AsyncMock + ), + patch.object( + ActronAirUserAirconSettings, "set_turbo_mode", new_callable=AsyncMock + ), + patch.object( + ActronAirUserAirconSettings, "set_temperature", new_callable=AsyncMock + ), + patch.object( + ActronAirUserAirconSettings, "set_fan_mode", new_callable=AsyncMock + ), ): api = mock_api.return_value @@ -65,47 +89,15 @@ def mock_actron_api() -> Generator[AsyncMock]: return_value=[ActronAirSystemInfo(serial="123456")] ) - # Mock state manager + # Build status from fixture JSON + status = ActronAirStatus.model_validate( + json.loads(load_fixture("status.json", DOMAIN)) + ) + status.set_api(api) + + # Mock state manager to return our real pydantic status api.state_manager = MagicMock() - status = api.state_manager.get_status.return_value - status.master_info.live_temp_c = 22.0 - status.master_info.live_humidity_pc = 50.0 - status.ac_system.system_name = "Test System" - status.ac_system.serial_number = "123456" - status.ac_system.master_wc_model = "Test Model" - status.ac_system.master_wc_firmware_version = "1.0.0" - status.ac_system.set_system_mode = AsyncMock() - status.remote_zone_info = [] - status.zones = {} - status.min_temp = 16 - status.max_temp = 30 - status.aircon_system.mode = "OFF" - status.fan_mode = "LOW" - status.set_point = 24 - status.room_temp = 25 - status.is_on = False - - # Mock user_aircon_settings for the switch and climate platforms - settings = status.user_aircon_settings - settings.away_mode = False - settings.continuous_fan_enabled = False - settings.quiet_mode_enabled = False - settings.turbo_enabled = False - settings.turbo_supported = True - settings.is_on = False - settings.mode = "COOL" - settings.base_fan_mode = "LOW" - settings.temperature_setpoint_cool_c = 24.0 - - settings.set_away_mode = AsyncMock() - settings.set_continuous_mode = AsyncMock() - settings.set_quiet_mode = AsyncMock() - settings.set_turbo_mode = AsyncMock() - settings.set_temperature = AsyncMock() - settings.set_fan_mode = AsyncMock() - - # Mock ac_system methods for climate platform - status.ac_system.set_system_mode = AsyncMock() + api.state_manager.get_status.return_value = status yield api @@ -126,7 +118,7 @@ def mock_zone() -> MagicMock: """Return a mocked zone.""" zone = MagicMock() zone.exists = True - zone.zone_id = 1 + zone.zone_id = 0 zone.zone_name = "Test Zone" zone.title = "Living Room" zone.live_temp_c = 22.0 @@ -160,7 +152,6 @@ async def init_integration_with_zone( """Set up the Actron Air integration with zone for testing.""" status = mock_actron_api.state_manager.get_status.return_value status.remote_zone_info = [mock_zone] - status.zones = {1: mock_zone} with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/actron_air/fixtures/status.json b/tests/components/actron_air/fixtures/status.json new file mode 100644 index 000000000000..fdb5c01dabca --- /dev/null +++ b/tests/components/actron_air/fixtures/status.json @@ -0,0 +1,41 @@ +{ + "isOnline": true, + "lastKnownState": { + "AirconSystem": { + "MasterWCModel": "Test Model", + "MasterSerial": "123456", + "MasterWCFirmwareVersion": "1.0.0", + "SystemName": "Test System" + }, + "NV_SystemSettings": { + "SystemName": "Test System" + }, + "UserAirconSettings": { + "isOn": false, + "Mode": "COOL", + "FanMode": "LOW", + "AwayMode": false, + "TemperatureSetpoint_Cool_oC": 24.0, + "TemperatureSetpoint_Heat_oC": 20.0, + "EnabledZones": [], + "QuietModeEnabled": false, + "TurboMode": { + "Enabled": false, + "Supported": true + } + }, + "MasterInfo": { + "LiveTemp_oC": 22.0, + "LiveHumidity_pc": 50.0, + "LiveOutdoorTemp_oC": 0.0 + }, + "NV_Limits": { + "UserSetpoint_oC": { + "setCool_Min": 16.0, + "setCool_Max": 30.0 + } + }, + "RemoteZoneInfo": [] + }, + "serial_number": "123456" +} diff --git a/tests/components/actron_air/snapshots/test_climate.ambr b/tests/components/actron_air/snapshots/test_climate.ambr index d2081870db39..86f28beadb8f 100644 --- a/tests/components/actron_air/snapshots/test_climate.ambr +++ b/tests/components/actron_air/snapshots/test_climate.ambr @@ -42,7 +42,7 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': '123456_zone_1', + 'unique_id': '123456_zone_0', 'unit_of_measurement': None, }) # --- @@ -92,8 +92,8 @@ , , ]), - 'max_temp': 30, - 'min_temp': 16, + 'max_temp': 30.0, + 'min_temp': 16.0, }), 'config_entry_id': , 'config_subentry_id': , @@ -145,8 +145,8 @@ , , ]), - 'max_temp': 30, - 'min_temp': 16, + 'max_temp': 30.0, + 'min_temp': 16.0, 'supported_features': , 'temperature': 24.0, }), diff --git a/tests/components/actron_air/snapshots/test_diagnostics.ambr b/tests/components/actron_air/snapshots/test_diagnostics.ambr new file mode 100644 index 000000000000..83970f76149c --- /dev/null +++ b/tests/components/actron_air/snapshots/test_diagnostics.ambr @@ -0,0 +1,55 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'coordinators': dict({ + '0': dict({ + 'status': dict({ + 'ac_system': dict({ + 'master_serial': '**REDACTED**', + 'master_wc_firmware_version': '1.0.0', + 'master_wc_model': 'Test Model', + 'outdoor_unit': None, + 'system_name': 'Test System', + }), + 'alerts': None, + 'is_online': True, + 'live_aircon': None, + 'master_info': dict({ + 'live_humidity_pc': 50.0, + 'live_outdoor_temp_c': 0.0, + 'live_temp_c': 22.0, + }), + 'peripherals': list([ + ]), + 'remote_zone_info': list([ + ]), + 'serial_number': '**REDACTED**', + 'user_aircon_settings': dict({ + 'away_mode': False, + 'enabled_zones': list([ + ]), + 'fan_mode': 'LOW', + 'is_on': False, + 'mode': 'COOL', + 'quiet_mode_enabled': False, + 'temperature_setpoint_cool_c': 24.0, + 'temperature_setpoint_heat_c': 20.0, + 'turbo_mode_enabled': dict({ + 'Enabled': False, + 'Supported': True, + }), + }), + }), + 'system': dict({ + 'links': dict({ + }), + 'serial': '**REDACTED**', + 'type': None, + }), + }), + }), + 'entry_data': dict({ + 'api_token': '**REDACTED**', + }), + }) +# --- diff --git a/tests/components/actron_air/test_climate.py b/tests/components/actron_air/test_climate.py index 61262dcc8501..5afd3261f602 100644 --- a/tests/components/actron_air/test_climate.py +++ b/tests/components/actron_air/test_climate.py @@ -36,7 +36,6 @@ async def test_climate_entities( """Test climate entities.""" status = mock_actron_api.state_manager.get_status.return_value status.remote_zone_info = [mock_zone] - status.zones = {1: mock_zone} with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): await setup_integration(hass, mock_config_entry) @@ -289,7 +288,6 @@ async def test_zone_hvac_mode_unmapped( status = mock_actron_api.state_manager.get_status.return_value status.remote_zone_info = [mock_zone] - status.zones = {1: mock_zone} with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): await setup_integration(hass, mock_config_entry) @@ -309,7 +307,6 @@ async def test_zone_hvac_mode_inactive( status = mock_actron_api.state_manager.get_status.return_value status.remote_zone_info = [mock_zone] - status.zones = {1: mock_zone} with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/actron_air/test_diagnostics.py b/tests/components/actron_air/test_diagnostics.py new file mode 100644 index 000000000000..c38b463b44a5 --- /dev/null +++ b/tests/components/actron_air/test_diagnostics.py @@ -0,0 +1,28 @@ +"""Tests for Actron Air diagnostics.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_integration + +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, + mock_actron_api: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + await setup_integration(hass, mock_config_entry) + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) diff --git a/tests/components/actron_air/test_switch.py b/tests/components/actron_air/test_switch.py index ef4b4e2f2926..d2f723bfbb57 100644 --- a/tests/components/actron_air/test_switch.py +++ b/tests/components/actron_air/test_switch.py @@ -83,7 +83,10 @@ async def test_turbo_mode_not_supported( ) -> None: """Test turbo mode switch is not created when not supported.""" status = mock_actron_api.state_manager.get_status.return_value - status.user_aircon_settings.turbo_supported = False + status.user_aircon_settings.turbo_mode_enabled = { + "Enabled": False, + "Supported": False, + } await setup_integration(hass, mock_config_entry)