Skip to content

Commit be9bbd2

Browse files
author
Steve
committed
1 parent 67bdeb9 commit be9bbd2

File tree

11 files changed

+363
-0
lines changed

11 files changed

+363
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""The AuroraWatch UK integration."""
2+
import logging
3+
4+
from homeassistant.config_entries import ConfigEntry
5+
from homeassistant.const import Platform
6+
from homeassistant.core import HomeAssistant
7+
8+
from .const import DOMAIN
9+
from .coordinator import AurowatchDataUpdateCoordinator
10+
11+
_LOGGER = logging.getLogger(__name__)
12+
13+
PLATFORMS = [Platform.SENSOR]
14+
15+
16+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
17+
"""Set up AuroraWatch UK from a config entry."""
18+
coordinator = AurowatchDataUpdateCoordinator(hass)
19+
20+
# Fetch initial data
21+
await coordinator.async_config_entry_first_refresh()
22+
23+
# Store coordinator
24+
hass.data.setdefault(DOMAIN, {})
25+
hass.data[DOMAIN][entry.entry_id] = coordinator
26+
27+
# Forward entry setup to sensor platform
28+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
29+
30+
return True
31+
32+
33+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
34+
"""Unload a config entry."""
35+
# Unload platforms
36+
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
37+
38+
# Remove coordinator from hass.data
39+
if unload_ok:
40+
hass.data[DOMAIN].pop(entry.entry_id)
41+
42+
return unload_ok
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Config flow for AuroraWatch UK integration."""
2+
import logging
3+
4+
from homeassistant import config_entries
5+
from homeassistant.core import HomeAssistant
6+
from homeassistant.data_entry_flow import FlowResult
7+
8+
from .const import DOMAIN
9+
10+
_LOGGER = logging.getLogger(__name__)
11+
12+
13+
class AurowatchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
14+
"""Handle a config flow for AuroraWatch UK."""
15+
16+
VERSION = 1
17+
18+
async def async_step_user(self, user_input=None) -> FlowResult:
19+
"""Handle the initial step."""
20+
# Check if already configured
21+
await self.async_set_unique_id(DOMAIN)
22+
self._abort_if_unique_id_configured()
23+
24+
if user_input is None:
25+
# Show the form to confirm setup
26+
return self.async_show_form(step_id="user")
27+
28+
# Create the config entry
29+
return self.async_create_entry(
30+
title="AuroraWatch UK",
31+
data={},
32+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Constants for the AuroraWatch UK integration."""
2+
from datetime import timedelta
3+
4+
DOMAIN = "aurorawatch"
5+
ATTRIBUTION = "Data provided by AuroraWatch UK"
6+
7+
# API Configuration
8+
API_URL = "http://aurorawatch-api.lancs.ac.uk/0.2/status/current-status.xml"
9+
API_ACTIVITY_URL = "https://aurorawatch-api.lancs.ac.uk/0.2/status/project/awn/sum-activity.xml"
10+
API_TIMEOUT = 10 # seconds
11+
12+
# Update Configuration
13+
UPDATE_INTERVAL = timedelta(minutes=5)
14+
15+
# Sensor Attributes
16+
ATTR_LAST_UPDATED = "last_updated"
17+
ATTR_PROJECT_ID = "project_id"
18+
ATTR_SITE_ID = "site_id"
19+
ATTR_SITE_URL = "site_url"
20+
ATTR_API_VERSION = "api_version"
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Data coordinator for AuroraWatch UK integration."""
2+
import logging
3+
import xml.etree.ElementTree as ET
4+
from datetime import timedelta
5+
6+
import async_timeout
7+
from homeassistant.core import HomeAssistant
8+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
9+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
10+
11+
from .const import API_ACTIVITY_URL, API_TIMEOUT, API_URL, UPDATE_INTERVAL
12+
13+
_LOGGER = logging.getLogger(__name__)
14+
15+
16+
class AurowatchDataUpdateCoordinator(DataUpdateCoordinator):
17+
"""Class to manage fetching AuroraWatch data from API."""
18+
19+
def __init__(self, hass: HomeAssistant) -> None:
20+
"""Initialize the coordinator."""
21+
super().__init__(
22+
hass,
23+
_LOGGER,
24+
name="AuroraWatch",
25+
update_interval=UPDATE_INTERVAL,
26+
)
27+
28+
async def _async_update_data(self):
29+
"""Fetch data from AuroraWatch API."""
30+
try:
31+
session = async_get_clientsession(self.hass)
32+
33+
# Fetch both status and activity data
34+
async with async_timeout.timeout(API_TIMEOUT):
35+
status_response = await session.get(API_URL)
36+
status_response.raise_for_status()
37+
status_xml = await status_response.text()
38+
39+
activity_response = await session.get(API_ACTIVITY_URL)
40+
activity_response.raise_for_status()
41+
activity_xml = await activity_response.text()
42+
43+
# Parse status XML
44+
try:
45+
status_root = ET.fromstring(status_xml)
46+
except ET.ParseError as err:
47+
_LOGGER.error("Failed to parse status XML response: %s", err)
48+
raise UpdateFailed(f"Invalid XML response: {err}") from err
49+
50+
# Parse activity XML
51+
try:
52+
activity_root = ET.fromstring(activity_xml)
53+
except ET.ParseError as err:
54+
_LOGGER.error("Failed to parse activity XML response: %s", err)
55+
raise UpdateFailed(f"Invalid activity XML response: {err}") from err
56+
57+
# Extract data
58+
try:
59+
# Get updated datetime from status
60+
datetime_element = status_root.find('.//updated/datetime')
61+
if datetime_element is None or datetime_element.text is None:
62+
raise UpdateFailed("Missing 'updated/datetime' element in XML")
63+
last_updated = datetime_element.text
64+
65+
# Get site status information
66+
status_element = status_root.find('.//site_status')
67+
if status_element is None:
68+
raise UpdateFailed("Missing 'site_status' element in XML")
69+
70+
status_id = status_element.get('status_id')
71+
if status_id is None:
72+
raise UpdateFailed("Missing 'status_id' attribute in site_status")
73+
74+
project_id = status_element.get('project_id', 'Unknown')
75+
site_id = status_element.get('site_id', 'Unknown')
76+
site_url = status_element.get('site_url', '')
77+
78+
# Get API version
79+
api_version = status_root.get('api_version', 'Unknown')
80+
81+
# Get current activity value (most recent in the list)
82+
activity_elements = activity_root.findall('.//activity')
83+
activity_value = None
84+
if activity_elements:
85+
# Get the last (most recent) activity reading
86+
last_activity = activity_elements[-1]
87+
value_element = last_activity.find('value')
88+
if value_element is not None and value_element.text is not None:
89+
activity_value = float(value_element.text)
90+
91+
data = {
92+
"status": status_id,
93+
"last_updated": last_updated,
94+
"project_id": project_id,
95+
"site_id": site_id,
96+
"site_url": site_url,
97+
"api_version": api_version,
98+
"activity": activity_value,
99+
}
100+
101+
_LOGGER.debug("Successfully fetched AuroraWatch data: %s", data)
102+
return data
103+
104+
except (AttributeError, KeyError) as err:
105+
_LOGGER.error("Failed to extract data from XML: %s", err)
106+
raise UpdateFailed(f"Failed to parse XML structure: {err}") from err
107+
108+
except Exception as err:
109+
_LOGGER.error("Error fetching AuroraWatch data: %s", err)
110+
raise UpdateFailed(f"Error communicating with API: {err}") from err
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""The AuroraWatch component."""
2+
3+
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
4+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
5+
6+
from .const import ATTRIBUTION, DOMAIN
7+
from .coordinator import AurowatchDataUpdateCoordinator
8+
9+
10+
class AurowatchEntity(CoordinatorEntity[AurowatchDataUpdateCoordinator]):
11+
"""Implementation of the base AuroraWatch Entity."""
12+
13+
_attr_attribution = ATTRIBUTION
14+
_attr_has_entity_name = True
15+
16+
def __init__(
17+
self,
18+
coordinator: AurowatchDataUpdateCoordinator,
19+
translation_key: str,
20+
) -> None:
21+
"""Initialize the AuroraWatch Entity."""
22+
23+
super().__init__(coordinator=coordinator)
24+
25+
self._attr_translation_key = translation_key
26+
self._attr_unique_id = f"{DOMAIN}_{translation_key}"
27+
self._attr_device_info = DeviceInfo(
28+
entry_type=DeviceEntryType.SERVICE,
29+
identifiers={(DOMAIN, DOMAIN)},
30+
manufacturer="Lancaster University",
31+
model="AuroraWatch UK",
32+
name="AuroraWatch UK",
33+
)
6.93 KB
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"entity": {
3+
"sensor": {
4+
"aurora_status": {
5+
"default": "mdi:weather-night"
6+
},
7+
"geomagnetic_activity": {
8+
"default": "mdi:chart-line"
9+
}
10+
}
11+
}
12+
}
6.93 KB
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"domain": "aurorawatch",
3+
"name": "AuroraWatch UK",
4+
"codeowners": ["@agentgonzo"],
5+
"config_flow": true,
6+
"documentation": "https://github.com/aurorawatch/homeassistant-aurorawatch",
7+
"integration_type": "service",
8+
"iot_class": "cloud_polling",
9+
"requirements": [],
10+
"version": "1.0.0"
11+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Sensor platform for AuroraWatch UK integration."""
2+
import logging
3+
4+
from homeassistant.components.sensor import SensorEntity, SensorStateClass
5+
from homeassistant.config_entries import ConfigEntry
6+
from homeassistant.core import HomeAssistant
7+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
8+
9+
from .const import (
10+
ATTR_API_VERSION,
11+
ATTR_LAST_UPDATED,
12+
ATTR_PROJECT_ID,
13+
ATTR_SITE_ID,
14+
ATTR_SITE_URL,
15+
DOMAIN,
16+
)
17+
from .coordinator import AurowatchDataUpdateCoordinator
18+
from .entity import AurowatchEntity
19+
20+
_LOGGER = logging.getLogger(__name__)
21+
22+
23+
async def async_setup_entry(
24+
hass: HomeAssistant,
25+
entry: ConfigEntry,
26+
async_add_entities: AddEntitiesCallback,
27+
) -> None:
28+
"""Set up the AuroraWatch sensor."""
29+
coordinator = hass.data[DOMAIN][entry.entry_id]
30+
async_add_entities([
31+
AurowatchSensor(coordinator),
32+
AurowatchActivitySensor(coordinator),
33+
])
34+
35+
36+
class AurowatchSensor(AurowatchEntity, SensorEntity):
37+
"""Representation of an AuroraWatch status sensor."""
38+
39+
def __init__(self, coordinator: AurowatchDataUpdateCoordinator) -> None:
40+
"""Initialize the sensor."""
41+
super().__init__(coordinator, "aurora_status")
42+
43+
@property
44+
def native_value(self):
45+
"""Return the state of the sensor."""
46+
if self.coordinator.data:
47+
return self.coordinator.data.get("status")
48+
return None
49+
50+
@property
51+
def extra_state_attributes(self):
52+
"""Return the state attributes."""
53+
if not self.coordinator.data:
54+
return {}
55+
56+
return {
57+
ATTR_LAST_UPDATED: self.coordinator.data.get("last_updated"),
58+
ATTR_PROJECT_ID: self.coordinator.data.get("project_id"),
59+
ATTR_SITE_ID: self.coordinator.data.get("site_id"),
60+
ATTR_SITE_URL: self.coordinator.data.get("site_url"),
61+
ATTR_API_VERSION: self.coordinator.data.get("api_version"),
62+
}
63+
64+
65+
class AurowatchActivitySensor(AurowatchEntity, SensorEntity):
66+
"""Representation of an AuroraWatch geomagnetic activity sensor."""
67+
68+
_attr_state_class = SensorStateClass.MEASUREMENT
69+
_attr_native_unit_of_measurement = "nT"
70+
71+
def __init__(self, coordinator: AurowatchDataUpdateCoordinator) -> None:
72+
"""Initialize the sensor."""
73+
super().__init__(coordinator, "geomagnetic_activity")
74+
75+
@property
76+
def native_value(self):
77+
"""Return the geomagnetic activity value."""
78+
if self.coordinator.data:
79+
return self.coordinator.data.get("activity")
80+
return None

0 commit comments

Comments
 (0)