Fix late review comments for Scrape (#81259)

* Review comments from #74325

* Remove moved test

* Fix init

* Handle no data

* Remove print

* Fix tests

* PlatformNotReady if no data

* Recover failed platform setup

* Fix broken test

* patch context

* reset test init

* Move to platform

* asyncio gather

* Remove duplicate code
This commit is contained in:
G Johansson 2022-11-02 17:52:36 +01:00 committed by GitHub
parent 442c5ccc06
commit 93d74cafdc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 113 additions and 73 deletions

View file

@ -1,8 +1,10 @@
"""The scrape component.""" """The scrape component."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Coroutine
from datetime import timedelta from datetime import timedelta
import logging from typing import Any
import voluptuous as vol import voluptuous as vol
@ -15,7 +17,6 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA
@ -24,9 +25,6 @@ from homeassistant.helpers.typing import ConfigType
from .const import CONF_INDEX, CONF_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN from .const import CONF_INDEX, CONF_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN
from .coordinator import ScrapeCoordinator from .coordinator import ScrapeCoordinator
_LOGGER = logging.getLogger(__name__)
SENSOR_SCHEMA = vol.Schema( SENSOR_SCHEMA = vol.Schema(
{ {
**TEMPLATE_SENSOR_BASE_SCHEMA.schema, **TEMPLATE_SENSOR_BASE_SCHEMA.schema,
@ -55,13 +53,12 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Scrape from yaml config.""" """Set up Scrape from yaml config."""
scrape_config: list[ConfigType] | None
if not (scrape_config := config.get(DOMAIN)): if not (scrape_config := config.get(DOMAIN)):
return True return True
load_coroutines: list[Coroutine[Any, Any, None]] = []
for resource_config in scrape_config: for resource_config in scrape_config:
if not (sensors := resource_config.get(SENSOR_DOMAIN)):
raise PlatformNotReady("No sensors configured")
rest = create_rest_data_from_config(hass, resource_config) rest = create_rest_data_from_config(hass, resource_config)
coordinator = ScrapeCoordinator( coordinator = ScrapeCoordinator(
hass, hass,
@ -70,17 +67,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
seconds=resource_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) seconds=resource_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
), ),
) )
await coordinator.async_refresh()
if coordinator.data is None:
raise PlatformNotReady
for sensor_config in sensors: sensors: list[ConfigType] = resource_config.get(SENSOR_DOMAIN, [])
discovery.load_platform( if sensors:
hass, load_coroutines.append(
Platform.SENSOR, discovery.async_load_platform(
DOMAIN, hass,
{"coordinator": coordinator, "config": sensor_config}, Platform.SENSOR,
config, DOMAIN,
{"coordinator": coordinator, "configs": sensors},
config,
)
) )
if load_coroutines:
await asyncio.gather(*load_coroutines)
return True return True

View file

