Add config entry for Flu Near You (#32858)
* Add config flow for Flu Near You * Cleanup * Cleanup * Add tests * Add test requirements * Code review * Reduce unnecessary async-ness * Handle API registration * Cleanup * Update homeassistant/components/flunearyou/.translations/en.json Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * Code review * Ensure config schema allows additional keys Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>pull/33557/head
parent
55870aec31
commit
cb058ff6c0
|
@ -219,6 +219,7 @@ omit =
|
||||||
homeassistant/components/flic/binary_sensor.py
|
homeassistant/components/flic/binary_sensor.py
|
||||||
homeassistant/components/flock/notify.py
|
homeassistant/components/flock/notify.py
|
||||||
homeassistant/components/flume/*
|
homeassistant/components/flume/*
|
||||||
|
homeassistant/components/flunearyou/__init__.py
|
||||||
homeassistant/components/flunearyou/sensor.py
|
homeassistant/components/flunearyou/sensor.py
|
||||||
homeassistant/components/flux_led/light.py
|
homeassistant/components/flux_led/light.py
|
||||||
homeassistant/components/folder/sensor.py
|
homeassistant/components/folder/sensor.py
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,216 @@
|
||||||
"""The flunearyou component."""
|
"""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:
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -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)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", errors={"base": "general_error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_create_entry(title=unique_id, data=user_input)
|
|
@ -0,0 +1,38 @@
|
||||||
|
"""Define flunearyou constants."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
DOMAIN = "flunearyou"
|
||||||
|
LOGGER = logging.getLogger("homeassistant.components.flunearyou")
|
||||||
|
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"domain": "flunearyou",
|
"domain": "flunearyou",
|
||||||
"name": "Flu Near You",
|
"name": "Flu Near You",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/flunearyou",
|
"documentation": "https://www.home-assistant.io/integrations/flunearyou",
|
||||||
"requirements": ["pyflunearyou==1.0.3"],
|
"requirements": ["pyflunearyou==1.0.7"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@bachya"]
|
"codeowners": ["@bachya"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
"""Support for user- and CDC-based flu info sensors from Flu Near You."""
|
"""Support for user- and CDC-based flu info sensors from Flu Near You."""
|
||||||
from datetime import timedelta
|
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_STATE
|
||||||
import logging
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from pyflunearyou import Client
|
|
||||||
from pyflunearyou.errors import FluNearYouError
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
|
||||||
from homeassistant.const import (
|
|
||||||
ATTR_ATTRIBUTION,
|
|
||||||
ATTR_STATE,
|
|
||||||
CONF_LATITUDE,
|
|
||||||
CONF_LONGITUDE,
|
|
||||||
CONF_MONITORED_CONDITIONS,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers import aiohttp_client
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
from .const import (
|
||||||
|
CATEGORY_CDC_REPORT,
|
||||||
|
CATEGORY_USER_REPORT,
|
||||||
|
DATA_CLIENT,
|
||||||
|
DOMAIN,
|
||||||
|
SENSORS,
|
||||||
|
TOPIC_UPDATE,
|
||||||
|
TYPE_USER_CHICK,
|
||||||
|
TYPE_USER_DENGUE,
|
||||||
|
TYPE_USER_FLU,
|
||||||
|
TYPE_USER_LEPTO,
|
||||||
|
TYPE_USER_NO_SYMPTOMS,
|
||||||
|
TYPE_USER_SYMPTOMS,
|
||||||
|
TYPE_USER_TOTAL,
|
||||||
|
)
|
||||||
|
|
||||||
ATTR_CITY = "city"
|
ATTR_CITY = "city"
|
||||||
ATTR_REPORTED_DATE = "reported_date"
|
ATTR_REPORTED_DATE = "reported_date"
|
||||||
|
@ -31,94 +30,46 @@ ATTR_ZIP_CODE = "zip_code"
|
||||||
|
|
||||||
DEFAULT_ATTRIBUTION = "Data provided by Flu Near You"
|
DEFAULT_ATTRIBUTION = "Data provided by Flu Near You"
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=30)
|
|
||||||
|
|
||||||
CATEGORY_CDC_REPORT = "cdc_report"
|
|
||||||
CATEGORY_USER_REPORT = "user_report"
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
EXTENDED_TYPE_MAPPING = {
|
EXTENDED_TYPE_MAPPING = {
|
||||||
TYPE_USER_FLU: "ili",
|
TYPE_USER_FLU: "ili",
|
||||||
TYPE_USER_NO_SYMPTOMS: "no_symptoms",
|
TYPE_USER_NO_SYMPTOMS: "no_symptoms",
|
||||||
TYPE_USER_TOTAL: "total_surveys",
|
TYPE_USER_TOTAL: "total_surveys",
|
||||||
}
|
}
|
||||||
|
|
||||||
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"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
{
|
"""Set up Flu Near You sensors based on a config entry."""
|
||||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
fny = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
|
||||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
|
||||||
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All(
|
|
||||||
cv.ensure_list, [vol.In(SENSORS)]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
[
|
||||||
"""Configure the platform and add the sensors."""
|
FluNearYouSensor(fny, sensor_type, name, category, icon, unit)
|
||||||
websession = aiohttp_client.async_get_clientsession(hass)
|
for category, sensors in SENSORS.items()
|
||||||
|
for sensor_type, name, icon, unit in sensors
|
||||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
],
|
||||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
True,
|
||||||
|
|
||||||
fny = FluNearYouData(
|
|
||||||
Client(websession), latitude, longitude, config[CONF_MONITORED_CONDITIONS]
|
|
||||||
)
|
)
|
||||||
await fny.async_update()
|
|
||||||
|
|
||||||
sensors = [
|
|
||||||
FluNearYouSensor(fny, kind, name, category, icon, unit)
|
|
||||||
for category in config[CONF_MONITORED_CONDITIONS]
|
|
||||||
for kind, name, icon, unit in SENSORS[category]
|
|
||||||
]
|
|
||||||
|
|
||||||
async_add_entities(sensors, True)
|
|
||||||
|
|
||||||
|
|
||||||
class FluNearYouSensor(Entity):
|
class FluNearYouSensor(Entity):
|
||||||
"""Define a base Flu Near You sensor."""
|
"""Define a base Flu Near You sensor."""
|
||||||
|
|
||||||
def __init__(self, fny, kind, name, category, icon, unit):
|
def __init__(self, fny, sensor_type, name, category, icon, unit):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
self._async_unsub_dispatcher_connect = None
|
||||||
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||||
self._category = category
|
self._category = category
|
||||||
|
self._fny = fny
|
||||||
self._icon = icon
|
self._icon = icon
|
||||||
self._kind = kind
|
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self._sensor_type = sensor_type
|
||||||
self._state = None
|
self._state = None
|
||||||
self._unit = unit
|
self._unit = unit
|
||||||
self.fny = fny
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return bool(self.fny.data[self._category])
|
return bool(self._fny.data[self._category])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
|
@ -143,19 +94,43 @@ class FluNearYouSensor(Entity):
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||||
return f"{self.fny.latitude},{self.fny.longitude}_{self._kind}"
|
return f"{self._fny.latitude},{self._fny.longitude}_{self._sensor_type}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit the value is expressed in."""
|
"""Return the unit the value is expressed in."""
|
||||||
return self._unit
|
return self._unit
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_added_to_hass(self):
|
||||||
"""Update the sensor."""
|
"""Register callbacks."""
|
||||||
await self.fny.async_update()
|
|
||||||
|
|
||||||
cdc_data = self.fny.data.get(CATEGORY_CDC_REPORT)
|
@callback
|
||||||
user_data = self.fny.data.get(CATEGORY_USER_REPORT)
|
def update():
|
||||||
|
"""Update the state."""
|
||||||
|
self.update_from_latest_data()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||||
|
self.hass, TOPIC_UPDATE, update
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._fny.async_register_api_interest(self._sensor_type)
|
||||||
|
|
||||||
|
self.update_from_latest_data()
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Disconnect dispatcher listener when removed."""
|
||||||
|
if self._async_unsub_dispatcher_connect:
|
||||||
|
self._async_unsub_dispatcher_connect()
|
||||||
|
self._async_unsub_dispatcher_connect = None
|
||||||
|
|
||||||
|
self._fny.async_deregister_api_interest(self._sensor_type)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def update_from_latest_data(self):
|
||||||
|
"""Update the sensor."""
|
||||||
|
cdc_data = self._fny.data.get(CATEGORY_CDC_REPORT)
|
||||||
|
user_data = self._fny.data.get(CATEGORY_USER_REPORT)
|
||||||
|
|
||||||
if self._category == CATEGORY_CDC_REPORT and cdc_data:
|
if self._category == CATEGORY_CDC_REPORT and cdc_data:
|
||||||
self._attrs.update(
|
self._attrs.update(
|
||||||
|
@ -164,7 +139,7 @@ class FluNearYouSensor(Entity):
|
||||||
ATTR_STATE: cdc_data["name"],
|
ATTR_STATE: cdc_data["name"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self._state = cdc_data[self._kind]
|
self._state = cdc_data[self._sensor_type]
|
||||||
elif self._category == CATEGORY_USER_REPORT and user_data:
|
elif self._category == CATEGORY_USER_REPORT and user_data:
|
||||||
self._attrs.update(
|
self._attrs.update(
|
||||||
{
|
{
|
||||||
|
@ -176,10 +151,10 @@ class FluNearYouSensor(Entity):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._kind in user_data["state"]["data"]:
|
if self._sensor_type in user_data["state"]["data"]:
|
||||||
states_key = self._kind
|
states_key = self._sensor_type
|
||||||
elif self._kind in EXTENDED_TYPE_MAPPING:
|
elif self._sensor_type in EXTENDED_TYPE_MAPPING:
|
||||||
states_key = EXTENDED_TYPE_MAPPING[self._kind]
|
states_key = EXTENDED_TYPE_MAPPING[self._sensor_type]
|
||||||
|
|
||||||
self._attrs[ATTR_STATE_REPORTS_THIS_WEEK] = user_data["state"]["data"][
|
self._attrs[ATTR_STATE_REPORTS_THIS_WEEK] = user_data["state"]["data"][
|
||||||
states_key
|
states_key
|
||||||
|
@ -188,7 +163,7 @@ class FluNearYouSensor(Entity):
|
||||||
"last_week_data"
|
"last_week_data"
|
||||||
][states_key]
|
][states_key]
|
||||||
|
|
||||||
if self._kind == TYPE_USER_TOTAL:
|
if self._sensor_type == TYPE_USER_TOTAL:
|
||||||
self._state = sum(
|
self._state = sum(
|
||||||
v
|
v
|
||||||
for k, v in user_data["local"].items()
|
for k, v in user_data["local"].items()
|
||||||
|
@ -202,32 +177,4 @@ class FluNearYouSensor(Entity):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._state = user_data["local"][self._kind]
|
self._state = user_data["local"][self._sensor_type]
|
||||||
|
|
||||||
|
|
||||||
class FluNearYouData:
|
|
||||||
"""Define a data object to retrieve info from Flu Near You."""
|
|
||||||
|
|
||||||
def __init__(self, client, latitude, longitude, sensor_types):
|
|
||||||
"""Initialize."""
|
|
||||||
self._client = client
|
|
||||||
self._sensor_types = sensor_types
|
|
||||||
self.data = {}
|
|
||||||
self.latitude = latitude
|
|
||||||
self.longitude = longitude
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
||||||
async def async_update(self):
|
|
||||||
"""Update Flu Near You data."""
|
|
||||||
for key, method in [
|
|
||||||
(CATEGORY_CDC_REPORT, self._client.cdc_reports.status_by_coordinates),
|
|
||||||
(CATEGORY_USER_REPORT, self._client.user_reports.status_by_coordinates),
|
|
||||||
]:
|
|
||||||
if key in self._sensor_types:
|
|
||||||
try:
|
|
||||||
self.data[key] = await method(self.latitude, self.longitude)
|
|
||||||
except FluNearYouError as err:
|
|
||||||
_LOGGER.error('There was an error with "%s" data: %s', key, err)
|
|
||||||
self.data[key] = {}
|
|
||||||
|
|
||||||
_LOGGER.debug("New data stored: %s", self.data)
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Flu Near You",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Configure Flu Near You",
|
||||||
|
"description": "Monitor user-based and CDC repots for a pair of coordinates.",
|
||||||
|
"data": {
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"general_error": "There was an unknown error."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "These coordinates are already registered."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ FLOWS = [
|
||||||
"elkm1",
|
"elkm1",
|
||||||
"emulated_roku",
|
"emulated_roku",
|
||||||
"esphome",
|
"esphome",
|
||||||
|
"flunearyou",
|
||||||
"freebox",
|
"freebox",
|
||||||
"garmin_connect",
|
"garmin_connect",
|
||||||
"gdacs",
|
"gdacs",
|
||||||
|
|
|
@ -1281,7 +1281,7 @@ pyflic-homeassistant==0.4.dev0
|
||||||
pyflume==0.3.0
|
pyflume==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.flunearyou
|
# homeassistant.components.flunearyou
|
||||||
pyflunearyou==1.0.3
|
pyflunearyou==1.0.7
|
||||||
|
|
||||||
# homeassistant.components.futurenow
|
# homeassistant.components.futurenow
|
||||||
pyfnip==0.2
|
pyfnip==0.2
|
||||||
|
|
|
@ -493,6 +493,9 @@ pyeverlights==0.1.0
|
||||||
# homeassistant.components.fido
|
# homeassistant.components.fido
|
||||||
pyfido==2.1.1
|
pyfido==2.1.1
|
||||||
|
|
||||||
|
# homeassistant.components.flunearyou
|
||||||
|
pyflunearyou==1.0.7
|
||||||
|
|
||||||
# homeassistant.components.fritzbox
|
# homeassistant.components.fritzbox
|
||||||
pyfritzhome==0.4.0
|
pyfritzhome==0.4.0
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""Define tests for the flunearyou component."""
|
|
@ -0,0 +1,87 @@
|
||||||
|
"""Define tests for the flunearyou config flow."""
|
||||||
|
from asynctest import patch
|
||||||
|
from pyflunearyou.errors import FluNearYouError
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.flunearyou import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_duplicate_error(hass):
|
||||||
|
"""Test that an error is shown when duplicates are added."""
|
||||||
|
conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"}
|
||||||
|
|
||||||
|
MockConfigEntry(
|
||||||
|
domain=DOMAIN, unique_id="51.528308, -0.3817765", data=conf
|
||||||
|
).add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}, data=conf
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_general_error(hass):
|
||||||
|
"""Test that an error is shown on a library error."""
|
||||||
|
conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"pyflunearyou.cdc.CdcReport.status_by_coordinates", side_effect=FluNearYouError,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}, data=conf
|
||||||
|
)
|
||||||
|
assert result["errors"] == {"base": "general_error"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_form(hass):
|
||||||
|
"""Test that the form is served with no input."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_import(hass):
|
||||||
|
"""Test that the import step works."""
|
||||||
|
conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.flunearyou.async_setup_entry", return_value=True
|
||||||
|
), patch("pyflunearyou.cdc.CdcReport.status_by_coordinates"):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "51.528308, -0.3817765"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_LATITUDE: "51.528308",
|
||||||
|
CONF_LONGITUDE: "-0.3817765",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_user(hass):
|
||||||
|
"""Test that the user step works."""
|
||||||
|
conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.flunearyou.async_setup_entry", return_value=True
|
||||||
|
), patch("pyflunearyou.cdc.CdcReport.status_by_coordinates"):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}, data=conf
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "51.528308, -0.3817765"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_LATITUDE: "51.528308",
|
||||||
|
CONF_LONGITUDE: "-0.3817765",
|
||||||
|
}
|
Loading…
Reference in New Issue