Skip to content

Commit

Permalink
Allow none for scan_interval
Browse files Browse the repository at this point in the history
  • Loading branch information
iprak committed Jan 31, 2021
1 parent 2ab397b commit c75e034
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 24 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ trending: up

## Optional Configuration

- Data fetch interval can be adjusted by specifying the `scan_interval` setting whose default value is 6 hours.
- Data fetch interval can be adjusted by specifying the `scan_interval` setting whose default value is 6 hours and the minimum value is 30 seconds.
```yaml
scan_interval:
hours: 4
```
You can disable automatic update by passing `None` for `scan_interval`.

- Trending icons (trending-up, trending-down or trending-neutral) can be displayed instead of currency based icon by specifying `show_trending_icon`.
```yaml
show_trending_icon: true
Expand Down
33 changes: 28 additions & 5 deletions custom_components/yahoofinance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from datetime import timedelta
import logging
from typing import Union

import async_timeout
from homeassistant.const import CONF_SCAN_INTERVAL
Expand Down Expand Up @@ -33,6 +34,7 @@

_LOGGER = logging.getLogger(__name__)
DEFAULT_SCAN_INTERVAL = timedelta(hours=6)
MINIMUM_SCAN_INTERVAL = timedelta(seconds=30)
WEBSESSION_TIMEOUT = 15

CONFIG_SCHEMA = vol.Schema(
Expand All @@ -42,7 +44,7 @@
vol.Required(CONF_SYMBOLS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.time_period,
): vol.Any("none", "None", cv.positive_time_period),
vol.Optional(
CONF_SHOW_TRENDING_ICON, default=DEFAULT_CONF_SHOW_TRENDING_ICON
): cv.boolean,
Expand All @@ -57,6 +59,22 @@
)


def parse_scan_interval(scan_interval: Union[timedelta, str]) -> timedelta:
"""Parse and validate scan_interval."""
if isinstance(scan_interval, str):
if isinstance(scan_interval, str):
if scan_interval.lower() == "none":
scan_interval = None
else:
raise vol.Invalid(
f"Invalid {CONF_SCAN_INTERVAL} specified: {scan_interval}"
)
elif scan_interval < MINIMUM_SCAN_INTERVAL:
raise vol.Invalid("Scan interval should be at least 30 seconds.")

return scan_interval


async def async_setup(hass, config) -> bool:
"""Set up the Yahoo Finance sensors."""

Expand All @@ -67,12 +85,17 @@ async def async_setup(hass, config) -> bool:
symbols = [sym.upper() for sym in symbols]
domain_config[CONF_SYMBOLS] = symbols

coordinator = YahooSymbolUpdateCoordinator(
symbols, hass, domain_config.get(CONF_SCAN_INTERVAL)
)
scan_interval = parse_scan_interval(domain_config.get(CONF_SCAN_INTERVAL))

# Populate parsed value into domain_config
domain_config[CONF_SCAN_INTERVAL] = scan_interval

coordinator = YahooSymbolUpdateCoordinator(symbols, hass, scan_interval)

# Refresh coordinator to get initial symbol data
_LOGGER.debug("Requesting initial data from coordinator")
_LOGGER.info(
f"Requesting data from coordinator with update interval of {scan_interval}."
)
await coordinator.async_refresh()

# Pass down the coordinator and config to platforms.
Expand Down
80 changes: 62 additions & 18 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.setup import async_setup_component
import pytest
import voluptuous as vol

from custom_components.yahoofinance import DEFAULT_SCAN_INTERVAL
from custom_components.yahoofinance import (
DEFAULT_SCAN_INTERVAL,
MINIMUM_SCAN_INTERVAL,
parse_scan_interval,
)
from custom_components.yahoofinance.const import (
CONF_DECIMAL_PLACES,
CONF_SHOW_TRENDING_ICON,
Expand All @@ -21,39 +26,56 @@

SAMPLE_VALID_CONFIG = {DOMAIN: {CONF_SYMBOLS: ["BABA"]}}
YSUC = "custom_components.yahoofinance.YahooSymbolUpdateCoordinator"
DEFAULT_PARTIAL_CONFIG = {
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
CONF_DECIMAL_PLACES: DEFAULT_DECIMAL_PLACES,
CONF_SHOW_TRENDING_ICON: DEFAULT_CONF_SHOW_TRENDING_ICON,
}


@pytest.mark.parametrize(
"config, expected_config",
"domain_config, expected_partial_config",
[
(
{DOMAIN: {CONF_SYMBOLS: ["xyz"]}},
{CONF_SYMBOLS: ["xyz"]},
{CONF_SYMBOLS: ["XYZ"]},
),
(
{
CONF_SYMBOLS: ["xyz"],
CONF_SCAN_INTERVAL: 3600,
CONF_DECIMAL_PLACES: 3,
},
{
CONF_SYMBOLS: ["XYZ"],
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
CONF_DECIMAL_PLACES: DEFAULT_DECIMAL_PLACES,
CONF_SHOW_TRENDING_ICON: DEFAULT_CONF_SHOW_TRENDING_ICON,
CONF_SCAN_INTERVAL: timedelta(hours=1),
CONF_DECIMAL_PLACES: 3,
},
),
(
{
DOMAIN: {
CONF_SYMBOLS: ["xyz"],
CONF_SCAN_INTERVAL: 3600,
CONF_DECIMAL_PLACES: 3,
}
CONF_SYMBOLS: ["xyz"],
CONF_SCAN_INTERVAL: "None",
},
{
CONF_SYMBOLS: ["XYZ"],
CONF_SCAN_INTERVAL: timedelta(hours=1),
CONF_DECIMAL_PLACES: 3,
CONF_SHOW_TRENDING_ICON: DEFAULT_CONF_SHOW_TRENDING_ICON,
CONF_SCAN_INTERVAL: None,
},
),
(
{
CONF_SYMBOLS: ["xyz"],
CONF_SCAN_INTERVAL: "none",
},
{
CONF_SYMBOLS: ["XYZ"],
CONF_SCAN_INTERVAL: None,
},
),
],
)
async def test_setup_refreshes_data_coordinator_and_loads_platform(
hass, config, expected_config
hass, domain_config, expected_partial_config
):
"""Component setup refreshed data coordinator and loads the platform."""

Expand All @@ -63,19 +85,36 @@ async def test_setup_refreshes_data_coordinator_and_loads_platform(
f"{YSUC}.async_refresh", AsyncMock(return_value=None)
) as mock_coordinator_async_refresh:

config = {DOMAIN: domain_config}

assert await async_setup_component(hass, DOMAIN, config) is True
await hass.async_block_till_done()

assert mock_coordinator_async_refresh.call_count == 1
assert mock_async_load_platform.call_count == 1

expected_config = DEFAULT_PARTIAL_CONFIG.copy()
expected_config.update(expected_partial_config)
assert expected_config == hass.data[DOMAIN][HASS_DATA_CONFIG]


async def test_setup_optionally_requests_coordinator_refresh(hass):
"""Component setup requets data coordinator refresh if it failed to load data."""
@pytest.mark.parametrize(
"scan_interval",
[
(timedelta(-1)),
(MINIMUM_SCAN_INTERVAL - timedelta(seconds=1)),
("None2"),
],
)
def test_invalid_scan_interval(hass, scan_interval):
"""Test invalid scan interval."""

# Mock `last_update_success` to be False which results in a call to `async_request_refresh`
with pytest.raises(vol.Invalid):
parse_scan_interval(scan_interval)


async def test_setup_optionally_requests_coordinator_refresh(hass):
"""Component setup requests data coordinator refresh if it failed to load data."""

with patch(
"homeassistant.helpers.discovery.async_load_platform"
Expand All @@ -84,13 +123,18 @@ async def test_setup_optionally_requests_coordinator_refresh(hass):
mock_instance = Mock()
mock_instance.async_refresh = AsyncMock(return_value=None)
mock_instance.async_request_refresh = AsyncMock(return_value=None)

# Mock `last_update_success` to be False which results in a call to `async_request_refresh`
mock_instance.last_update_success = False

mock_coordinator.return_value = mock_instance

assert await async_setup_component(hass, DOMAIN, SAMPLE_VALID_CONFIG) is True
await hass.async_block_till_done()

assert mock_coordinator.called_with(
SAMPLE_VALID_CONFIG[DOMAIN][CONF_SYMBOLS], hass, DEFAULT_SCAN_INTERVAL
)
assert mock_instance.async_refresh.call_count == 1
assert mock_instance.async_request_refresh.call_count == 1
assert mock_async_load_platform.call_count == 1
Expand Down

0 comments on commit c75e034

Please sign in to comment.