mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 05:06:13 +01:00
GIOS quality scale fixes to platinum (#162510)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,15 +8,14 @@ from aiohttp.client_exceptions import ClientConnectorError
|
||||
from gios import Gios
|
||||
from gios.exceptions import GiosError
|
||||
|
||||
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_STATION_ID, DOMAIN
|
||||
from .coordinator import GiosConfigEntry, GiosData, GiosDataUpdateCoordinator
|
||||
from .coordinator import GiosConfigEntry, GiosDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,19 +55,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: GiosConfigEntry) -> bool
|
||||
coordinator = GiosDataUpdateCoordinator(hass, entry, gios)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = GiosData(coordinator)
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Remove air_quality entities from registry if they exist
|
||||
ent_reg = er.async_get(hass)
|
||||
unique_id = str(coordinator.gios.station_id)
|
||||
if entity_id := ent_reg.async_get_entity_id(
|
||||
AIR_QUALITY_PLATFORM, DOMAIN, unique_id
|
||||
):
|
||||
_LOGGER.debug("Removing deprecated air_quality entity %s", entity_id)
|
||||
ent_reg.async_remove(entity_id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -38,14 +38,18 @@ class GiosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
station_id = user_input[CONF_STATION_ID]
|
||||
|
||||
try:
|
||||
await self.async_set_unique_id(station_id, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
await self.async_set_unique_id(station_id, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(API_TIMEOUT):
|
||||
gios = await Gios.create(websession, int(station_id))
|
||||
await gios.async_update()
|
||||
|
||||
except ApiError, ClientConnectorError, TimeoutError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidSensorsDataError:
|
||||
errors[CONF_STATION_ID] = "invalid_sensors_data"
|
||||
else:
|
||||
# GIOS treats station ID as int
|
||||
user_input[CONF_STATION_ID] = int(station_id)
|
||||
|
||||
@@ -60,10 +64,6 @@ class GiosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# raising errors.
|
||||
data={**user_input, CONF_NAME: gios.station_name},
|
||||
)
|
||||
except ApiError, ClientConnectorError, TimeoutError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidSensorsDataError:
|
||||
errors[CONF_STATION_ID] = "invalid_sensors_data"
|
||||
|
||||
try:
|
||||
gios = await Gios.create(websession)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -22,14 +21,7 @@ from .const import API_TIMEOUT, DOMAIN, MANUFACTURER, SCAN_INTERVAL, URL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type GiosConfigEntry = ConfigEntry[GiosData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GiosData:
|
||||
"""Data for GIOS integration."""
|
||||
|
||||
coordinator: GiosDataUpdateCoordinator
|
||||
type GiosConfigEntry = ConfigEntry[GiosDataUpdateCoordinator]
|
||||
|
||||
|
||||
class GiosDataUpdateCoordinator(DataUpdateCoordinator[GiosSensors]):
|
||||
|
||||
@@ -14,7 +14,7 @@ async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: GiosConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
return {
|
||||
"config_entry": config_entry.as_dict(),
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["dacite", "gios"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["gios==7.0.0"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
rules:
|
||||
# Other comments:
|
||||
# - we could consider removing the air quality entity removal
|
||||
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
@@ -9,14 +6,8 @@ rules:
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage:
|
||||
status: todo
|
||||
comment:
|
||||
We should have the happy flow as the first test, which can be merged with test_show_form.
|
||||
The config flow tests are missing adding a duplicate entry test.
|
||||
config-flow:
|
||||
status: todo
|
||||
comment: Limit the scope of the try block in the user step
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
@@ -27,9 +18,7 @@ rules:
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data:
|
||||
status: todo
|
||||
comment: No direct need to wrap the coordinator in a dataclass to store in the config entry
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
@@ -50,11 +39,7 @@ rules:
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: This integration does not require authentication.
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment:
|
||||
The `test_async_setup_entry` should test the state of the mock config entry, instead of an entity state
|
||||
The `test_availability` doesn't really do what it says it does, and this is now already tested via the snapshot tests.
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
@@ -78,13 +63,9 @@ rules:
|
||||
status: exempt
|
||||
comment: This integration does not have devices.
|
||||
entity-category: done
|
||||
entity-device-class:
|
||||
status: todo
|
||||
comment: We can use the CO device class for the carbon monoxide sensor
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations:
|
||||
status: todo
|
||||
comment: We can remove the options state_attributes.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
|
||||
@@ -72,9 +72,9 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
|
||||
key=ATTR_CO,
|
||||
value=lambda sensors: sensors.co.value if sensors.co else None,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.CO,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="co",
|
||||
),
|
||||
GiosSensorEntityDescription(
|
||||
key=ATTR_NO,
|
||||
@@ -181,7 +181,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add a GIOS entities from a config_entry."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
coordinator = entry.runtime_data
|
||||
# Due to the change of the attribute name of one sensor, it is necessary to migrate
|
||||
# the unique_id to the new name.
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -31,26 +31,11 @@
|
||||
"sufficient": "Sufficient",
|
||||
"very_bad": "Very bad",
|
||||
"very_good": "Very good"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"bad": "[%key:component::gios::entity::sensor::aqi::state::bad%]",
|
||||
"good": "[%key:component::gios::entity::sensor::aqi::state::good%]",
|
||||
"moderate": "[%key:component::gios::entity::sensor::aqi::state::moderate%]",
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"c6h6": {
|
||||
"name": "Benzene"
|
||||
},
|
||||
"co": {
|
||||
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
|
||||
},
|
||||
"no2_index": {
|
||||
"name": "Nitrogen dioxide index",
|
||||
"state": {
|
||||
@@ -60,18 +45,6 @@
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"bad": "[%key:component::gios::entity::sensor::aqi::state::bad%]",
|
||||
"good": "[%key:component::gios::entity::sensor::aqi::state::good%]",
|
||||
"moderate": "[%key:component::gios::entity::sensor::aqi::state::moderate%]",
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nox": {
|
||||
@@ -86,18 +59,6 @@
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"bad": "[%key:component::gios::entity::sensor::aqi::state::bad%]",
|
||||
"good": "[%key:component::gios::entity::sensor::aqi::state::good%]",
|
||||
"moderate": "[%key:component::gios::entity::sensor::aqi::state::moderate%]",
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pm10_index": {
|
||||
@@ -109,18 +70,6 @@
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"bad": "[%key:component::gios::entity::sensor::aqi::state::bad%]",
|
||||
"good": "[%key:component::gios::entity::sensor::aqi::state::good%]",
|
||||
"moderate": "[%key:component::gios::entity::sensor::aqi::state::moderate%]",
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pm25_index": {
|
||||
@@ -132,18 +81,6 @@
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"bad": "[%key:component::gios::entity::sensor::aqi::state::bad%]",
|
||||
"good": "[%key:component::gios::entity::sensor::aqi::state::good%]",
|
||||
"moderate": "[%key:component::gios::entity::sensor::aqi::state::moderate%]",
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"so2_index": {
|
||||
@@ -155,18 +92,6 @@
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"bad": "[%key:component::gios::entity::sensor::aqi::state::bad%]",
|
||||
"good": "[%key:component::gios::entity::sensor::aqi::state::good%]",
|
||||
"moderate": "[%key:component::gios::entity::sensor::aqi::state::moderate%]",
|
||||
"sufficient": "[%key:component::gios::entity::sensor::aqi::state::sufficient%]",
|
||||
"very_bad": "[%key:component::gios::entity::sensor::aqi::state::very_bad%]",
|
||||
"very_good": "[%key:component::gios::entity::sensor::aqi::state::very_good%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1403,7 +1403,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"geofency",
|
||||
"geonetnz_quakes",
|
||||
"geonetnz_volcano",
|
||||
"gios",
|
||||
"github",
|
||||
"gitlab_ci",
|
||||
"gitter",
|
||||
|
||||
@@ -153,14 +153,14 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_device_class': <SensorDeviceClass.CO: 'carbon_monoxide'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Carbon monoxide',
|
||||
'platform': 'gios',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'co',
|
||||
'translation_key': None,
|
||||
'unique_id': '123-co',
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
})
|
||||
@@ -169,6 +169,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by GIOŚ',
|
||||
'device_class': 'carbon_monoxide',
|
||||
'friendly_name': 'Home Carbon monoxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
|
||||
@@ -11,6 +11,8 @@ from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONFIG = {
|
||||
CONF_STATION_ID: "123",
|
||||
}
|
||||
@@ -18,8 +20,8 @@ CONFIG = {
|
||||
pytestmark = pytest.mark.usefixtures("mock_gios")
|
||||
|
||||
|
||||
async def test_show_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the form is served with no input."""
|
||||
async def test_happy_flow(hass: HomeAssistant) -> None:
|
||||
"""Test that the user step works."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
@@ -28,6 +30,19 @@ async def test_show_form(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "user"
|
||||
assert len(result["data_schema"].schema[CONF_STATION_ID].config["options"]) == 2
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Home"
|
||||
assert result["data"] == {
|
||||
CONF_STATION_ID: 123,
|
||||
CONF_NAME: "Home",
|
||||
}
|
||||
|
||||
assert result["result"].unique_id == "123"
|
||||
|
||||
|
||||
async def test_form_with_api_error(hass: HomeAssistant, mock_gios: MagicMock) -> None:
|
||||
"""Test the form is aborted because of API error."""
|
||||
@@ -76,21 +91,19 @@ async def test_form_submission_errors(
|
||||
assert result["title"] == "Home"
|
||||
|
||||
|
||||
async def test_create_entry(hass: HomeAssistant) -> None:
|
||||
"""Test that the user step works."""
|
||||
async def test_duplicate_entry(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that duplicate station IDs are rejected."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Home"
|
||||
assert result["data"] == {
|
||||
CONF_STATION_ID: 123,
|
||||
CONF_NAME: "Home",
|
||||
}
|
||||
|
||||
assert result["result"].unique_id == "123"
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
@@ -4,12 +4,10 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
|
||||
from homeassistant.components.gios.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
@@ -19,12 +17,10 @@ from tests.common import MockConfigEntry
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a successful setup entry."""
|
||||
state = hass.states.get("sensor.home_pm2_5")
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "4"
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_config_not_ready(
|
||||
@@ -93,26 +89,3 @@ async def test_migrate_unique_id_to_str(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.unique_id == "123"
|
||||
|
||||
|
||||
async def test_remove_air_quality_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_gios: MagicMock,
|
||||
) -> None:
|
||||
"""Test remove air_quality entities from registry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
entity_registry.async_get_or_create(
|
||||
AIR_QUALITY_PLATFORM,
|
||||
DOMAIN,
|
||||
"123",
|
||||
suggested_object_id="home",
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entry = entity_registry.async_get("air_quality.home")
|
||||
assert entry is None
|
||||
|
||||
@@ -37,22 +37,6 @@ async def test_sensor(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_availability(hass: HomeAssistant) -> None:
|
||||
"""Ensure that we mark the entities unavailable correctly when service causes an error."""
|
||||
state = hass.states.get("sensor.home_pm2_5")
|
||||
assert state
|
||||
assert state.state == "4"
|
||||
|
||||
state = hass.states.get("sensor.home_pm2_5_index")
|
||||
assert state
|
||||
assert state.state == "good"
|
||||
|
||||
state = hass.states.get("sensor.home_air_quality_index")
|
||||
assert state
|
||||
assert state.state == "good"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_availability_api_error(
|
||||
hass: HomeAssistant,
|
||||
|
||||
Reference in New Issue
Block a user