diff --git a/homeassistant/components/aurorawatch/__init__.py b/homeassistant/components/aurorawatch/__init__.py
new file mode 100644
index 00000000000000..e0e16f93c3bc35
--- /dev/null
+++ b/homeassistant/components/aurorawatch/__init__.py
@@ -0,0 +1,38 @@
+"""The AuroraWatch UK integration."""
+
+import logging
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+
+from .const import DOMAIN
+from .coordinator import AurowatchDataUpdateCoordinator
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = [Platform.SENSOR]
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up AuroraWatch UK from a config entry."""
+ coordinator = AurowatchDataUpdateCoordinator(hass)
+
+ # Perform the first refresh and raise ConfigEntryNotReady on failure
+ await coordinator.async_config_entry_first_refresh()
+
+ # Store coordinator on the config entry runtime data
+ entry.runtime_data = coordinator
+
+ # Forward entry setup to sensor platform
+ await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ # Unload platforms
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+
+ return unload_ok
diff --git a/homeassistant/components/aurorawatch/config_flow.py b/homeassistant/components/aurorawatch/config_flow.py
new file mode 100644
index 00000000000000..4822a2305770b1
--- /dev/null
+++ b/homeassistant/components/aurorawatch/config_flow.py
@@ -0,0 +1,34 @@
+"""Config flow for AuroraWatch UK integration."""
+
+import logging
+
+from homeassistant import config_entries
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResult
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AurowatchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for AuroraWatch UK."""
+
+ VERSION = 1
+ MINOR_VERSION = 1
+
+ async def async_step_user(self, user_input=None) -> FlowResult:
+ """Handle the initial step."""
+ # Check if already configured
+ await self.async_set_unique_id(DOMAIN)
+ self._abort_if_unique_id_configured()
+
+ if user_input is None:
+ # Show the form to confirm setup
+ return self.async_show_form(step_id="user")
+
+ # Create the config entry
+ return self.async_create_entry(
+ title="AuroraWatch UK",
+ data={},
+ )
diff --git a/homeassistant/components/aurorawatch/const.py b/homeassistant/components/aurorawatch/const.py
new file mode 100644
index 00000000000000..c4167d05042240
--- /dev/null
+++ b/homeassistant/components/aurorawatch/const.py
@@ -0,0 +1,23 @@
+"""Constants for the AuroraWatch UK integration."""
+
+from datetime import timedelta
+
+DOMAIN = "aurorawatch"
+ATTRIBUTION = "Data provided by AuroraWatch UK"
+
+# API Configuration
+API_URL = "https://aurorawatch-api.lancs.ac.uk/0.2/status/current-status.xml"
+API_ACTIVITY_URL = (
+ "https://aurorawatch-api.lancs.ac.uk/0.2/status/project/awn/sum-activity.xml"
+)
+API_TIMEOUT = 10 # seconds
+
+# Update Configuration
+UPDATE_INTERVAL = timedelta(minutes=5)
+
+# Sensor Attributes
+ATTR_LAST_UPDATED = "last_updated"
+ATTR_PROJECT_ID = "project_id"
+ATTR_SITE_ID = "site_id"
+ATTR_SITE_URL = "site_url"
+ATTR_API_VERSION = "api_version"
diff --git a/homeassistant/components/aurorawatch/coordinator.py b/homeassistant/components/aurorawatch/coordinator.py
new file mode 100644
index 00000000000000..b684905264e6ae
--- /dev/null
+++ b/homeassistant/components/aurorawatch/coordinator.py
@@ -0,0 +1,117 @@
+"""Data coordinator for AuroraWatch UK integration."""
+
+import logging
+from defusedxml import ElementTree as ET
+from datetime import timedelta
+
+import asyncio
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import API_ACTIVITY_URL, API_TIMEOUT, API_URL, UPDATE_INTERVAL
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AurowatchDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching AuroraWatch data from API."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ config_entry: ConfigEntry | None = None,
+ ) -> None:
+ """Initialize the coordinator."""
+ super().__init__(
+ hass,
+ _LOGGER,
+ name="AuroraWatch",
+ update_interval=UPDATE_INTERVAL,
+ config_entry=config_entry,
+ )
+
+ async def _async_update_data(self):
+ """Fetch data from AuroraWatch API."""
+ try:
+ session = async_get_clientsession(self.hass)
+
+ # Fetch both status and activity data
+ async with asyncio.timeout(API_TIMEOUT):
+ async with session.get(API_URL) as status_response:
+ status_response.raise_for_status()
+ status_xml = await status_response.text()
+
+ async with session.get(API_ACTIVITY_URL) as activity_response:
+ activity_response.raise_for_status()
+ activity_xml = await activity_response.text()
+
+ # Parse status XML
+ try:
+ status_root = ET.fromstring(status_xml)
+ except ET.ParseError as err:
+ _LOGGER.error("Failed to parse status XML response: %s", err)
+ raise UpdateFailed(f"Invalid XML response: {err}") from err
+
+ # Parse activity XML
+ try:
+ activity_root = ET.fromstring(activity_xml)
+ except ET.ParseError as err:
+ _LOGGER.error("Failed to parse activity XML response: %s", err)
+ raise UpdateFailed(f"Invalid activity XML response: {err}") from err
+
+ # Extract data
+ try:
+ # Get updated datetime from status
+ datetime_element = status_root.find(".//updated/datetime")
+ if datetime_element is None or datetime_element.text is None:
+ raise UpdateFailed("Missing 'updated/datetime' element in XML")
+ last_updated = datetime_element.text
+
+ # Get site status information
+ status_element = status_root.find(".//site_status")
+ if status_element is None:
+ raise UpdateFailed("Missing 'site_status' element in XML")
+
+ status_id = status_element.get("status_id")
+ if status_id is None:
+ raise UpdateFailed("Missing 'status_id' attribute in site_status")
+
+ project_id = status_element.get("project_id", "Unknown")
+ site_id = status_element.get("site_id", "Unknown")
+ site_url = status_element.get("site_url", "")
+
+ # Get API version
+ api_version = status_root.get("api_version", "Unknown")
+
+ # Get current activity value (most recent in the list)
+ activity_elements = activity_root.findall(".//activity")
+ activity_value = None
+ if activity_elements:
+ # Get the last (most recent) activity reading
+ last_activity = activity_elements[-1]
+ value_element = last_activity.find("value")
+ if value_element is not None and value_element.text is not None:
+ activity_value = float(value_element.text)
+
+ data = {
+ "status": status_id,
+ "last_updated": last_updated,
+ "project_id": project_id,
+ "site_id": site_id,
+ "site_url": site_url,
+ "api_version": api_version,
+ "activity": activity_value,
+ }
+
+ _LOGGER.debug("Successfully fetched data: %s", data)
+ return data
+
+ except (AttributeError, KeyError) as err:
+ _LOGGER.error("Failed to extract data from XML: %s", err)
+ raise UpdateFailed(f"Failed to parse XML structure: {err}") from err
+
+ except Exception as err:
+ _LOGGER.error("Error fetching data: %s", err)
+ raise UpdateFailed(f"Error communicating with API: {err}") from err
diff --git a/homeassistant/components/aurorawatch/entity.py b/homeassistant/components/aurorawatch/entity.py
new file mode 100644
index 00000000000000..7352c304050fc4
--- /dev/null
+++ b/homeassistant/components/aurorawatch/entity.py
@@ -0,0 +1,45 @@
+"""The AuroraWatch component."""
+
+from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import ATTRIBUTION, DOMAIN
+from .coordinator import AurowatchDataUpdateCoordinator
+
+
+class AurowatchEntity(CoordinatorEntity[AurowatchDataUpdateCoordinator]):
+ """Implementation of the base AuroraWatch Entity."""
+
+ _attr_attribution = ATTRIBUTION
+ _attr_has_entity_name = True
+
+ def __init__(
+ self,
+ coordinator: AurowatchDataUpdateCoordinator,
+ translation_key: str,
+ ) -> None:
+ """Initialize the AuroraWatch Entity."""
+
+ super().__init__(coordinator=coordinator)
+
+ self._attr_translation_key = translation_key
+
+ # Try to obtain the config entry ID from the coordinator if available.
+ config_entry = getattr(coordinator, "config_entry", None)
+ entry_id = getattr(config_entry, "entry_id", None)
+
+ if entry_id is not None:
+ base_id = entry_id
+ else:
+ # Fallback base ID when the coordinator has no config_entry.
+ # This avoids AttributeError while still providing a stable ID.
+ base_id = translation_key
+
+ self._attr_unique_id = f"{base_id}_{translation_key}"
+ self._attr_device_info = DeviceInfo(
+ entry_type=DeviceEntryType.SERVICE,
+ identifiers={(DOMAIN, base_id)},
+ manufacturer="Lancaster University",
+ model="AuroraWatch UK",
+ name="AuroraWatch UK",
+ )
diff --git a/homeassistant/components/aurorawatch/icon.png b/homeassistant/components/aurorawatch/icon.png
new file mode 100644
index 00000000000000..0e9c48693d5bd0
Binary files /dev/null and b/homeassistant/components/aurorawatch/icon.png differ
diff --git a/homeassistant/components/aurorawatch/icons.json b/homeassistant/components/aurorawatch/icons.json
new file mode 100644
index 00000000000000..044af871253a8c
--- /dev/null
+++ b/homeassistant/components/aurorawatch/icons.json
@@ -0,0 +1,12 @@
+{
+ "entity": {
+ "sensor": {
+ "aurora_status": {
+ "default": "mdi:weather-night"
+ },
+ "geomagnetic_activity": {
+ "default": "mdi:chart-line"
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/aurorawatch/logo.png b/homeassistant/components/aurorawatch/logo.png
new file mode 100644
index 00000000000000..0e9c48693d5bd0
Binary files /dev/null and b/homeassistant/components/aurorawatch/logo.png differ
diff --git a/homeassistant/components/aurorawatch/manifest.json b/homeassistant/components/aurorawatch/manifest.json
new file mode 100644
index 00000000000000..d5cbeabe4368ca
--- /dev/null
+++ b/homeassistant/components/aurorawatch/manifest.json
@@ -0,0 +1,10 @@
+{
+ "domain": "aurorawatch",
+ "name": "AuroraWatch UK",
+ "codeowners": ["@agentgonzo"],
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/aurorawatch",
+ "integration_type": "service",
+ "iot_class": "cloud_polling",
+ "requirements": []
+}
diff --git a/homeassistant/components/aurorawatch/sensor.py b/homeassistant/components/aurorawatch/sensor.py
new file mode 100644
index 00000000000000..4c971eca44c9e1
--- /dev/null
+++ b/homeassistant/components/aurorawatch/sensor.py
@@ -0,0 +1,85 @@
+"""Sensor platform for AuroraWatch UK integration."""
+
+import logging
+from typing import cast
+
+from homeassistant.components.sensor import SensorEntity, SensorStateClass
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import (
+ ATTR_API_VERSION,
+ ATTR_LAST_UPDATED,
+ ATTR_PROJECT_ID,
+ ATTR_SITE_ID,
+ ATTR_SITE_URL,
+ DOMAIN,
+)
+from .coordinator import AurowatchDataUpdateCoordinator
+from .entity import AurowatchEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up the AuroraWatch sensor."""
+ coordinator = cast(
+ AurowatchDataUpdateCoordinator,
+ hass.data[DOMAIN][entry.entry_id],
+ )
+ async_add_entities(
+ [
+ AurowatchSensor(coordinator),
+ AurowatchActivitySensor(coordinator),
+ ]
+ )
+
+
+class AurowatchSensor(AurowatchEntity, SensorEntity):
+ """Representation of an AuroraWatch status sensor."""
+
+ def __init__(self, coordinator: AurowatchDataUpdateCoordinator) -> None:
+ """Initialize the sensor."""
+ super().__init__(coordinator, "aurora_status")
+
+ @property
+ def native_value(self) -> str | None:
+ """Return the state of the sensor."""
+ if self.coordinator.data:
+ return self.coordinator.data.get("status")
+ return None
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ data = self.coordinator.data or {}
+ return {
+ ATTR_LAST_UPDATED: data.get("last_updated"),
+ ATTR_PROJECT_ID: data.get("project_id"),
+ ATTR_SITE_ID: data.get("site_id"),
+ ATTR_SITE_URL: data.get("site_url"),
+ ATTR_API_VERSION: data.get("api_version"),
+ }
+
+
+class AurowatchActivitySensor(AurowatchEntity, SensorEntity):
+ """Representation of an AuroraWatch geomagnetic activity sensor."""
+
+ _attr_state_class = SensorStateClass.MEASUREMENT
+ _attr_native_unit_of_measurement = "nT"
+
+ def __init__(self, coordinator: AurowatchDataUpdateCoordinator) -> None:
+ """Initialize the sensor."""
+ super().__init__(coordinator, "geomagnetic_activity")
+
+ @property
+ def native_value(self) -> int | float | None:
+ """Return the geomagnetic activity value."""
+ if self.coordinator.data:
+ return self.coordinator.data.get("activity")
+ return None
diff --git a/homeassistant/components/aurorawatch/strings.json b/homeassistant/components/aurorawatch/strings.json
new file mode 100644
index 00000000000000..7b6890daa3485a
--- /dev/null
+++ b/homeassistant/components/aurorawatch/strings.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "AuroraWatch UK",
+ "description": "Monitor aurora activity from AuroraWatch UK. This integration will add a sensor showing the current aurora alert status (green, yellow, amber, or red)."
+ }
+ },
+ "abort": {
+ "already_configured": "AuroraWatch UK is already configured"
+ }
+ },
+ "entity": {
+ "sensor": {
+ "aurora_status": {
+ "name": "Aurora status"
+ },
+ "geomagnetic_activity": {
+ "name": "Geomagnetic activity"
+ }
+ }
+ }
+}
diff --git a/tests/components/aurorawatch/__init__.py b/tests/components/aurorawatch/__init__.py
new file mode 100644
index 00000000000000..728cee9699ccb4
--- /dev/null
+++ b/tests/components/aurorawatch/__init__.py
@@ -0,0 +1,13 @@
+"""The tests for the AuroraWatch UK integration."""
+
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry
+
+
+async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
+ """Fixture for setting up the component."""
+ config_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
diff --git a/tests/components/aurorawatch/conftest.py b/tests/components/aurorawatch/conftest.py
new file mode 100644
index 00000000000000..50f45439cc426f
--- /dev/null
+++ b/tests/components/aurorawatch/conftest.py
@@ -0,0 +1,105 @@
+"""Common fixtures for the AuroraWatch UK tests."""
+
+from collections.abc import Generator
+from unittest.mock import AsyncMock, Mock, patch
+
+import pytest
+
+from homeassistant.components.aurorawatch.const import DOMAIN
+
+from tests.common import MockConfigEntry
+
+# Sample XML responses for testing
+MOCK_STATUS_XML = """
+
+
+ 2024-01-15T12:00:00Z
+
+
+ No significant activity
+
+
+"""
+
+MOCK_ACTIVITY_XML = """
+
+
+ 2024-01-15T11:00:00Z
+ 45.2
+
+
+ 2024-01-15T12:00:00Z
+ 52.7
+
+
+"""
+
+MOCK_INVALID_STATUS_XML = """
+
+
+
+"""
+
+MOCK_MALFORMED_XML = """
+
+"""
+
+
+@pytest.fixture
+def mock_setup_entry() -> Generator[AsyncMock]:
+ """Override async_setup_entry."""
+ with patch(
+ "homeassistant.components.aurorawatch.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ yield mock_setup_entry
+
+
+@pytest.fixture
+def mock_aiohttp_session() -> Generator[AsyncMock]:
+ """Mock aiohttp session."""
+ with patch(
+ "homeassistant.components.aurorawatch.coordinator.async_get_clientsession"
+ ) as mock_session_factory:
+ mock_session = AsyncMock()
+ mock_session_factory.return_value = mock_session
+
+ # Create mock responses
+ mock_status_response = AsyncMock()
+ mock_status_response.text = AsyncMock(return_value=MOCK_STATUS_XML)
+ mock_status_response.raise_for_status = Mock()
+
+ mock_activity_response = AsyncMock()
+ mock_activity_response.text = AsyncMock(return_value=MOCK_ACTIVITY_XML)
+ mock_activity_response.raise_for_status = Mock()
+
+ # Mock session.get to return different responses based on URL
+ async def mock_get(url: str, *args, **kwargs):
+ """Return an async context manager yielding the appropriate response."""
+ if "current-status.xml" in url:
+ response = mock_status_response
+ elif "sum-activity.xml" in url:
+ response = mock_activity_response
+ else:
+ response = AsyncMock()
+
+ context_manager = AsyncMock()
+ context_manager.__aenter__.return_value = response
+ context_manager.__aexit__.return_value = AsyncMock(return_value=None)
+
+ return context_manager
+
+ mock_session.get = mock_get
+
+ yield mock_session
+
+
+@pytest.fixture
+def mock_config_entry() -> MockConfigEntry:
+ """Mock a config entry."""
+ return MockConfigEntry(
+ domain=DOMAIN,
+ title="AuroraWatch UK",
+ data={},
+ unique_id=DOMAIN,
+ )
diff --git a/tests/components/aurorawatch/test_config_flow.py b/tests/components/aurorawatch/test_config_flow.py
new file mode 100644
index 00000000000000..a9c4a3129d5ba7
--- /dev/null
+++ b/tests/components/aurorawatch/test_config_flow.py
@@ -0,0 +1,58 @@
+"""Test the AuroraWatch UK config flow."""
+
+from unittest.mock import AsyncMock
+
+import pytest
+
+from homeassistant.components.aurorawatch.const import DOMAIN
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResultType
+
+from tests.common import MockConfigEntry
+
+
+async def test_full_flow(
+ hass: HomeAssistant,
+ mock_setup_entry: AsyncMock,
+ mock_aiohttp_session: AsyncMock,
+) -> None:
+ """Test full config flow."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] is FlowResultType.FORM
+ assert result["errors"] == {}
+ assert result["step_id"] == "user"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={},
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] is FlowResultType.CREATE_ENTRY
+ assert result["title"] == "AuroraWatch UK"
+ assert result["data"] == {}
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_flow_already_configured(
+ hass: HomeAssistant,
+ mock_aiohttp_session: AsyncMock,
+) -> None:
+ """Test config flow aborts if already configured."""
+ mock_entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="AuroraWatch UK",
+ data={},
+ unique_id=DOMAIN,
+ )
+ mock_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ assert result["type"] is FlowResultType.ABORT
+ assert result["reason"] == "already_configured"
diff --git a/tests/components/aurorawatch/test_sensor.py b/tests/components/aurorawatch/test_sensor.py
new file mode 100644
index 00000000000000..f3b791d702c73a
--- /dev/null
+++ b/tests/components/aurorawatch/test_sensor.py
@@ -0,0 +1,214 @@
+"""Test the AuroraWatch UK sensor platform."""
+
+from unittest.mock import AsyncMock
+
+from aiohttp import ClientError
+import pytest
+
+from homeassistant.components.aurorawatch.const import (
+ ATTR_API_VERSION,
+ ATTR_LAST_UPDATED,
+ ATTR_PROJECT_ID,
+ ATTR_SITE_ID,
+ ATTR_SITE_URL,
+ DOMAIN,
+)
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.config_entries import ConfigEntryState
+from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from . import setup_integration
+
+from tests.common import MockConfigEntry
+
+
+async def test_sensor_setup(
+ hass: HomeAssistant,
+ mock_aiohttp_session: AsyncMock,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test sensor setup."""
+ await setup_integration(hass, mock_config_entry)
+
+ entity_registry = er.async_get(hass)
+
+ # Check that both sensors are created
+ status_sensor = entity_registry.async_get("sensor.aurorawatch_uk_aurora_status")
+ assert status_sensor
+ assert status_sensor.unique_id == "aurorawatch_aurora_status"
+ assert status_sensor.translation_key == "aurora_status"
+
+ activity_sensor = entity_registry.async_get(
+ "sensor.aurorawatch_uk_geomagnetic_activity"
+ )
+ assert activity_sensor
+ assert activity_sensor.unique_id == "aurorawatch_geomagnetic_activity"
+ assert activity_sensor.translation_key == "geomagnetic_activity"
+
+
+async def test_sensor_states(
+ hass: HomeAssistant,
+ mock_aiohttp_session: AsyncMock,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test sensor states."""
+ await setup_integration(hass, mock_config_entry)
+
+ # Check status sensor state
+ status_state = hass.states.get("sensor.aurorawatch_uk_aurora_status")
+ assert status_state
+ assert status_state.state == "green"
+
+ # Check activity sensor state
+ activity_state = hass.states.get("sensor.aurorawatch_uk_geomagnetic_activity")
+ assert activity_state
+ assert activity_state.state == "52.7"
+ assert activity_state.attributes.get("unit_of_measurement") == "nT"
+ assert activity_state.attributes.get("state_class") == "measurement"
+
+
+async def test_sensor_attributes(
+ hass: HomeAssistant,
+ mock_aiohttp_session: AsyncMock,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test sensor attributes."""
+ await setup_integration(hass, mock_config_entry)
+
+ # Check status sensor attributes
+ status_state = hass.states.get("sensor.aurorawatch_uk_aurora_status")
+ assert status_state
+ assert status_state.attributes.get(ATTR_LAST_UPDATED) == "2024-01-15T12:00:00Z"
+ assert status_state.attributes.get(ATTR_PROJECT_ID) == "awn"
+ assert status_state.attributes.get(ATTR_SITE_ID) == "lancaster"
+ assert (
+ status_state.attributes.get(ATTR_SITE_URL) == "http://aurorawatch.lancs.ac.uk"
+ )
+ assert status_state.attributes.get(ATTR_API_VERSION) == "0.2"
+ assert (
+ status_state.attributes.get("attribution") == "Data provided by AuroraWatch UK"
+ )
+
+
+async def test_sensor_update_failure_network_error(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test sensor update with network error."""
+ from unittest.mock import patch
+
+ with patch(
+ "homeassistant.components.aurorawatch.coordinator.async_get_clientsession"
+ ) as mock_session_factory:
+ mock_session = AsyncMock()
+ mock_session_factory.return_value = mock_session
+
+ # Mock network error
+ mock_session.get.side_effect = ClientError("Connection failed")
+
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Initial setup should fail and transition the entry to SETUP_RETRY,
+ # and entities should not be created yet.
+ entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
+ assert entry is not None
+ assert entry.state is ConfigEntryState.SETUP_RETRY
+
+ status_state = hass.states.get("sensor.aurorawatch_uk_aurora_status")
+ assert status_state is None
+
+ activity_state = hass.states.get("sensor.aurorawatch_uk_geomagnetic_activity")
+ assert activity_state is None
+
+
+async def test_sensor_update_failure_invalid_xml(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test sensor update with invalid XML."""
+ from unittest.mock import Mock, patch
+
+ from tests.components.aurorawatch.conftest import MOCK_MALFORMED_XML
+
+ with patch(
+ "homeassistant.components.aurorawatch.coordinator.async_get_clientsession"
+ ) as mock_session_factory:
+ mock_session = AsyncMock()
+ mock_session_factory.return_value = mock_session
+
+ # Mock malformed XML response
+ mock_response = AsyncMock()
+ mock_response.text = AsyncMock(return_value=MOCK_MALFORMED_XML)
+ mock_response.raise_for_status = Mock()
+ mock_session.get.return_value = mock_response
+
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Initial setup should fail due to parsing error; entry should be in
+ # SETUP_RETRY and entities should not be created yet.
+ entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
+ assert entry is not None
+ assert entry.state is ConfigEntryState.SETUP_RETRY
+
+ status_state = hass.states.get("sensor.aurorawatch_uk_aurora_status")
+ assert status_state is None
+
+
+async def test_sensor_update_failure_missing_fields(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test sensor update with missing required fields."""
+ from unittest.mock import Mock, patch
+
+ from tests.components.aurorawatch.conftest import MOCK_INVALID_STATUS_XML
+
+ with patch(
+ "homeassistant.components.aurorawatch.coordinator.async_get_clientsession"
+ ) as mock_session_factory:
+ mock_session = AsyncMock()
+ mock_session_factory.return_value = mock_session
+
+ # Mock invalid XML response (missing required fields)
+ mock_response = AsyncMock()
+ mock_response.text = AsyncMock(return_value=MOCK_INVALID_STATUS_XML)
+ mock_response.raise_for_status = Mock()
+ mock_session.get.return_value = mock_response
+
+ mock_config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Sensors should be unavailable due to missing fields
+ status_state = hass.states.get("sensor.aurorawatch_uk_aurora_status")
+ assert status_state
+ assert status_state.state == STATE_UNAVAILABLE
+
+
+async def test_sensor_device_info(
+ hass: HomeAssistant,
+ mock_aiohttp_session: AsyncMock,
+ mock_config_entry: MockConfigEntry,
+) -> None:
+ """Test sensor device info."""
+ await setup_integration(hass, mock_config_entry)
+
+ entity_registry = er.async_get(hass)
+ status_sensor = entity_registry.async_get("sensor.aurorawatch_uk_aurora_status")
+ assert status_sensor
+ assert status_sensor.device_id
+
+ from homeassistant.helpers import device_registry as dr
+
+ device_registry = dr.async_get(hass)
+ device = device_registry.async_get(status_sensor.device_id)
+ assert device
+ assert device.manufacturer == "Lancaster University"
+ assert device.model == "AuroraWatch UK"
+ assert device.name == "AuroraWatch UK"