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:
Kamil Breguła
2026-02-16 18:59:45 +01:00
committed by GitHub
parent e6c5e72470
commit e49767d37a
13 changed files with 55 additions and 196 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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]):

View File

@@ -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(),

View File

@@ -7,5 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["dacite", "gios"],
"quality_scale": "platinum",
"requirements": ["gios==7.0.0"]
}

View File

@@ -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:

View File

@@ -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)

View File

@@ -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%]"
}
}
}
}
}

View File

@@ -1403,7 +1403,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"geofency",
"geonetnz_quakes",
"geonetnz_volcano",
"gios",
"github",
"gitlab_ci",
"gitter",

View File

@@ -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³',

View File

@@ -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"

View File

@@ -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

View File

@@ -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,