Skip to content

Commit

Permalink
Merge pull request #64 from firstof9/device-tracker
Browse files Browse the repository at this point in the history
feat: add device trackers to GPS location checking
  • Loading branch information
finity69x2 authored May 9, 2023
2 parents 9091a68 + 826b44b commit 6a0bdbd
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 62 deletions.
95 changes: 61 additions & 34 deletions custom_components/nws_alerts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
CONF_GPS_LOC,
CONF_INTERVAL,
CONF_TIMEOUT,
CONF_TRACKER,
CONF_ZONE_ID,
COORDINATOR,
DEFAULT_INTERVAL,
Expand Down Expand Up @@ -48,9 +49,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b

ent_reg = async_get(hass)
for entity in async_entries_for_config_entry(ent_reg, config_entry.entry_id):
ent_reg.async_update_entity(entity.entity_id, new_unique_id=config_entry.entry_id)
ent_reg.async_update_entity(
entity.entity_id, new_unique_id=config_entry.entry_id
)

updated_config = config_entry.data.copy()

# Strip spaces from manually entered GPS locations
if CONF_GPS_LOC in updated_config:
updated_config[CONF_GPS_LOC].replace(" ", "")

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

config_entry.add_update_listener(update_listener)

# Setup the data coordinator
coordinator = AlertsDataUpdateCoordinator(
Expand Down Expand Up @@ -89,7 +101,7 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
if config_entry.data == config_entry.options:
_LOGGER.debug("No changes detected not reloading sensors.")
return

new_data = config_entry.options.copy()
hass.config_entries.async_update_entry(
entry=config_entry,
Expand Down Expand Up @@ -139,58 +151,73 @@ def __init__(self, hass, config, the_timeout: int, interval: int):

async def _async_update_data(self):
"""Fetch data"""
coords = None
if CONF_TRACKER in self.config:
coords = await self._get_tracker_gps()
async with timeout(self.timeout):
try:
data = await update_alerts(self.config)
data = await update_alerts(self.config, coords)
except Exception as error:
raise UpdateFailed(error) from error
return data

async def _get_tracker_gps(self):
"""Return device tracker GPS data."""
tracker = self.config[CONF_TRACKER]
entity = self.hass.states.get(tracker)
if entity and "source_type" in entity.attributes:
return f"{entity.attributes['latitude']},{entity.attributes['longitude']}"
return None


async def update_alerts(config) -> dict:
async def update_alerts(config, coords) -> dict:
"""Fetch new state data for the sensor.
This is the only method that should fetch new data for Home Assistant.
"""

data = await async_get_state(config)
data = await async_get_state(config, coords)
return data


async def async_get_state(config) -> dict:
async def async_get_state(config, coords) -> dict:
"""Query API for status."""

zone_id = ""
gps_loc = ""
url = "%s/alerts/active/count" % API_ENDPOINT
values = {}
values = {
"state": 0,
"event": None,
"event_id": None,
"message_type": None,
"event_status": None,
"event_severity": None,
"event_expires": None,
"display_desc": None,
"spoken_desc": None,
}
headers = {"User-Agent": USER_AGENT, "Accept": "application/ld+json"}
data = None

if CONF_ZONE_ID in config:
zone_id = config[CONF_ZONE_ID]
_LOGGER.debug("getting state for %s from %s" % (zone_id, url))
elif CONF_GPS_LOC in config:
gps_loc = config[CONF_GPS_LOC]
elif CONF_GPS_LOC in config or CONF_TRACKER in config:
if coords is not None:
gps_loc = coords
else:
gps_loc = config[CONF_GPS_LOC].replace(" ", "")
_LOGGER.debug("getting state for %s from %s" % (gps_loc, url))

async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as r:
if r.status == 200:
data = await r.json()
else:
_LOGGER.error("Problem updating NWS data: (%s) - %s", r.status, r.body)

if data is not None:
# Reset values before reassigning
values = {
"state": 0,
"event": None,
"event_id": None,
"message_type": None,
"event_status": None,
"event_severity": None,
"event_expires": None,
"display_desc": None,
"spoken_desc": None,
}
if "zones" in data and zone_id != "":
for zone in zone_id.split(","):
if zone in data["zones"]:
Expand All @@ -206,7 +233,17 @@ async def async_get_alerts(zone_id: str = "", gps_loc: str = "") -> dict:
"""Query API for Alerts."""

url = ""
values = {}
values = {
"state": 0,
"event": None,
"event_id": None,
"message_type": None,
"event_status": None,
"event_severity": None,
"event_expires": None,
"display_desc": None,
"spoken_desc": None,
}
headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
data = None

Expand All @@ -221,6 +258,8 @@ async def async_get_alerts(zone_id: str = "", gps_loc: str = "") -> dict:
async with session.get(url, headers=headers) as r:
if r.status == 200:
data = await r.json()
else:
_LOGGER.error("Problem updating NWS data: (%s) - %s", r.status, r.body)

if data is not None:
events = []
Expand Down Expand Up @@ -326,17 +365,5 @@ async def async_get_alerts(zone_id: str = "", gps_loc: str = "") -> dict:
values["event_expires"] = event_expires
values["display_desc"] = display_desc
values["spoken_desc"] = spoken_desc
else:
values = {
"state": 0,
"event": None,
"event_id": None,
"message_type": None,
"event_status": None,
"event_severity": None,
"event_expires": None,
"display_desc": None,
"spoken_desc": None,
}

return values
96 changes: 93 additions & 3 deletions custom_components/nws_alerts/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
from __future__ import annotations

import logging
from typing import Any
from typing import Any, List

import aiohttp
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult

from .const import (
API_ENDPOINT,
CONF_GPS_LOC,
CONF_INTERVAL,
CONF_TIMEOUT,
CONF_TRACKER,
CONF_ZONE_ID,
DEFAULT_INTERVAL,
DEFAULT_NAME,
Expand All @@ -29,7 +31,8 @@
JSON_ID = "id"

_LOGGER = logging.getLogger(__name__)
MENU_OPTIONS = ["zone", "gps_loc"]
MENU_OPTIONS = ["zone", "gps"]
MENU_GPS = ["gps_loc", "gps_tracker"]


def _get_schema_zone(hass: Any, user_input: list, default_dict: list) -> Any:
Expand Down Expand Up @@ -70,6 +73,48 @@ def _get_default(key):
)


def _get_schema_tracker(hass: Any, user_input: list, default_dict: list) -> Any:
"""Gets a schema using the default_dict as a backup."""
if user_input is None:
user_input = {}

def _get_default(key: str, fallback_default: Any = None) -> None:
"""Gets default value for key."""
return user_input.get(key, default_dict.get(key, fallback_default))

return vol.Schema(
{
vol.Required(
CONF_TRACKER, default=_get_default(CONF_TRACKER, "(none)")
): vol.In(_get_entities(hass, TRACKER_DOMAIN)),
vol.Optional(CONF_NAME, default=_get_default(CONF_NAME)): str,
vol.Optional(CONF_INTERVAL, default=_get_default(CONF_INTERVAL)): int,
vol.Optional(CONF_TIMEOUT, default=_get_default(CONF_TIMEOUT)): int,
}
)


def _get_entities(
hass: HomeAssistant,
domain: str,
search: List[str] = None,
extra_entities: List[str] = None,
) -> List[str]:
data = ["(none)"]
if domain not in hass.data:
return data

for entity in hass.data[domain].entities:
if search is not None and not any(map(entity.entity_id.__contains__, search)):
continue
data.append(entity.entity_id)

if extra_entities:
data.extend(extra_entities)

return data


async def _get_zone_list(self) -> list | None:
"""Return list of zone by lat/lon"""

Expand Down Expand Up @@ -127,6 +172,36 @@ async def async_step_user(
"""Handle the flow initialized by the user."""
return self.async_show_menu(step_id="user", menu_options=MENU_OPTIONS)

async def async_step_gps(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the flow initialized by the user."""
return self.async_show_menu(step_id="gps", menu_options=MENU_GPS)

async def async_step_gps_tracker(self, user_input={}):
"""Handle a flow for device trackers."""
self._errors = {}
if user_input is not None:
self._data.update(user_input)
return self.async_create_entry(title=self._data[CONF_NAME], data=self._data)
return await self._show_config_gps_tracker(user_input)

async def _show_config_gps_tracker(self, user_input):
"""Show the configuration form to edit location data."""

# Defaults
defaults = {
CONF_NAME: DEFAULT_NAME,
CONF_INTERVAL: DEFAULT_INTERVAL,
CONF_TIMEOUT: DEFAULT_TIMEOUT,
}

return self.async_show_form(
step_id="gps_tracker",
data_schema=_get_schema_tracker(self.hass, user_input, defaults),
errors=self._errors,
)

async def async_step_gps_loc(self, user_input={}):
"""Handle a flow initialized by the user."""
lat = self.hass.config.latitude
Expand Down Expand Up @@ -214,6 +289,15 @@ async def async_step_gps_loc(self, user_input={}):
return self.async_create_entry(title="", data=self._data)
return await self._show_options_form(user_input)

async def async_step_gps_tracker(self, user_input={}):
"""Handle a flow initialized by the user."""
self._errors = {}

if user_input is not None:
self._data.update(user_input)
return self.async_create_entry(title="", data=self._data)
return await self._show_options_form(user_input)

async def async_step_zone(self, user_input={}):
"""Handle a flow initialized by the user."""
self._errors = {}
Expand All @@ -238,3 +322,9 @@ async def _show_options_form(self, user_input):
data_schema=_get_schema_zone(self.hass, user_input, self._data),
errors=self._errors,
)
elif CONF_TRACKER in self.config.data:
return self.async_show_form(
step_id="gps_tracker",
data_schema=_get_schema_tracker(self.hass, user_input, self._data),
errors=self._errors,
)
1 change: 1 addition & 0 deletions custom_components/nws_alerts/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
CONF_INTERVAL = "interval"
CONF_ZONE_ID = "zone_id"
CONF_GPS_LOC = "gps_loc"
CONF_TRACKER = "tracker"

# Defaults
DEFAULT_ICON = "mdi:alert"
Expand Down
10 changes: 5 additions & 5 deletions custom_components/nws_alerts/manifest.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"domain": "nws_alerts",
"name": "NWS Alerts",
"version": "2.8",
"documentation": "https://github.com/finity69x2/nws_alerts/",
"issue_tracker": "https://github.com/finity69x2/nws_alerts/issues",
"dependencies": [],
"codeowners": ["@finity69x2"],
"config_flow": true,
"documentation": "https://github.com/finity69x2/nws_alerts/",
"dependencies": [],
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/finity69x2/nws_alerts/issues",
"requirements": [],
"iot_class": "cloud_polling"
"version": "2.8"
}
14 changes: 10 additions & 4 deletions custom_components/nws_alerts/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
CONF_GPS_LOC,
CONF_INTERVAL,
CONF_TIMEOUT,
CONF_TRACKER,
CONF_ZONE_ID,
COORDINATOR,
DEFAULT_ICON,
Expand All @@ -40,6 +41,7 @@
{
vol.Optional(CONF_ZONE_ID): cv.string,
vol.Optional(CONF_GPS_LOC): cv.string,
vol.Optional(CONF_TRACKER): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): int,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): int,
Expand All @@ -55,16 +57,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
config.entry_id = slugify(f"{config.get(CONF_ZONE_ID)}")
elif CONF_GPS_LOC in config:
config.entry_id = slugify(f"{config.get(CONF_GPS_LOC)}")
elif CONF_GPS_LOC and CONF_ZONE_ID not in config:
raise ValueError("GPS or Zone needs to be configured.")
elif CONF_TRACKER in config:
config.entry_id = slugify(f"{config.get(CONF_TRACKER)}")
else:
raise ValueError("GPS, Zone or Device Tracker needs to be configured.")
config.data = config
else:
if CONF_ZONE_ID in config:
config.entry_id = slugify(f"{config.get(CONF_ZONE_ID)}")
elif CONF_GPS_LOC in config:
config.entry_id = slugify(f"{config.get(CONF_GPS_LOC)}")
elif CONF_GPS_LOC and CONF_ZONE_ID not in config:
raise ValueError("GPS or Zone needs to be configured.")
elif CONF_TRACKER in config:
config.entry_id = slugify(f"{config.get(CONF_TRACKER)}")
else:
raise ValueError("GPS, Zone or Device Tracker needs to be configured.")
config.data = config

# Setup the data coordinator
Expand Down
Loading

0 comments on commit 6a0bdbd

Please sign in to comment.