@ -83,6 +83,7 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Web scrape sensor.""" """Set up the Web scrape sensor."""
entities: list[ScrapeSensor] = []
if discovery_info is None: if discovery_info is None:
async_create_issue( async_create_issue(
hass, hass,
@ -97,45 +98,47 @@ async def async_setup_platform(
rest = create_rest_data_from_config(hass, resource_config) rest = create_rest_data_from_config(hass, resource_config)
coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL) coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL)
await coordinator.async_refresh()
if coordinator.data is None:
raise PlatformNotReady
sensor_config = config sensors_config: list[tuple[ConfigType, ConfigType]] = [
template_config = vol.Schema( (
TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA config,
)(sensor_config) vol.Schema(TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA)(
config
),
)
]
else: else:
coordinator = discovery_info["coordinator"] coordinator = discovery_info["coordinator"]
sensor_config = discovery_info["config"] sensors_config = [
template_config = sensor_config (sensor_config, sensor_config)
for sensor_config in discovery_info["configs"]
]
name: str = template_config[CONF_NAME] await coordinator.async_refresh()
unique_id: str | None = template_config.get(CONF_UNIQUE_ID) if coordinator.data is None:
select: str | None = sensor_config.get(CONF_SELECT) raise PlatformNotReady
attr: str | None = sensor_config.get(CONF_ATTRIBUTE)
index: int = sensor_config[CONF_INDEX]
value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: for sensor_config, template_config in sensors_config:
value_template.hass = hass value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = hass
async_add_entities( entities.append(
[
ScrapeSensor( ScrapeSensor(
hass, hass,
coordinator, coordinator,
template_config, template_config,
name, template_config[CONF_NAME],
unique_id, template_config.get(CONF_UNIQUE_ID),
select, sensor_config.get(CONF_SELECT),
attr, sensor_config.get(CONF_ATTRIBUTE),
index, sensor_config[CONF_INDEX],
value_template, value_template,
) )
], )
)
async_add_entities(entities)
class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor): class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor):

View file

@ -1,15 +1,21 @@
"""Test Scrape component setup process.""" """Test Scrape component setup process."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from unittest.mock import patch from unittest.mock import patch
import pytest
from homeassistant.components.scrape.const import DOMAIN from homeassistant.components.scrape.const import DOMAIN
from homeassistant.components.scrape.sensor import SCAN_INTERVAL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import MockRestData, return_integration_config from . import MockRestData, return_integration_config
from tests.common import async_fire_time_changed
async def test_setup_config(hass: HomeAssistant) -> None: async def test_setup_config(hass: HomeAssistant) -> None:
"""Test setup from yaml.""" """Test setup from yaml."""
@ -35,8 +41,10 @@ async def test_setup_config(hass: HomeAssistant) -> None:
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
async def test_setup_no_data_fails(hass: HomeAssistant) -> None: async def test_setup_no_data_fails_with_recovery(
"""Test setup entry no data fails.""" hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test setup entry no data fails and recovers."""
config = { config = {
DOMAIN: [ DOMAIN: [
return_integration_config( return_integration_config(
@ -45,15 +53,25 @@ async def test_setup_no_data_fails(hass: HomeAssistant) -> None:
] ]
} }
mocker = MockRestData("test_scrape_sensor_no_data")
with patch( with patch(
"homeassistant.components.scrape.coordinator.RestData", "homeassistant.components.rest.RestData",
return_value=MockRestData("test_scrape_sensor_no_data"), return_value=mocker,
): ):
assert not await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version")
assert state is None
assert "Platform scrape not ready yet" in caplog.text
mocker.payload = "test_scrape_sensor"
async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version") state = hass.states.get("sensor.ha_version")
assert state is None assert state.state == "Current Version: 2021.12.10"
async def test_setup_config_no_configuration(hass: HomeAssistant) -> None: async def test_setup_config_no_configuration(hass: HomeAssistant) -> None:
@ -65,3 +83,30 @@ async def test_setup_config_no_configuration(hass: HomeAssistant) -> None:
entities = er.async_get(hass) entities = er.async_get(hass)
assert entities.entities == {} assert entities.entities == {}
async def test_setup_config_no_sensors(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test setup from yaml with no configured sensors finalize properly."""
config = {
DOMAIN: [
{
"resource": "https://www.address.com",
"verify_ssl": True,
},
{
"resource": "https://www.address2.com",
"verify_ssl": True,
"sensor": None,
},
]
}
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.rest.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()

View file

@ -4,6 +4,8 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from unittest.mock import patch from unittest.mock import patch
import pytest
from homeassistant.components.scrape.sensor import SCAN_INTERVAL from homeassistant.components.scrape.sensor import SCAN_INTERVAL
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
CONF_STATE_CLASS, CONF_STATE_CLASS,
@ -67,6 +69,7 @@ async def test_scrape_sensor_platform_yaml(hass: HomeAssistant) -> None:
name="Auth page2", name="Auth page2",
username="user@secret.com", username="user@secret.com",
password="12345678", password="12345678",
template="{{value}}",
), ),
] ]
} }
@ -248,7 +251,9 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None:
assert state2.state == "secret text" assert state2.state == "secret text"
async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test Scrape sensor fails on no data.""" """Test Scrape sensor fails on no data."""
config = { config = {
DOMAIN: return_integration_config( DOMAIN: return_integration_config(
@ -261,12 +266,14 @@ async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None:
"homeassistant.components.rest.RestData", "homeassistant.components.rest.RestData",
return_value=mocker, return_value=mocker,
): ):
assert not await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version") state = hass.states.get("sensor.ha_version")
assert state is None assert state is None
assert "Platform scrape not ready yet" in caplog.text
async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
"""Test Scrape sensor no data on refresh.""" """Test Scrape sensor no data on refresh."""
@ -286,13 +293,13 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
assert await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version") state = hass.states.get("sensor.ha_version")
assert state assert state
assert state.state == "Current Version: 2021.12.10" assert state.state == "Current Version: 2021.12.10"
mocker.payload = "test_scrape_sensor_no_data" mocker.payload = "test_scrape_sensor_no_data"
async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL) async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version") state = hass.states.get("sensor.ha_version")
assert state is not None assert state is not None
@ -398,18 +405,3 @@ async def test_scrape_sensor_unique_id(hass: HomeAssistant) -> None:
entity = entity_reg.async_get("sensor.ha_version") entity = entity_reg.async_get("sensor.ha_version")
assert entity.unique_id == "ha_version_unique_id" assert entity.unique_id == "ha_version_unique_id"
async def test_scrape_sensor_not_configured_sensor(hass: HomeAssistant, caplog) -> None:
"""Test Scrape sensor with missing configured sensors."""
config = {DOMAIN: [return_integration_config(sensors=None)]}
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.rest.RestData",
return_value=mocker,
):
assert not await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
assert "No sensors configured" in caplog.text