diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 84d54e7b8a5cfc..c4ac08c7cadacf 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -1,10 +1,12 @@ """Coordinator for The Internet Printing Protocol (IPP) integration.""" +from dataclasses import dataclass from datetime import timedelta import logging -from typing import override +from typing import Any, override from pyipp import IPP, IPPError, Printer as IPPPrinter +from pyipp.enums import IppOperation from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL @@ -16,12 +18,35 @@ SCAN_INTERVAL = timedelta(seconds=60) +# Integer page-count attributes returned by Get-Printer-Attributes +PAGE_COUNT_INT_ATTRIBUTES = ( + "printer-impressions-completed", + "printer-pages-completed", + "printer-media-sheets-completed", +) + +# Collection page-count attributes — dicts of monochrome/full-color sub-counters +PAGE_COUNT_COLLECTION_ATTRIBUTES = ("printer-impressions-completed-col",) + +REQUESTED_PAGE_COUNT_ATTRIBUTES = ( + *PAGE_COUNT_INT_ATTRIBUTES, + *PAGE_COUNT_COLLECTION_ATTRIBUTES, +) + _LOGGER = logging.getLogger(__name__) type IPPConfigEntry = ConfigEntry[IPPDataUpdateCoordinator] -class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): +@dataclass +class IPPData: + """Data fetched from an IPP printer.""" + + printer: IPPPrinter + page_counts: dict[str, int] + + +class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPData]): """Class to manage fetching IPP data from single endpoint.""" config_entry: IPPConfigEntry @@ -47,9 +72,51 @@ def __init__(self, hass: HomeAssistant, config_entry: IPPConfigEntry) -> None: ) @override - async def _async_update_data(self) -> IPPPrinter: + async def _async_update_data(self) -> IPPData: """Fetch data from IPP.""" try: - return await self.ipp.printer() + printer = await self.ipp.printer() except IPPError as error: raise UpdateFailed(f"Invalid response from API: {error}") from error + + # Page counts are fetched via a separate request for now. Once pyipp PR #715 + # (https://github.com/ctalkington/python-ipp/pull/715) is merged, page + # counters will be included in printer.counters by default and this extra + # request can be removed. + previous_page_counts = self.data.page_counts if self.data else {} + page_counts = await self._async_fetch_page_counts(previous_page_counts) + + return IPPData(printer=printer, page_counts=page_counts) + + async def _async_fetch_page_counts( + self, previous_page_counts: dict[str, int] + ) -> dict[str, int]: + """Fetch page count attributes from the printer.""" + try: + response = await self.ipp.execute( + IppOperation.GET_PRINTER_ATTRIBUTES, + { + "operation-attributes-tag": { + "requested-attributes": REQUESTED_PAGE_COUNT_ATTRIBUTES, + }, + }, + ) + except IPPError, TimeoutError: + _LOGGER.debug( + "Failed to fetch page count attributes from printer", exc_info=True + ) + return previous_page_counts + + parsed: dict[str, Any] = next(iter(response.get("printers") or []), {}) + page_counts: dict[str, int] = {} + + for attr in PAGE_COUNT_INT_ATTRIBUTES: + if (value := parsed.get(attr)) is not None: + page_counts[attr] = value + + # pyipp parses collection attributes into dicts of member name to value + for attr in PAGE_COUNT_COLLECTION_ATTRIBUTES: + for sub_key, sub_value in parsed.get(attr, {}).items(): + page_counts[f"{attr}/{sub_key}"] = sub_value + + return page_counts diff --git a/homeassistant/components/ipp/diagnostics.py b/homeassistant/components/ipp/diagnostics.py index 2d2c587e5108bc..7e606063d81f68 100644 --- a/homeassistant/components/ipp/diagnostics.py +++ b/homeassistant/components/ipp/diagnostics.py @@ -20,5 +20,6 @@ async def async_get_config_entry_diagnostics( }, "unique_id": config_entry.unique_id, }, - "data": coordinator.data.as_dict(), + "data": coordinator.data.printer.as_dict(), + "page_counts": coordinator.data.page_counts, } diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py index ca961be07dfd46..c2c077ea51a8e2 100644 --- a/homeassistant/components/ipp/entity.py +++ b/homeassistant/components/ipp/entity.py @@ -24,12 +24,13 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{coordinator.device_id}_{description.key}" + printer = self.coordinator.data.printer self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, coordinator.device_id)}, - manufacturer=self.coordinator.data.info.manufacturer, - model=self.coordinator.data.info.model, - name=self.coordinator.data.info.name, - serial_number=self.coordinator.data.info.serial, - sw_version=self.coordinator.data.info.version, - configuration_url=self.coordinator.data.info.more_info, + manufacturer=printer.info.manufacturer, + model=printer.info.model, + name=printer.info.name, + serial_number=printer.info.serial, + sw_version=printer.info.version, + configuration_url=printer.info.more_info, ) diff --git a/homeassistant/components/ipp/icons.json b/homeassistant/components/ipp/icons.json index 1469ba01518363..555220f57eba66 100644 --- a/homeassistant/components/ipp/icons.json +++ b/homeassistant/components/ipp/icons.json @@ -1,9 +1,24 @@ { "entity": { "sensor": { + "impressions_completed": { + "default": "mdi:file-document-outline" + }, + "impressions_completed_full_color": { + "default": "mdi:file-document-outline" + }, + "impressions_completed_monochrome": { + "default": "mdi:file-document-outline" + }, "marker": { "default": "mdi:water" }, + "media_sheets_completed": { + "default": "mdi:file-document-outline" + }, + "pages_completed": { + "default": "mdi:file-document-outline" + }, "printer": { "default": "mdi:printer" }, diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index e3cc30dc5e046a..77cd2cf218ee92 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -41,6 +41,13 @@ class IPPSensorEntityDescription(SensorEntityDescription): attributes_fn: Callable[[Printer], dict[Any, StateType]] = lambda _: {} +@dataclass(frozen=True, kw_only=True) +class IPPPageCountSensorEntityDescription(SensorEntityDescription): + """Describes IPP page count sensor entity.""" + + ipp_attribute: str + + def _get_marker_attributes_fn( marker_index: int, attributes_fn: Callable[[Marker], dict[Any, StateType]] ) -> Callable[[Printer], dict[Any, StateType]]: @@ -81,6 +88,44 @@ def _get_marker_value_fn( ), ) +PAGE_COUNT_SENSORS: tuple[IPPPageCountSensorEntityDescription, ...] = ( + IPPPageCountSensorEntityDescription( + key="pages_completed", + translation_key="pages_completed", + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + ipp_attribute="printer-pages-completed", + ), + IPPPageCountSensorEntityDescription( + key="impressions_completed", + translation_key="impressions_completed", + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + ipp_attribute="printer-impressions-completed", + ), + IPPPageCountSensorEntityDescription( + key="media_sheets_completed", + translation_key="media_sheets_completed", + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + ipp_attribute="printer-media-sheets-completed", + ), + IPPPageCountSensorEntityDescription( + key="impressions_completed_monochrome", + translation_key="impressions_completed_monochrome", + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + ipp_attribute="printer-impressions-completed-col/monochrome", + ), + IPPPageCountSensorEntityDescription( + key="impressions_completed_full_color", + translation_key="impressions_completed_full_color", + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + ipp_attribute="printer-impressions-completed-col/full-color", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -97,7 +142,7 @@ async def async_setup_entry( for description in PRINTER_SENSORS ] - for index, marker in enumerate(coordinator.data.markers): + for index, marker in enumerate(coordinator.data.printer.markers): sensors.append( IPPSensor( coordinator, @@ -123,6 +168,12 @@ async def async_setup_entry( ) ) + sensors.extend( + IPPPageCountSensor(coordinator, description) + for description in PAGE_COUNT_SENSORS + if description.ipp_attribute in coordinator.data.page_counts + ) + async_add_entities(sensors, True) @@ -135,10 +186,24 @@ class IPPSensor(IPPEntity, SensorEntity): @override def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the entity.""" - return self.entity_description.attributes_fn(self.coordinator.data) + return self.entity_description.attributes_fn(self.coordinator.data.printer) @property @override def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" - return self.entity_description.value_fn(self.coordinator.data) + return self.entity_description.value_fn(self.coordinator.data.printer) + + +class IPPPageCountSensor(IPPEntity, SensorEntity): + """Defines an IPP page count sensor.""" + + entity_description: IPPPageCountSensorEntityDescription + + @property + @override + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.coordinator.data.page_counts.get( + self.entity_description.ipp_attribute + ) diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index 2bb9f08a701c19..f94fd835d2fd04 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -34,6 +34,26 @@ }, "entity": { "sensor": { + "impressions_completed": { + "name": "Impressions completed", + "unit_of_measurement": "impressions" + }, + "impressions_completed_full_color": { + "name": "Color impressions completed", + "unit_of_measurement": "[%key:component::ipp::entity::sensor::impressions_completed::unit_of_measurement%]" + }, + "impressions_completed_monochrome": { + "name": "Monochrome impressions completed", + "unit_of_measurement": "[%key:component::ipp::entity::sensor::impressions_completed::unit_of_measurement%]" + }, + "media_sheets_completed": { + "name": "Media sheets completed", + "unit_of_measurement": "sheets" + }, + "pages_completed": { + "name": "Pages completed", + "unit_of_measurement": "pages" + }, "printer": { "state": { "idle": "[%key:common::state::idle%]", diff --git a/tests/components/ipp/conftest.py b/tests/components/ipp/conftest.py index 54b8ed60452753..10a73374544da8 100644 --- a/tests/components/ipp/conftest.py +++ b/tests/components/ipp/conftest.py @@ -81,6 +81,19 @@ def mock_ipp(mock_printer: Printer) -> Generator[MagicMock]: ) as ipp_mock: client = ipp_mock.return_value client.printer.return_value = mock_printer + client.execute.return_value = { + "printers": [ + { + "printer-pages-completed": 1234, + "printer-impressions-completed": 2468, + "printer-media-sheets-completed": 1234, + "printer-impressions-completed-col": { + "monochrome": 1500, + "full-color": 968, + }, + } + ], + } yield client diff --git a/tests/components/ipp/snapshots/test_diagnostics.ambr b/tests/components/ipp/snapshots/test_diagnostics.ambr index bd2564c5a4098d..6f2cf07b957c90 100644 --- a/tests/components/ipp/snapshots/test_diagnostics.ambr +++ b/tests/components/ipp/snapshots/test_diagnostics.ambr @@ -97,5 +97,12 @@ }), 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251', }), + 'page_counts': dict({ + 'printer-impressions-completed': 2468, + 'printer-impressions-completed-col/full-color': 968, + 'printer-impressions-completed-col/monochrome': 1500, + 'printer-media-sheets-completed': 1234, + 'printer-pages-completed': 1234, + }), }) # --- diff --git a/tests/components/ipp/snapshots/test_sensor.ambr b/tests/components/ipp/snapshots/test_sensor.ambr index 717164343b8d54..454d9fb0f04077 100644 --- a/tests/components/ipp/snapshots/test_sensor.ambr +++ b/tests/components/ipp/snapshots/test_sensor.ambr @@ -125,6 +125,60 @@ 'state': '58', }) # --- +# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Color impressions completed', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Color impressions completed', + 'platform': 'ipp', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'impressions_completed_full_color', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_full_color', + 'unit_of_measurement': 'impressions', + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Test HA-1000 Series Color impressions completed', + : , + : 'impressions', + }), + 'context': , + 'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '968', + }) +# --- # name: test_sensors[sensor.test_ha_1000_series_cyan_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -182,6 +236,60 @@ 'state': '91', }) # --- +# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_impressions_completed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Impressions completed', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Impressions completed', + 'platform': 'ipp', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'impressions_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed', + 'unit_of_measurement': 'impressions', + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Test HA-1000 Series Impressions completed', + : , + : 'impressions', + }), + 'context': , + 'entity_id': 'sensor.test_ha_1000_series_impressions_completed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2468', + }) +# --- # name: test_sensors[sensor.test_ha_1000_series_magenta_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -239,6 +347,168 @@ 'state': '73', }) # --- +# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Media sheets completed', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Media sheets completed', + 'platform': 'ipp', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'media_sheets_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_media_sheets_completed', + 'unit_of_measurement': 'sheets', + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Test HA-1000 Series Media sheets completed', + : , + : 'sheets', + }), + 'context': , + 'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1234', + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Monochrome impressions completed', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Monochrome impressions completed', + 'platform': 'ipp', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'impressions_completed_monochrome', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_monochrome', + 'unit_of_measurement': 'impressions', + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Test HA-1000 Series Monochrome impressions completed', + : , + : 'impressions', + }), + 'context': , + 'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1500', + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_pages_completed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_pages_completed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Pages completed', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pages completed', + 'platform': 'ipp', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pages_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_pages_completed', + 'unit_of_measurement': 'pages', + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_pages_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Test HA-1000 Series Pages completed', + : , + : 'pages', + }), + 'context': , + 'entity_id': 'sensor.test_ha_1000_series_pages_completed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1234', + }) +# --- # name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index bdbb9a88d3559e..7915d783b9c945 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,10 +1,13 @@ """Tests for the IPP sensor platform.""" -from unittest.mock import AsyncMock +from typing import Any +from unittest.mock import AsyncMock, MagicMock +from pyipp import IPPError import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -29,13 +32,18 @@ async def test_disabled_by_default_sensors( init_integration: MockConfigEntry, ) -> None: """Test the disabled by default IPP sensors.""" - state = hass.states.get("sensor.test_ha_1000_series_uptime") + entity_id = entity_registry.async_get_entity_id( + "sensor", "ipp", f"{init_integration.unique_id}_uptime" + ) + assert entity_id is not None + + state = hass.states.get(entity_id) assert state is None - entry = entity_registry.async_get("sensor.test_ha_1000_series_uptime") - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_missing_entry_unique_id( @@ -54,3 +62,71 @@ async def test_missing_entry_unique_id( entity = entity_registry.async_get("sensor.test_ha_1000_series") assert entity assert entity.unique_id == f"{mock_config_entry.entry_id}_printer" + + +@pytest.mark.parametrize( + "execute_response", + [ + {"printers": [{}]}, # Empty printer dict + {"printers": []}, # Empty printers list + {}, # Missing printers key + ], +) +async def test_no_page_count_sensors_when_unsupported( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_ipp: MagicMock, + execute_response: dict[str, Any], +) -> None: + """Test that page count sensors are not created when printer doesn't support them.""" + mock_ipp.execute.return_value = execute_response + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + unique_id = mock_config_entry.unique_id + for key in ( + "pages_completed", + "impressions_completed", + "media_sheets_completed", + "impressions_completed_monochrome", + "impressions_completed_full_color", + ): + assert not entity_registry.async_get_entity_id( + "sensor", "ipp", f"{unique_id}_{key}" + ) + + +@pytest.mark.parametrize( + ("execute_side_effect", "execute_response", "expected_state"), + [ + pytest.param(IPPError("boom"), None, "1234", id="error-retains-previous"), + pytest.param(None, {"printers": [{}]}, STATE_UNKNOWN, id="empty-clears"), + ], +) +async def test_page_counts_after_fetch_issue( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_ipp: MagicMock, + execute_side_effect: IPPError | None, + execute_response: dict[str, Any] | None, + expected_state: str, +) -> None: + """Test page count sensor values after a failed or empty fetch. + + A failed request keeps the previous values, while a successful response + without page count attributes clears them. + """ + assert hass.states.get("sensor.test_ha_1000_series_pages_completed").state == "1234" + + mock_ipp.execute.side_effect = execute_side_effect + mock_ipp.execute.return_value = execute_response + await init_integration.runtime_data.async_refresh() + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.test_ha_1000_series_pages_completed").state + == expected_state + )