Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 38 additions & 0 deletions homeassistant/components/aurorawatch/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Check failure on line 9 in homeassistant/components/aurorawatch/__init__.py

View workflow job for this annotation

GitHub Actions / Run prek checks

ruff (F401)

homeassistant/components/aurorawatch/__init__.py:9:20: F401 `.const.DOMAIN` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
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()

Comment on lines +17 to +23
# 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)

Comment thread
agentgonzo marked this conversation as resolved.
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

Check failure on line 38 in homeassistant/components/aurorawatch/__init__.py

View workflow job for this annotation

GitHub Actions / Run prek checks

ruff (RET504)

homeassistant/components/aurorawatch/__init__.py:38:12: RET504 Unnecessary assignment to `unload_ok` before `return` statement
34 changes: 34 additions & 0 deletions homeassistant/components/aurorawatch/config_flow.py
Original file line number Diff line number Diff line change
@@ -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
Comment thread
agentgonzo marked this conversation as resolved.
MINOR_VERSION = 1

async def async_step_user(self, user_input=None) -> FlowResult:

Check failure on line 20 in homeassistant/components/aurorawatch/config_flow.py

View workflow job for this annotation

GitHub Actions / Check mypy

Return type "Coroutine[Any, Any, FlowResult[FlowContext, str]]" of "async_step_user" incompatible with return type "Coroutine[Any, Any, ConfigFlowResult]" in supertype "homeassistant.config_entries.ConfigFlow" [override]

Check warning on line 20 in homeassistant/components/aurorawatch/config_flow.py

View workflow job for this annotation

GitHub Actions / Check pylint

W7432: Return type should be ConfigFlowResult in async_step_user (hass-return-type)

Check warning on line 20 in homeassistant/components/aurorawatch/config_flow.py

View workflow job for this annotation

GitHub Actions / Check pylint

W7432: Return type should be ConfigFlowResult in async_step_user (hass-return-type)
"""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")

Check failure on line 28 in homeassistant/components/aurorawatch/config_flow.py

View workflow job for this annotation

GitHub Actions / Check mypy

Incompatible return value type (got "ConfigFlowResult", expected "FlowResult[FlowContext, str]") [return-value]

# Create the config entry
return self.async_create_entry(

Check failure on line 31 in homeassistant/components/aurorawatch/config_flow.py

View workflow job for this annotation

GitHub Actions / Check mypy

Incompatible return value type (got "ConfigFlowResult", expected "FlowResult[FlowContext, str]") [return-value]
title="AuroraWatch UK",
data={},
)
23 changes: 23 additions & 0 deletions homeassistant/components/aurorawatch/const.py
Original file line number Diff line number Diff line change
@@ -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"
117 changes: 117 additions & 0 deletions homeassistant/components/aurorawatch/coordinator.py
Original file line number Diff line number Diff line change
@@ -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,
)
Comment thread
agentgonzo marked this conversation as resolved.

async def _async_update_data(self):
"""Fetch data from AuroraWatch API."""
Comment on lines +18 to +36
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

Check failure on line 109 in homeassistant/components/aurorawatch/coordinator.py

View workflow job for this annotation

GitHub Actions / Run prek checks

ruff (TRY300)

homeassistant/components/aurorawatch/coordinator.py:109:17: TRY300 Consider moving this statement to an `else` block

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
Comment on lines +115 to +117
Comment on lines +115 to +117
Comment thread
agentgonzo marked this conversation as resolved.
45 changes: 45 additions & 0 deletions homeassistant/components/aurorawatch/entity.py
Original file line number Diff line number Diff line change
@@ -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",
Comment on lines +27 to +42
model="AuroraWatch UK",
name="AuroraWatch UK",
)
Binary file added homeassistant/components/aurorawatch/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions homeassistant/components/aurorawatch/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"entity": {
"sensor": {
"aurora_status": {
"default": "mdi:weather-night"
},
"geomagnetic_activity": {
"default": "mdi:chart-line"
}
}
}
}
Binary file added homeassistant/components/aurorawatch/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions homeassistant/components/aurorawatch/manifest.json
Original file line number Diff line number Diff line change
@@ -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": []
}
85 changes: 85 additions & 0 deletions homeassistant/components/aurorawatch/sensor.py
Original file line number Diff line number Diff line change
@@ -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,

Check warning on line 28 in homeassistant/components/aurorawatch/sensor.py

View workflow job for this annotation

GitHub Actions / Check pylint

W7431: Argument 3 should be of type AddConfigEntryEntitiesCallback in async_setup_entry (hass-argument-type)

Check warning on line 28 in homeassistant/components/aurorawatch/sensor.py

View workflow job for this annotation

GitHub Actions / Check pylint

W7431: Argument 3 should be of type AddConfigEntryEntitiesCallback in async_setup_entry (hass-argument-type)
) -> None:
"""Set up the AuroraWatch sensor."""
coordinator = cast(
AurowatchDataUpdateCoordinator,
hass.data[DOMAIN][entry.entry_id],
)
Comment on lines +30 to +34
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):

Check warning on line 58 in homeassistant/components/aurorawatch/sensor.py

View workflow job for this annotation

GitHub Actions / Check pylint

W7432: Return type should be ['Mapping[str, Any]', None] in extra_state_attributes (hass-return-type)

Check warning on line 58 in homeassistant/components/aurorawatch/sensor.py

View workflow job for this annotation

GitHub Actions / Check pylint

W7432: Return type should be ['Mapping[str, Any]', None] in extra_state_attributes (hass-return-type)
"""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"),
}
Comment on lines +57 to +67


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
23 changes: 23 additions & 0 deletions homeassistant/components/aurorawatch/strings.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Loading
Loading