Skip to content

Commit

Permalink
fix: fix entity availability due to NWS API issues
Browse files Browse the repository at this point in the history
  • Loading branch information
firstof9 committed Dec 23, 2024
1 parent 2e4a2ee commit f70e364
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 145 deletions.
23 changes: 14 additions & 9 deletions custom_components/nws_alerts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import hashlib
import logging
import uuid
from datetime import timedelta
from datetime import datetime, timedelta

import aiohttp
from async_timeout import timeout
Expand All @@ -23,6 +23,7 @@
CONF_TIMEOUT,
CONF_TRACKER,
CONF_ZONE_ID,
CONFIG_VERSION,
COORDINATOR,
DEFAULT_INTERVAL,
DEFAULT_TIMEOUT,
Expand Down Expand Up @@ -124,11 +125,10 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
if CONF_TIMEOUT not in updated_config.keys():
updated_config[CONF_TIMEOUT] = DEFAULT_TIMEOUT

if updated_config != config_entry.data:
hass.config_entries.async_update_entry(config_entry, data=updated_config)
if updated_config != config_entry.data:
hass.config_entries.async_update_entry(config_entry, data=updated_config)

config_entry.version = 2
_LOGGER.debug("Migration to version %s complete", config_entry.version)
_LOGGER.debug("Migration to version %s complete", CONFIG_VERSION)

return True

Expand Down Expand Up @@ -157,7 +157,9 @@ async def _async_update_data(self):
try:
data = await update_alerts(self.config, coords)
except AttributeError:
_LOGGER.debug("Error fetching most recent data from NWS Alerts API; will continue trying")
_LOGGER.debug(
"Error fetching most recent data from NWS Alerts API; will continue trying"
)
data = "AttributeError"
except Exception as error:
raise UpdateFailed(error) from error
Expand Down Expand Up @@ -268,16 +270,18 @@ async def async_get_alerts(zone_id: str = "", gps_loc: str = "") -> dict:
# Generate stable Alert ID
id = await generate_id(alert["id"])

tmp_dict["Event"] = alert["properties"]["event"]
tmp_dict["Event"] = alert["properties"]["event"]
tmp_dict["ID"] = id
tmp_dict["URL"] = alert["id"]

event = alert["properties"]["event"]
if "NWSheadline" in alert["properties"]["parameters"]:
tmp_dict["Headline"] = alert["properties"]["parameters"]["NWSheadline"][0]
tmp_dict["Headline"] = alert["properties"]["parameters"]["NWSheadline"][
0
]
else:
tmp_dict["Headline"] = event

tmp_dict["Type"] = alert["properties"]["messageType"]
tmp_dict["Status"] = alert["properties"]["status"]
tmp_dict["Severity"] = alert["properties"]["severity"]
Expand All @@ -294,6 +298,7 @@ async def async_get_alerts(zone_id: str = "", gps_loc: str = "") -> dict:

alerts["state"] = len(features)
alerts["alerts"] = alert_list
alerts["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

return alerts

Expand Down
3 changes: 2 additions & 1 deletion custom_components/nws_alerts/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
CONF_TIMEOUT,
CONF_TRACKER,
CONF_ZONE_ID,
CONFIG_VERSION,
DEFAULT_INTERVAL,
DEFAULT_NAME,
DEFAULT_TIMEOUT,
Expand Down Expand Up @@ -150,7 +151,7 @@ async def _get_zone_list(self) -> list | None:
class NWSAlertsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for NWS Alerts."""

VERSION = 2
VERSION = CONFIG_VERSION
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

def __init__(self):
Expand Down
1 change: 1 addition & 0 deletions custom_components/nws_alerts/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@
ATTRIBUTION = "Data provided by Weather.gov"
COORDINATOR = "coordinator"
PLATFORMS = [Platform.SENSOR]
CONFIG_VERSION = 2 # Config flow version
84 changes: 34 additions & 50 deletions custom_components/nws_alerts/sensor.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,27 @@
import logging
import uuid
from typing import Final

import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
from homeassistant.core import (
callback,
HomeAssistant,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify

from . import AlertsDataUpdateCoordinator
from .const import (
ATTRIBUTION,
CONF_GPS_LOC,
CONF_INTERVAL,
CONF_TIMEOUT,
CONF_TRACKER,
CONF_ZONE_ID,
COORDINATOR,
DEFAULT_ICON,
DEFAULT_INTERVAL,
DEFAULT_NAME,
DEFAULT_TIMEOUT,
DOMAIN,
)
from .const import ATTRIBUTION, COORDINATOR, DOMAIN

SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
"state": SensorEntityDescription(key="state", name="Alerts", icon="mdi:alert"),
"last_updated": SensorEntityDescription(
name="Last Updated",
key="last_updated",
icon="mdi:update",
device_class=SensorDeviceClass.TIMESTAMP,
),
}

# ---------------------------------------------------------
# API Documentation
Expand All @@ -43,65 +35,57 @@

async def async_setup_entry(hass, entry, async_add_entities):
"""Setup the sensor platform."""
async_add_entities([NWSAlertSensor(hass, entry)], True)
sensors = [NWSAlertSensor(hass, entry, sensor) for sensor in SENSOR_TYPES.values()]
async_add_entities(sensors, True)


class NWSAlertSensor(CoordinatorEntity):
"""Representation of a Sensor."""

def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
sensor_description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(hass.data[DOMAIN][entry.entry_id][COORDINATOR])
self._config = entry
self._name = entry.data[CONF_NAME]
self._icon = DEFAULT_ICON
self.coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
self._key = sensor_description.key

self._attr_icon = sensor_description.icon
self._attr_name = sensor_description.name
self._attr_device_class = sensor_description.device_class

@property
def unique_id(self):
"""
Return a unique, Home Assistant friendly identifier for this entity.
"""
return f"{slugify(self._name)}_{self._config.entry_id}"

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._icon
return f"{slugify(self._attr_name)}_{self._config.entry_id}"

@property
def state(self) -> int | None:
"""Return the state of the sensor."""
if self.coordinator.data is None:
return None
elif "state" in self.coordinator.data.keys():
return int(self.coordinator.data["state"])
elif self._key in self.coordinator.data.keys():
return self.coordinator.data[self._key]
return None

@property
def extra_state_attributes(self):
"""Return the state message."""
attrs = {}

if self.coordinator.data is None:
return attrs
if "alerts" in self.coordinator.data and self._key == "state":
attrs["Alerts"] = self.coordinator.data["alerts"]

attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
x = 0
if "alerts" in self.coordinator.data:
attrs["Alerts"] = self.coordinator.data["alerts"]
return attrs

@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success

@property
def device_info(self) -> DeviceInfo:
"""Return device registry information."""
Expand Down
3 changes: 2 additions & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ black
isort
pytest
pytest-cov
pytest-homeassistant-custom-component
pytest-homeassistant-custom-component
aioresponses
7 changes: 3 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""Fixtures for tests"""

import pytest
from aioresponses import aioresponses

import os
from unittest.mock import patch

import os
import pytest
from aioresponses import aioresponses

pytest_plugins = "pytest_homeassistant_custom_component"

Expand Down
41 changes: 6 additions & 35 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,15 @@

import pytest
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_NAME
from homeassistant.helpers.entity_registry import async_get
from homeassistant.setup import async_setup_component
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.nws_alerts.const import CONF_ZONE_ID, DOMAIN
from custom_components.nws_alerts.const import DOMAIN
from tests.const import CONFIG_DATA, CONFIG_DATA_3

pytestmark = pytest.mark.asyncio


async def test_setup_entry(
hass,
):
async def test_setup_entry(hass, mock_api):
"""Test settting up entities."""
entry = MockConfigEntry(
domain=DOMAIN,
Expand All @@ -29,12 +24,12 @@ async def test_setup_entry(
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1


async def test_unload_entry(hass):
async def test_unload_entry(hass, mock_api):
"""Test unloading entities."""
entry = MockConfigEntry(
domain=DOMAIN,
Expand All @@ -46,39 +41,15 @@ async def test_unload_entry(hass):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1

assert await hass.config_entries.async_unload(entries[0].entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
assert len(hass.states.async_entity_ids(DOMAIN)) == 0

assert await hass.config_entries.async_remove(entries[0].entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0


# async def test_import(hass):
# """Test importing a config."""
# entry = MockConfigEntry(
# domain=DOMAIN,
# title="NWS Alerts",
# data=CONFIG_DATA,
# )
# await async_setup_component(hass, "persistent_notification", {})
# with patch(
# "custom_components.nws_alerts.async_setup_entry",
# return_value=True,
# ) as mock_setup_entry:

# ent_reg = async_get(hass)
# ent_entry = ent_reg.async_get_or_create(
# "sensor", DOMAIN, unique_id="replaceable_unique_id", config_entry=entry
# )
# entity_id = ent_entry.entity_id
# entry.add_to_hass(hass)
# await hass.config_entries.async_setup(entry.entry_id)
# assert entry.unique_id is None
# assert ent_reg.async_get(entity_id).unique_id == entry.entry_id
Loading

0 comments on commit f70e364

Please sign in to comment.