Skip to content
Merged
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
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ omit =
homeassistant/components/flic/binary_sensor.py
homeassistant/components/flock/notify.py
homeassistant/components/flume/*
homeassistant/components/flunearyou/__init__.py
homeassistant/components/flunearyou/sensor.py
homeassistant/components/flux_led/light.py
homeassistant/components/folder/sensor.py
Expand Down
21 changes: 21 additions & 0 deletions homeassistant/components/flunearyou/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "These coordinates are already registered."
},
"error": {
"general_error": "There was an unknown error."
},
"step": {
"user": {
"data": {
"latitude": "Latitude",
"longitude": "Longitude"
},
"description": "Monitor user-based and CDC flu reports.",
"title": "Configure Flu Near You"
}
},
"title": "Flu Near You"
}
}
215 changes: 215 additions & 0 deletions homeassistant/components/flunearyou/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,216 @@
"""The flunearyou component."""
import asyncio
from datetime import timedelta

from pyflunearyou import Client
from pyflunearyou.errors import FluNearYouError
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval

from .const import (
CATEGORY_CDC_REPORT,
CATEGORY_USER_REPORT,
DATA_CLIENT,
DOMAIN,
LOGGER,
SENSORS,
TOPIC_UPDATE,
)

DATA_LISTENER = "listener"

DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)

CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(DOMAIN): vol.Schema(
{
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
}
)
},
extra=vol.ALLOW_EXTRA,
)


@callback
def async_get_api_category(sensor_type):
"""Get the category that a particular sensor type belongs to."""
try:
return next(
(
category
for category, sensors in SENSORS.items()
for sensor in sensors
if sensor[0] == sensor_type
)
)
except StopIteration:
raise ValueError(f"Can't find category sensor type: {sensor_type}")


async def async_setup(hass, config):
"""Set up the Flu Near You component."""
hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}}

if DOMAIN not in config:
return True

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_LATITUDE: config[DOMAIN].get(CONF_LATITUDE, hass.config.latitude),
CONF_LONGITUDE: config[DOMAIN].get(
CONF_LATITUDE, hass.config.longitude
),
},
)
)

return True


async def async_setup_entry(hass, config_entry):
"""Set up Flu Near You as config entry."""
websession = aiohttp_client.async_get_clientsession(hass)

fny = FluNearYouData(
hass,
Client(websession),
config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
)

try:
await fny.async_update()
except FluNearYouError as err:
Comment thread
bachya marked this conversation as resolved.
LOGGER.error("Error while setting up integration: %s", err)
raise ConfigEntryNotReady

hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = fny

hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
)

async def refresh(event_time):
"""Refresh data from Flu Near You."""
await fny.async_update()

hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval(
hass, refresh, DEFAULT_SCAN_INTERVAL
)
Comment on lines +106 to +112
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I had no idea this was a thing? 😆

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things:

  1. FNY requires more than one API call. I could wrap them both in a single callback that gets passed to this, but that doesn't feel great.
    2 The Data Update Coordinator doesn't appear to handle disabling/enabling of entities and the appropriate removal/addition of API calls.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data Update Coordinators will only refresh if they have at least 1 listener. So you can create two coordinators and let entities subscribe.


return True


async def async_unload_entry(hass, config_entry):
"""Unload an Flu Near You config entry."""
hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)

remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id)
remove_listener()

await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")

return True


class FluNearYouData:
"""Define a data object to retrieve info from Flu Near You."""

def __init__(self, hass, client, latitude, longitude):
"""Initialize."""
self._async_cancel_time_interval_listener = None
self._client = client
self._hass = hass
self.data = {}
self.latitude = latitude
self.longitude = longitude

self._api_coros = {
CATEGORY_CDC_REPORT: self._client.cdc_reports.status_by_coordinates(
latitude, longitude
),
CATEGORY_USER_REPORT: self._client.user_reports.status_by_coordinates(
latitude, longitude
),
}

self._api_category_count = {
CATEGORY_CDC_REPORT: 0,
CATEGORY_USER_REPORT: 0,
}

self._api_category_locks = {
CATEGORY_CDC_REPORT: asyncio.Lock(),
CATEGORY_USER_REPORT: asyncio.Lock(),
}

async def _async_get_data_from_api(self, api_category):
"""Update and save data for a particular API category."""
if self._api_category_count[api_category] == 0:
return

try:
self.data[api_category] = await self._api_coros[api_category]
except FluNearYouError as err:
LOGGER.error("Unable to get %s data: %s", api_category, err)
self.data[api_category] = None

async def _async_update_listener_action(self, now):
"""Define an async_track_time_interval action to update data."""
await self.async_update()

@callback
def async_deregister_api_interest(self, sensor_type):
"""Decrement the number of entities with data needs from an API category."""
# If this deregistration should leave us with no registration at all, remove the
# time interval:
if sum(self._api_category_count.values()) == 0:
if self._async_cancel_time_interval_listener:
self._async_cancel_time_interval_listener()
self._async_cancel_time_interval_listener = None
return

api_category = async_get_api_category(sensor_type)
self._api_category_count[api_category] -= 1

async def async_register_api_interest(self, sensor_type):
"""Increment the number of entities with data needs from an API category."""
# If this is the first registration we have, start a time interval:
if not self._async_cancel_time_interval_listener:
self._async_cancel_time_interval_listener = async_track_time_interval(
self._hass, self._async_update_listener_action, DEFAULT_SCAN_INTERVAL,
)

api_category = async_get_api_category(sensor_type)
self._api_category_count[api_category] += 1

# If a sensor registers interest in a particular API call and the data doesn't
# exist for it yet, make the API call and grab the data:
async with self._api_category_locks[api_category]:
if api_category not in self.data:
await self._async_get_data_from_api(api_category)

async def async_update(self):
"""Update Flu Near You data."""
tasks = [
self._async_get_data_from_api(api_category)
for api_category in self._api_coros
]

await asyncio.gather(*tasks)

LOGGER.debug("Received new data")
async_dispatcher_send(self._hass, TOPIC_UPDATE)
60 changes: 60 additions & 0 deletions homeassistant/components/flunearyou/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Define a config flow manager for flunearyou."""
from pyflunearyou import Client
from pyflunearyou.errors import FluNearYouError
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers import aiohttp_client, config_validation as cv

from .const import DOMAIN, LOGGER # pylint: disable=unused-import


class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an FluNearYou config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

@property
def data_schema(self):
"""Return the data schema for integration."""
return vol.Schema(
{
vol.Required(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
vol.Required(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
}
)

async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)

async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input:
return self.async_show_form(step_id="user", data_schema=self.data_schema)

unique_id = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}"

await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()

websession = aiohttp_client.async_get_clientsession(self.hass)
client = Client(websession)

try:
await client.cdc_reports.status_by_coordinates(
user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE]
)
except FluNearYouError as err:
LOGGER.error("Error while setting up integration: %s", err)
Comment thread
bachya marked this conversation as resolved.
return self.async_show_form(
step_id="user", errors={"base": "general_error"}
)

return self.async_create_entry(title=unique_id, data=user_input)
38 changes: 38 additions & 0 deletions homeassistant/components/flunearyou/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Define flunearyou constants."""
import logging

DOMAIN = "flunearyou"
LOGGER = logging.getLogger("homeassistant.components.flunearyou")
Comment thread
bachya marked this conversation as resolved.

DATA_CLIENT = "client"

CATEGORY_CDC_REPORT = "cdc_report"
CATEGORY_USER_REPORT = "user_report"

TOPIC_UPDATE = "flunearyou_update"

TYPE_CDC_LEVEL = "level"
TYPE_CDC_LEVEL2 = "level2"
TYPE_USER_CHICK = "chick"
TYPE_USER_DENGUE = "dengue"
TYPE_USER_FLU = "flu"
TYPE_USER_LEPTO = "lepto"
TYPE_USER_NO_SYMPTOMS = "none"
TYPE_USER_SYMPTOMS = "symptoms"
TYPE_USER_TOTAL = "total"

SENSORS = {
CATEGORY_CDC_REPORT: [
(TYPE_CDC_LEVEL, "CDC Level", "mdi:biohazard", None),
(TYPE_CDC_LEVEL2, "CDC Level 2", "mdi:biohazard", None),
],
CATEGORY_USER_REPORT: [
(TYPE_USER_CHICK, "Avian Flu Symptoms", "mdi:alert", "reports"),
(TYPE_USER_DENGUE, "Dengue Fever Symptoms", "mdi:alert", "reports"),
(TYPE_USER_FLU, "Flu Symptoms", "mdi:alert", "reports"),
(TYPE_USER_LEPTO, "Leptospirosis Symptoms", "mdi:alert", "reports"),
(TYPE_USER_NO_SYMPTOMS, "No Symptoms", "mdi:alert", "reports"),
(TYPE_USER_SYMPTOMS, "Flu-like Symptoms", "mdi:alert", "reports"),
(TYPE_USER_TOTAL, "Total Symptoms", "mdi:alert", "reports"),
],
}
3 changes: 2 additions & 1 deletion homeassistant/components/flunearyou/manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"domain": "flunearyou",
"name": "Flu Near You",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flunearyou",
"requirements": ["pyflunearyou==1.0.3"],
"requirements": ["pyflunearyou==1.0.7"],
"dependencies": [],
"codeowners": ["@bachya"]
}
Loading