From 46696632923ab7c84ad4e02d52cbdfcfcee5318b Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 14 Mar 2026 16:36:04 -0400 Subject: [PATCH 01/23] Add page count sensors to IPP integration Fetch page count attributes from IPP printers via a separate Get-Printer-Attributes request. Supports both simple integer attributes (printer-impressions-completed, printer-pages-completed, printer-media-sheets-completed) and collection-based attributes (printer-impressions-completed-col with monochrome/full-color sub-values). Sensors are only created when the printer exposes the corresponding attributes. Co-Authored-By: Claude Opus 4.6 --- homeassistant/components/ipp/coordinator.py | 53 +++- homeassistant/components/ipp/icons.json | 15 + homeassistant/components/ipp/sensor.py | 72 ++++- homeassistant/components/ipp/strings.json | 15 + tests/components/ipp/conftest.py | 13 + .../components/ipp/snapshots/test_sensor.ambr | 260 ++++++++++++++++++ 6 files changed, 425 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 84d54e7b8a5cfc..8ed28aca873741 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -2,9 +2,10 @@ 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,6 +17,13 @@ SCAN_INTERVAL = timedelta(seconds=60) +PAGE_COUNT_ATTRIBUTES = [ + "printer-impressions-completed", + "printer-pages-completed", + "printer-media-sheets-completed", + "printer-impressions-completed-col", +] + _LOGGER = logging.getLogger(__name__) type IPPConfigEntry = ConfigEntry[IPPDataUpdateCoordinator] @@ -25,10 +33,12 @@ class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): """Class to manage fetching IPP data from single endpoint.""" config_entry: IPPConfigEntry + page_counts: dict[str, int] def __init__(self, hass: HomeAssistant, config_entry: IPPConfigEntry) -> None: """Initialize global IPP data updater.""" self.device_id = config_entry.unique_id or config_entry.entry_id + self.page_counts = {} self.ipp = IPP( host=config_entry.data[CONF_HOST], port=config_entry.data[CONF_PORT], @@ -50,6 +60,45 @@ def __init__(self, hass: HomeAssistant, config_entry: IPPConfigEntry) -> None: async def _async_update_data(self) -> IPPPrinter: """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 + + self.page_counts = await self._async_fetch_page_counts() + + return printer + + async def _async_fetch_page_counts(self) -> dict[str, int]: + """Fetch page count attributes from the printer.""" + page_counts: dict[str, int] = {} + + try: + response = await self.ipp.execute( + IppOperation.GET_PRINTER_ATTRIBUTES, + { + "operation-attributes-tag": { + "requested-attributes": PAGE_COUNT_ATTRIBUTES, + }, + }, + ) + except IPPError: + return page_counts + + parsed: dict[str, Any] = next( + iter(response.get("printers") or []), {} + ) + for attr in PAGE_COUNT_ATTRIBUTES: + if attr not in parsed: + continue + value = parsed[attr] + if isinstance(value, int): + page_counts[attr] = value + elif isinstance(value, dict): + # Handle collection attributes like + # printer-impressions-completed-col + # which contain sub-keys like "monochrome" and "full-color" + for sub_key, sub_value in value.items(): + if isinstance(sub_value, int): + page_counts[f"{attr}/{sub_key}"] = sub_value + + return page_counts diff --git a/homeassistant/components/ipp/icons.json b/homeassistant/components/ipp/icons.json index 1469ba01518363..69907aa19b6cff 100644 --- a/homeassistant/components/ipp/icons.json +++ b/homeassistant/components/ipp/icons.json @@ -9,6 +9,21 @@ }, "uptime": { "default": "mdi:clock-outline" + }, + "pages_completed": { + "default": "mdi:file-document-outline" + }, + "impressions_completed": { + "default": "mdi:file-document-outline" + }, + "media_sheets_completed": { + "default": "mdi:file-document-outline" + }, + "impressions_completed_monochrome": { + "default": "mdi:file-document-outline" + }, + "impressions_completed_full_color": { + "default": "mdi:file-document-outline" } } } diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index e3cc30dc5e046a..9d8d1b58d2ac02 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -29,7 +29,7 @@ ATTR_STATE_REASON, ATTR_URI_SUPPORTED, ) -from .coordinator import IPPConfigEntry +from .coordinator import IPPConfigEntry, IPPDataUpdateCoordinator from .entity import IPPEntity @@ -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, @@ -123,6 +168,10 @@ async def async_setup_entry( ) ) + for description in PAGE_COUNT_SENSORS: + if description.ipp_attribute in coordinator.page_counts: + sensors.append(IPPPageCountSensor(coordinator, description)) + async_add_entities(sensors, True) @@ -142,3 +191,24 @@ def extra_state_attributes(self) -> dict[str, Any]: def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data) + + +class IPPPageCountSensor(IPPEntity, SensorEntity): + """Defines an IPP page count sensor.""" + + entity_description: IPPPageCountSensorEntityDescription + + def __init__( + self, + coordinator: IPPDataUpdateCoordinator, + description: IPPPageCountSensorEntityDescription, + ) -> None: + """Initialize the page count sensor.""" + super().__init__(coordinator, description) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.coordinator.page_counts.get( + self.entity_description.ipp_attribute + ) diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index 2bb9f08a701c19..eef1f0e39c4572 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -43,6 +43,21 @@ }, "uptime": { "name": "Uptime" + }, + "pages_completed": { + "name": "Pages completed" + }, + "impressions_completed": { + "name": "Impressions completed" + }, + "media_sheets_completed": { + "name": "Media sheets completed" + }, + "impressions_completed_monochrome": { + "name": "Monochrome impressions completed" + }, + "impressions_completed_full_color": { + "name": "Color impressions completed" } } } 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_sensor.ambr b/tests/components/ipp/snapshots/test_sensor.ambr index 717164343b8d54..68dbb357be4843 100644 --- a/tests/components/ipp/snapshots/test_sensor.ambr +++ b/tests/components/ipp/snapshots/test_sensor.ambr @@ -125,6 +125,58 @@ 'state': '58', }) # --- +# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + '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': None, + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test HA-1000 Series Color impressions completed', + 'state_class': , + }), + '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 +234,58 @@ 'state': '91', }) # --- +# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + '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': None, + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test HA-1000 Series Impressions completed', + 'state_class': , + }), + '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 +343,162 @@ 'state': '73', }) # --- +# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + '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': None, + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test HA-1000 Series Media sheets completed', + 'state_class': , + }), + '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': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + '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': None, + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test HA-1000 Series Monochrome impressions completed', + 'state_class': , + }), + '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': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + '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': None, + }) +# --- +# name: test_sensors[sensor.test_ha_1000_series_pages_completed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test HA-1000 Series Pages completed', + 'state_class': , + }), + '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([ From 37dfb1a622441c82a55ca791d8697b305f0b1595 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 14 Mar 2026 16:49:28 -0400 Subject: [PATCH 02/23] Address review feedback for IPP page count sensors - Sort strings.json keys alphabetically to pass prek checks - Preserve previous page_counts on transient fetch failure - Add test for printers that don't support page count attributes Co-Authored-By: Claude Opus 4.6 --- homeassistant/components/ipp/coordinator.py | 9 +++---- homeassistant/components/ipp/sensor.py | 12 ++++----- homeassistant/components/ipp/strings.json | 30 ++++++++++----------- tests/components/ipp/test_sensor.py | 28 ++++++++++++++++++- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 8ed28aca873741..5b583043bf0261 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -70,8 +70,6 @@ async def _async_update_data(self) -> IPPPrinter: async def _async_fetch_page_counts(self) -> dict[str, int]: """Fetch page count attributes from the printer.""" - page_counts: dict[str, int] = {} - try: response = await self.ipp.execute( IppOperation.GET_PRINTER_ATTRIBUTES, @@ -82,11 +80,10 @@ async def _async_fetch_page_counts(self) -> dict[str, int]: }, ) except IPPError: - return page_counts + return self.page_counts - parsed: dict[str, Any] = next( - iter(response.get("printers") or []), {} - ) + page_counts: dict[str, int] = {} + parsed: dict[str, Any] = next(iter(response.get("printers") or []), {}) for attr in PAGE_COUNT_ATTRIBUTES: if attr not in parsed: continue diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 9d8d1b58d2ac02..0083b02984563f 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -168,9 +168,11 @@ async def async_setup_entry( ) ) - for description in PAGE_COUNT_SENSORS: - if description.ipp_attribute in coordinator.page_counts: - sensors.append(IPPPageCountSensor(coordinator, description)) + sensors.extend( + IPPPageCountSensor(coordinator, description) + for description in PAGE_COUNT_SENSORS + if description.ipp_attribute in coordinator.page_counts + ) async_add_entities(sensors, True) @@ -209,6 +211,4 @@ def __init__( @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.coordinator.page_counts.get( - self.entity_description.ipp_attribute - ) + return self.coordinator.page_counts.get(self.entity_description.ipp_attribute) diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index eef1f0e39c4572..4eb787006ed17e 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -34,6 +34,21 @@ }, "entity": { "sensor": { + "impressions_completed": { + "name": "Impressions completed" + }, + "impressions_completed_full_color": { + "name": "Color impressions completed" + }, + "impressions_completed_monochrome": { + "name": "Monochrome impressions completed" + }, + "media_sheets_completed": { + "name": "Media sheets completed" + }, + "pages_completed": { + "name": "Pages completed" + }, "printer": { "state": { "idle": "[%key:common::state::idle%]", @@ -43,21 +58,6 @@ }, "uptime": { "name": "Uptime" - }, - "pages_completed": { - "name": "Pages completed" - }, - "impressions_completed": { - "name": "Impressions completed" - }, - "media_sheets_completed": { - "name": "Media sheets completed" - }, - "impressions_completed_monochrome": { - "name": "Monochrome impressions completed" - }, - "impressions_completed_full_color": { - "name": "Color impressions completed" } } } diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index bdbb9a88d3559e..672de3be593156 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,6 +1,6 @@ """Tests for the IPP sensor platform.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock import pytest from syrupy.assertion import SnapshotAssertion @@ -54,3 +54,29 @@ 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" + + +async def test_no_page_count_sensors_when_unsupported( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_ipp: MagicMock, +) -> None: + """Test that page count sensors are not created when printer doesn't support them.""" + mock_ipp.execute.return_value = {"printers": [{}]} + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.states.get("sensor.test_ha_1000_series_pages_completed") + assert not hass.states.get("sensor.test_ha_1000_series_impressions_completed") + assert not hass.states.get("sensor.test_ha_1000_series_media_sheets_completed") + assert not hass.states.get( + "sensor.test_ha_1000_series_monochrome_impressions_completed" + ) + assert not hass.states.get("sensor.test_ha_1000_series_color_impressions_completed") + assert not entity_registry.async_get("sensor.test_ha_1000_series_pages_completed") + assert not entity_registry.async_get( + "sensor.test_ha_1000_series_impressions_completed" + ) From fe802f5fb954740d264b992dbb27e845fed57fb7 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 14 Mar 2026 17:11:20 -0400 Subject: [PATCH 03/23] Sort icons.json keys alphabetically Co-Authored-By: Claude Opus 4.6 --- homeassistant/components/ipp/icons.json | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/ipp/icons.json b/homeassistant/components/ipp/icons.json index 69907aa19b6cff..555220f57eba66 100644 --- a/homeassistant/components/ipp/icons.json +++ b/homeassistant/components/ipp/icons.json @@ -1,29 +1,29 @@ { "entity": { "sensor": { - "marker": { - "default": "mdi:water" - }, - "printer": { - "default": "mdi:printer" - }, - "uptime": { - "default": "mdi:clock-outline" + "impressions_completed": { + "default": "mdi:file-document-outline" }, - "pages_completed": { + "impressions_completed_full_color": { "default": "mdi:file-document-outline" }, - "impressions_completed": { + "impressions_completed_monochrome": { "default": "mdi:file-document-outline" }, + "marker": { + "default": "mdi:water" + }, "media_sheets_completed": { "default": "mdi:file-document-outline" }, - "impressions_completed_monochrome": { + "pages_completed": { "default": "mdi:file-document-outline" }, - "impressions_completed_full_color": { - "default": "mdi:file-document-outline" + "printer": { + "default": "mdi:printer" + }, + "uptime": { + "default": "mdi:clock-outline" } } } From 65d27f325ac1835493859e7ad54a367e7a77a85d Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sun, 22 Mar 2026 07:53:23 -0400 Subject: [PATCH 04/23] Add debug logging for page count fetch failures Co-Authored-By: Claude Opus 4.6 (1M context) --- homeassistant/components/ipp/coordinator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 5b583043bf0261..6fc1c17f709dd3 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -80,6 +80,9 @@ async def _async_fetch_page_counts(self) -> dict[str, int]: }, ) except IPPError: + _LOGGER.debug( + "Failed to fetch page count attributes from printer", exc_info=True + ) return self.page_counts page_counts: dict[str, int] = {} From f8e849638360f8697f19e111c7dbfcb71c3def36 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 26 Mar 2026 08:05:29 -0400 Subject: [PATCH 05/23] Fix test failures: update snapshots and use unique_id lookups Merge upstream dev to pick up entity registry changes, regenerate test snapshots, and update tests to look up entities by unique_id instead of hardcoded entity_ids that depend on translation resolution. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/ipp/snapshots/test_sensor.ambr | 325 +++++++++--------- tests/components/ipp/test_sensor.py | 37 +- 2 files changed, 186 insertions(+), 176 deletions(-) diff --git a/tests/components/ipp/snapshots/test_sensor.ambr b/tests/components/ipp/snapshots/test_sensor.ambr index 68dbb357be4843..1d7489282cb7d6 100644 --- a/tests/components/ipp/snapshots/test_sensor.ambr +++ b/tests/components/ipp/snapshots/test_sensor.ambr @@ -6,7 +6,7 @@ ]), 'area_id': None, 'capabilities': dict({ - : list([ + 'options': list([ 'idle', 'printing', 'stopped', @@ -46,11 +46,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF', - : 'enum', - : 'Test HA-1000 Series', + 'device_class': 'enum', + 'friendly_name': 'Test HA-1000 Series', 'info': 'Test HA-1000 Series', 'location': None, - : list([ + 'options': list([ 'idle', 'printing', 'stopped', @@ -68,23 +68,21 @@ 'state': 'idle', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_black_ink-entry] +# name: test_sensors[sensor.test_ha_1000_series_2-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, - 'capabilities': dict({ - : , - }), + 'capabilities': None, 'config_entry_id': , 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_ha_1000_series_black_ink', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -92,43 +90,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Black ink', + 'object_id_base': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Black ink', + 'original_name': None, 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'marker', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_0', - 'unit_of_measurement': '%', + 'translation_key': 'uptime', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_uptime', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_black_ink-state] +# name: test_sensors[sensor.test_ha_1000_series_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'Test HA-1000 Series Black ink', - 'marker_high_level': 100, - 'marker_low_level': 10, - 'marker_type': 'ink-cartridge', - : , - : '%', + 'device_class': 'timestamp', + 'friendly_name': 'Test HA-1000 Series', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_black_ink', + 'entity_id': 'sensor.test_ha_1000_series_2', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '58', + 'state': '2019-11-11T09:10:02+00:00', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-entry] +# name: test_sensors[sensor.test_ha_1000_series_3-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': dict({ 'state_class': , @@ -140,7 +135,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed', + 'entity_id': 'sensor.test_ha_1000_series_3', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -148,43 +143,43 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Color impressions completed', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Color impressions completed', + 'original_name': None, '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', + 'translation_key': 'pages_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_pages_completed', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-state] +# name: test_sensors[sensor.test_ha_1000_series_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Color impressions completed', + 'friendly_name': 'Test HA-1000 Series', 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed', + 'entity_id': 'sensor.test_ha_1000_series_3', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '968', + 'state': '1234', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-entry] +# name: test_sensors[sensor.test_ha_1000_series_4-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - : , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -192,8 +187,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_4', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -201,43 +196,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Cyan ink', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Cyan ink', + 'original_name': None, 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'marker', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_1', - 'unit_of_measurement': '%', + 'translation_key': 'impressions_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-state] +# name: test_sensors[sensor.test_ha_1000_series_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'Test HA-1000 Series Cyan ink', - 'marker_high_level': 100, - 'marker_low_level': 10, - 'marker_type': 'ink-cartridge', - : , - : '%', + 'friendly_name': 'Test HA-1000 Series', + 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', + 'entity_id': 'sensor.test_ha_1000_series_4', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '91', + 'state': '2468', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-entry] +# name: test_sensors[sensor.test_ha_1000_series_5-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': dict({ 'state_class': , @@ -249,7 +241,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_impressions_completed', + 'entity_id': 'sensor.test_ha_1000_series_5', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -257,43 +249,43 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Impressions completed', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Impressions completed', + 'original_name': None, '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', + 'translation_key': 'media_sheets_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_media_sheets_completed', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-state] +# name: test_sensors[sensor.test_ha_1000_series_5-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Impressions completed', + 'friendly_name': 'Test HA-1000 Series', 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_impressions_completed', + 'entity_id': 'sensor.test_ha_1000_series_5', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2468', + 'state': '1234', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-entry] +# name: test_sensors[sensor.test_ha_1000_series_6-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - : , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -301,8 +293,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_6', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -310,43 +302,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Magenta ink', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Magenta ink', + 'original_name': None, 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'marker', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_2', - 'unit_of_measurement': '%', + 'translation_key': 'impressions_completed_monochrome', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_monochrome', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-state] +# name: test_sensors[sensor.test_ha_1000_series_6-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'Test HA-1000 Series Magenta ink', - 'marker_high_level': 100, - 'marker_low_level': 10, - 'marker_type': 'ink-cartridge', - : , - : '%', + 'friendly_name': 'Test HA-1000 Series', + 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', + 'entity_id': 'sensor.test_ha_1000_series_6', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '73', + 'state': '1500', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-entry] +# name: test_sensors[sensor.test_ha_1000_series_7-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': dict({ 'state_class': , @@ -358,7 +347,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed', + 'entity_id': 'sensor.test_ha_1000_series_7', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -366,42 +355,43 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Media sheets completed', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Media sheets completed', + 'original_name': None, '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', + 'translation_key': 'impressions_completed_full_color', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_full_color', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-state] +# name: test_sensors[sensor.test_ha_1000_series_7-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Media sheets completed', + 'friendly_name': 'Test HA-1000 Series', 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed', + 'entity_id': 'sensor.test_ha_1000_series_7', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1234', + 'state': '968', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-entry] +# name: test_sensors[sensor.test_ha_1000_series_black_ink-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -409,8 +399,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed', + 'entity_category': None, + 'entity_id': 'sensor.test_ha_1000_series_black_ink', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -418,42 +408,47 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Monochrome impressions completed', + 'object_id_base': 'Black ink', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Monochrome impressions completed', + 'original_name': 'Black ink', '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': None, + 'translation_key': 'marker', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_0', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-state] +# name: test_sensors[sensor.test_ha_1000_series_black_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Monochrome impressions completed', - 'state_class': , + 'friendly_name': 'Test HA-1000 Series Black ink', + 'marker_high_level': 100, + 'marker_low_level': 10, + 'marker_type': 'ink-cartridge', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed', + 'entity_id': 'sensor.test_ha_1000_series_black_ink', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1500', + 'state': '58', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_pages_completed-entry] +# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -461,8 +456,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_pages_completed', + 'entity_category': None, + 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -470,43 +465,47 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Pages completed', + 'object_id_base': 'Cyan ink', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Pages completed', + 'original_name': 'Cyan ink', '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': None, + 'translation_key': 'marker', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_1', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_pages_completed-state] +# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Pages completed', - 'state_class': , + 'friendly_name': 'Test HA-1000 Series Cyan ink', + 'marker_high_level': 100, + 'marker_low_level': 10, + 'marker_type': 'ink-cartridge', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_pages_completed', + 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1234', + 'state': '91', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-entry] +# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - : , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -515,7 +514,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', + 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -523,54 +522,56 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Photo black ink', + 'object_id_base': 'Magenta ink', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Photo black ink', + 'original_name': 'Magenta ink', 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'marker', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_3', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_2', 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-state] +# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'Test HA-1000 Series Photo black ink', + 'friendly_name': 'Test HA-1000 Series Magenta ink', 'marker_high_level': 100, 'marker_low_level': 10, 'marker_type': 'ink-cartridge', - : , - : '%', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', + 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '98', + 'state': '73', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_uptime-entry] +# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), '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_uptime', + 'entity_category': None, + 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -578,33 +579,37 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Uptime', + 'object_id_base': 'Photo black ink', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Uptime', + 'original_name': 'Photo black ink', 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'uptime', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_uptime', - 'unit_of_measurement': None, + 'translation_key': 'marker', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_3', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_uptime-state] +# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'timestamp', - : 'Test HA-1000 Series Uptime', + 'friendly_name': 'Test HA-1000 Series Photo black ink', + 'marker_high_level': 100, + 'marker_low_level': 10, + 'marker_type': 'ink-cartridge', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_uptime', + 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2019-11-11T09:10:02+00:00', + 'state': '98', }) # --- # name: test_sensors[sensor.test_ha_1000_series_yellow_ink-entry] @@ -614,7 +619,7 @@ ]), 'area_id': None, 'capabilities': dict({ - : , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -649,12 +654,12 @@ # name: test_sensors[sensor.test_ha_1000_series_yellow_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'Test HA-1000 Series Yellow ink', + 'friendly_name': 'Test HA-1000 Series Yellow ink', 'marker_high_level': 100, 'marker_low_level': 10, 'marker_type': 'ink-cartridge', - : , - : '%', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_yellow_ink', diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 672de3be593156..b07a1fbeac928d 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -29,13 +29,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") + entry = entity_registry.async_get_entity_id( + "sensor", "ipp", f"{init_integration.unique_id}_uptime" + ) + assert entry is not None + + state = hass.states.get(entry) 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(entry) + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_missing_entry_unique_id( @@ -69,14 +74,14 @@ async def test_no_page_count_sensors_when_unsupported( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert not hass.states.get("sensor.test_ha_1000_series_pages_completed") - assert not hass.states.get("sensor.test_ha_1000_series_impressions_completed") - assert not hass.states.get("sensor.test_ha_1000_series_media_sheets_completed") - assert not hass.states.get( - "sensor.test_ha_1000_series_monochrome_impressions_completed" - ) - assert not hass.states.get("sensor.test_ha_1000_series_color_impressions_completed") - assert not entity_registry.async_get("sensor.test_ha_1000_series_pages_completed") - assert not entity_registry.async_get( - "sensor.test_ha_1000_series_impressions_completed" - ) + 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}" + ) From da150871d73975c406e002fd8da1693eb6ebbed5 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 26 Mar 2026 08:26:39 -0400 Subject: [PATCH 06/23] Regenerate snapshots with translations resolved Previous snapshot generation was missing translation files, causing entity IDs to be generic numbered suffixes instead of proper names. Generated translations first, then regenerated snapshots. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/ipp/snapshots/test_sensor.ambr | 278 +++++++++--------- 1 file changed, 139 insertions(+), 139 deletions(-) diff --git a/tests/components/ipp/snapshots/test_sensor.ambr b/tests/components/ipp/snapshots/test_sensor.ambr index 1d7489282cb7d6..7f4f1ee2b20bbb 100644 --- a/tests/components/ipp/snapshots/test_sensor.ambr +++ b/tests/components/ipp/snapshots/test_sensor.ambr @@ -68,21 +68,23 @@ 'state': 'idle', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_2-entry] +# name: test_sensors[sensor.test_ha_1000_series_black_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), '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_2', + 'entity_category': None, + 'entity_id': 'sensor.test_ha_1000_series_black_ink', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -90,36 +92,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Black ink', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Black ink', 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'uptime', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_uptime', - 'unit_of_measurement': None, + 'translation_key': 'marker', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_0', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_2-state] +# name: test_sensors[sensor.test_ha_1000_series_black_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'timestamp', - 'friendly_name': 'Test HA-1000 Series', + 'friendly_name': 'Test HA-1000 Series Black ink', + 'marker_high_level': 100, + 'marker_low_level': 10, + 'marker_type': 'ink-cartridge', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_2', + 'entity_id': 'sensor.test_ha_1000_series_black_ink', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2019-11-11T09:10:02+00:00', + 'state': '58', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_3-entry] +# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -135,7 +141,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_3', + 'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -143,43 +149,43 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Color impressions completed', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Color impressions 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', + 'translation_key': 'impressions_completed_full_color', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_full_color', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_3-state] +# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series', + 'friendly_name': 'Test HA-1000 Series Color impressions completed', 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_3', + 'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1234', + 'state': '968', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_4-entry] +# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -187,8 +193,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_4', + 'entity_category': None, + 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -196,36 +202,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Cyan ink', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Cyan ink', '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': None, + 'translation_key': 'marker', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_1', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_4-state] +# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series', - 'state_class': , + 'friendly_name': 'Test HA-1000 Series Cyan ink', + 'marker_high_level': 100, + 'marker_low_level': 10, + 'marker_type': 'ink-cartridge', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_4', + 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2468', + 'state': '91', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_5-entry] +# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -241,7 +251,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_5', + 'entity_id': 'sensor.test_ha_1000_series_impressions_completed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -249,43 +259,43 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Impressions completed', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Impressions 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', + 'translation_key': 'impressions_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_5-state] +# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series', + 'friendly_name': 'Test HA-1000 Series Impressions completed', 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_5', + 'entity_id': 'sensor.test_ha_1000_series_impressions_completed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1234', + 'state': '2468', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_6-entry] +# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -293,8 +303,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_6', + 'entity_category': None, + 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -302,36 +312,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Magenta ink', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Magenta ink', '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': None, + 'translation_key': 'marker', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_2', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_6-state] +# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series', - 'state_class': , + 'friendly_name': 'Test HA-1000 Series Magenta ink', + 'marker_high_level': 100, + 'marker_low_level': 10, + 'marker_type': 'ink-cartridge', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_6', + 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1500', + 'state': '73', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_7-entry] +# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -347,7 +361,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.test_ha_1000_series_7', + 'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -355,43 +369,43 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Media sheets completed', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Media sheets 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', + 'translation_key': 'media_sheets_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_media_sheets_completed', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_7-state] +# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series', + 'friendly_name': 'Test HA-1000 Series Media sheets completed', 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_7', + 'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '968', + 'state': '1234', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_black_ink-entry] +# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -399,8 +413,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_ha_1000_series_black_ink', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -408,47 +422,43 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Black ink', + 'object_id_base': 'Monochrome impressions completed', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Black ink', + 'original_name': 'Monochrome impressions completed', 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'marker', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_0', - 'unit_of_measurement': '%', + 'translation_key': 'impressions_completed_monochrome', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_monochrome', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_black_ink-state] +# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Black ink', - 'marker_high_level': 100, - 'marker_low_level': 10, - 'marker_type': 'ink-cartridge', - 'state_class': , - 'unit_of_measurement': '%', + 'friendly_name': 'Test HA-1000 Series Monochrome impressions completed', + 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_black_ink', + 'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '58', + 'state': '1500', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-entry] +# name: test_sensors[sensor.test_ha_1000_series_pages_completed-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -456,8 +466,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_pages_completed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -465,40 +475,36 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Cyan ink', + 'object_id_base': 'Pages completed', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Cyan ink', + 'original_name': 'Pages completed', 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'marker', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_1', - 'unit_of_measurement': '%', + 'translation_key': 'pages_completed', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_pages_completed', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-state] +# name: test_sensors[sensor.test_ha_1000_series_pages_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Cyan ink', - 'marker_high_level': 100, - 'marker_low_level': 10, - 'marker_type': 'ink-cartridge', - 'state_class': , - 'unit_of_measurement': '%', + 'friendly_name': 'Test HA-1000 Series Pages completed', + 'state_class': , }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', + 'entity_id': 'sensor.test_ha_1000_series_pages_completed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '91', + 'state': '1234', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-entry] +# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -514,7 +520,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', + 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -522,25 +528,25 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Magenta ink', + 'object_id_base': 'Photo black ink', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Magenta ink', + 'original_name': 'Photo black ink', 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'marker', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_2', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_3', 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-state] +# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Magenta ink', + 'friendly_name': 'Test HA-1000 Series Photo black ink', 'marker_high_level': 100, 'marker_low_level': 10, 'marker_type': 'ink-cartridge', @@ -548,30 +554,28 @@ 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', + 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '73', + 'state': '98', }) # --- -# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-entry] +# name: test_sensors[sensor.test_ha_1000_series_uptime-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), + 'capabilities': None, 'config_entry_id': , 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', + 'entity_category': , + 'entity_id': 'sensor.test_ha_1000_series_uptime', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -579,37 +583,33 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Photo black ink', + 'object_id_base': 'Uptime', 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Photo black ink', + 'original_name': 'Uptime', 'platform': 'ipp', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'marker', - 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_3', - 'unit_of_measurement': '%', + 'translation_key': 'uptime', + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_uptime', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-state] +# name: test_sensors[sensor.test_ha_1000_series_uptime-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Photo black ink', - 'marker_high_level': 100, - 'marker_low_level': 10, - 'marker_type': 'ink-cartridge', - 'state_class': , - 'unit_of_measurement': '%', + 'device_class': 'timestamp', + 'friendly_name': 'Test HA-1000 Series Uptime', }), 'context': , - 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', + 'entity_id': 'sensor.test_ha_1000_series_uptime', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '98', + 'state': '2019-11-11T09:10:02+00:00', }) # --- # name: test_sensors[sensor.test_ha_1000_series_yellow_ink-entry] From 36c9bf75df4f87614e4ff8d7e6b7adf9e7770ee9 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 16 May 2026 09:24:01 -0400 Subject: [PATCH 07/23] Return IPPData dataclass from coordinator Refactor _async_update_data to return an IPPData dataclass containing the printer and page_counts, instead of storing page_counts as a separate coordinator attribute. Co-Authored-By: Claude Opus 4.7 (1M context) --- homeassistant/components/ipp/coordinator.py | 26 +++++++++++++------ homeassistant/components/ipp/diagnostics.py | 3 ++- homeassistant/components/ipp/entity.py | 13 +++++----- homeassistant/components/ipp/sensor.py | 12 +++++---- .../ipp/snapshots/test_diagnostics.ambr | 7 +++++ 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 6fc1c17f709dd3..5bd2c0bc1c4e97 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -1,5 +1,6 @@ """Coordinator for The Internet Printing Protocol (IPP) integration.""" +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, override @@ -29,16 +30,22 @@ 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 - page_counts: dict[str, int] def __init__(self, hass: HomeAssistant, config_entry: IPPConfigEntry) -> None: """Initialize global IPP data updater.""" self.device_id = config_entry.unique_id or config_entry.entry_id - self.page_counts = {} self.ipp = IPP( host=config_entry.data[CONF_HOST], port=config_entry.data[CONF_PORT], @@ -57,18 +64,21 @@ 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: printer = await self.ipp.printer() except IPPError as error: raise UpdateFailed(f"Invalid response from API: {error}") from error - self.page_counts = await self._async_fetch_page_counts() + previous_page_counts = self.data.page_counts if self.data else {} + page_counts = await self._async_fetch_page_counts(previous_page_counts) - return printer + return IPPData(printer=printer, page_counts=page_counts) - async def _async_fetch_page_counts(self) -> dict[str, int]: + 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( @@ -83,7 +93,7 @@ async def _async_fetch_page_counts(self) -> dict[str, int]: _LOGGER.debug( "Failed to fetch page count attributes from printer", exc_info=True ) - return self.page_counts + return previous_page_counts page_counts: dict[str, int] = {} parsed: dict[str, Any] = next(iter(response.get("printers") or []), {}) 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/sensor.py b/homeassistant/components/ipp/sensor.py index 0083b02984563f..65d4f0bcd1492d 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -142,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, @@ -171,7 +171,7 @@ async def async_setup_entry( sensors.extend( IPPPageCountSensor(coordinator, description) for description in PAGE_COUNT_SENSORS - if description.ipp_attribute in coordinator.page_counts + if description.ipp_attribute in coordinator.data.page_counts ) async_add_entities(sensors, True) @@ -186,13 +186,13 @@ 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): @@ -211,4 +211,6 @@ def __init__( @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.coordinator.page_counts.get(self.entity_description.ipp_attribute) + return self.coordinator.data.page_counts.get( + self.entity_description.ipp_attribute + ) 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, + }), }) # --- From 1d1191a69b1bd56a0f1cbb82d3802a3fa669b38c Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 16 May 2026 09:24:25 -0400 Subject: [PATCH 08/23] Add translatable units of measurement for page count sensors Use pages/impressions/sheets as translatable units in strings.json so the sensor unit is displayed and can be localized. Co-Authored-By: Claude Opus 4.7 (1M context) --- homeassistant/components/ipp/strings.json | 15 ++++++++++----- tests/components/ipp/snapshots/test_sensor.ambr | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index 4eb787006ed17e..f94fd835d2fd04 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -35,19 +35,24 @@ "entity": { "sensor": { "impressions_completed": { - "name": "Impressions completed" + "name": "Impressions completed", + "unit_of_measurement": "impressions" }, "impressions_completed_full_color": { - "name": "Color impressions completed" + "name": "Color impressions completed", + "unit_of_measurement": "[%key:component::ipp::entity::sensor::impressions_completed::unit_of_measurement%]" }, "impressions_completed_monochrome": { - "name": "Monochrome impressions completed" + "name": "Monochrome impressions completed", + "unit_of_measurement": "[%key:component::ipp::entity::sensor::impressions_completed::unit_of_measurement%]" }, "media_sheets_completed": { - "name": "Media sheets completed" + "name": "Media sheets completed", + "unit_of_measurement": "sheets" }, "pages_completed": { - "name": "Pages completed" + "name": "Pages completed", + "unit_of_measurement": "pages" }, "printer": { "state": { diff --git a/tests/components/ipp/snapshots/test_sensor.ambr b/tests/components/ipp/snapshots/test_sensor.ambr index 7f4f1ee2b20bbb..5e47d1e575a761 100644 --- a/tests/components/ipp/snapshots/test_sensor.ambr +++ b/tests/components/ipp/snapshots/test_sensor.ambr @@ -161,7 +161,7 @@ 'supported_features': 0, 'translation_key': 'impressions_completed_full_color', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_full_color', - 'unit_of_measurement': None, + 'unit_of_measurement': 'impressions', }) # --- # name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-state] @@ -169,6 +169,7 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test HA-1000 Series Color impressions completed', 'state_class': , + 'unit_of_measurement': 'impressions', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed', @@ -271,7 +272,7 @@ 'supported_features': 0, 'translation_key': 'impressions_completed', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed', - 'unit_of_measurement': None, + 'unit_of_measurement': 'impressions', }) # --- # name: test_sensors[sensor.test_ha_1000_series_impressions_completed-state] @@ -279,6 +280,7 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test HA-1000 Series Impressions completed', 'state_class': , + 'unit_of_measurement': 'impressions', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_impressions_completed', @@ -381,7 +383,7 @@ 'supported_features': 0, 'translation_key': 'media_sheets_completed', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_media_sheets_completed', - 'unit_of_measurement': None, + 'unit_of_measurement': 'sheets', }) # --- # name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-state] @@ -389,6 +391,7 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test HA-1000 Series Media sheets completed', 'state_class': , + 'unit_of_measurement': 'sheets', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed', @@ -434,7 +437,7 @@ 'supported_features': 0, 'translation_key': 'impressions_completed_monochrome', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_monochrome', - 'unit_of_measurement': None, + 'unit_of_measurement': 'impressions', }) # --- # name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-state] @@ -442,6 +445,7 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test HA-1000 Series Monochrome impressions completed', 'state_class': , + 'unit_of_measurement': 'impressions', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed', @@ -487,7 +491,7 @@ 'supported_features': 0, 'translation_key': 'pages_completed', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_pages_completed', - 'unit_of_measurement': None, + 'unit_of_measurement': 'pages', }) # --- # name: test_sensors[sensor.test_ha_1000_series_pages_completed-state] @@ -495,6 +499,7 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test HA-1000 Series Pages completed', 'state_class': , + 'unit_of_measurement': 'pages', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_pages_completed', From 0445a110d3ab38fd918aaaa355ea06ce334efb86 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 16 May 2026 10:15:52 -0400 Subject: [PATCH 09/23] Make PAGE_COUNT_ATTRIBUTES a tuple Tuples better convey constant intent and prevent accidental mutation. Co-Authored-By: Claude Opus 4.7 (1M context) --- homeassistant/components/ipp/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 5bd2c0bc1c4e97..bc1c1801c0ca18 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -18,12 +18,12 @@ SCAN_INTERVAL = timedelta(seconds=60) -PAGE_COUNT_ATTRIBUTES = [ +PAGE_COUNT_ATTRIBUTES = ( "printer-impressions-completed", "printer-pages-completed", "printer-media-sheets-completed", "printer-impressions-completed-col", -] +) _LOGGER = logging.getLogger(__name__) From 82e04fa594e721ef340dc29cf90a4b4ba1fae365 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 16 May 2026 10:29:39 -0400 Subject: [PATCH 10/23] Test page count sensors keep last value when fetch fails Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/components/ipp/test_sensor.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index b07a1fbeac928d..5d1f0b59a8b95f 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -2,9 +2,11 @@ from unittest.mock import AsyncMock, MagicMock +from pyipp import IPPError import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.ipp.coordinator import IPPConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -85,3 +87,23 @@ async def test_no_page_count_sensors_when_unsupported( assert not entity_registry.async_get_entity_id( "sensor", "ipp", f"{unique_id}_{key}" ) + + +async def test_page_counts_retained_on_fetch_failure( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_ipp: MagicMock, +) -> None: + """Test page count sensors keep previous values when fetch fails.""" + assert hass.states.get("sensor.test_ha_1000_series_pages_completed").state == "1234" + + mock_ipp.execute.side_effect = IPPError("boom") + entry: IPPConfigEntry = init_integration + await entry.runtime_data.async_refresh() + await hass.async_block_till_done() + + assert hass.states.get("sensor.test_ha_1000_series_pages_completed").state == "1234" + assert ( + hass.states.get("sensor.test_ha_1000_series_impressions_completed").state + == "2468" + ) From 7b1974046251392628c67b6e023e2204b35dbab8 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 16 May 2026 12:16:57 -0400 Subject: [PATCH 11/23] Address review: merge partial page count responses - Start page count fetch from previous values so a partial response (printer returning a subset of attributes) doesn't drop known keys - Rename test variable entry -> entity_id for clarity Co-Authored-By: Claude Opus 4.7 (1M context) --- homeassistant/components/ipp/coordinator.py | 4 +++- tests/components/ipp/test_sensor.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index bc1c1801c0ca18..ccf334f7059e9d 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -95,7 +95,9 @@ async def _async_fetch_page_counts( ) return previous_page_counts - page_counts: dict[str, int] = {} + # Start from previous values so a partial response (e.g. printer + # returning a subset of attributes) doesn't drop known keys + page_counts: dict[str, int] = dict(previous_page_counts) parsed: dict[str, Any] = next(iter(response.get("printers") or []), {}) for attr in PAGE_COUNT_ATTRIBUTES: if attr not in parsed: diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 5d1f0b59a8b95f..c48f5a33ffebe6 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -31,15 +31,15 @@ async def test_disabled_by_default_sensors( init_integration: MockConfigEntry, ) -> None: """Test the disabled by default IPP sensors.""" - entry = entity_registry.async_get_entity_id( + entity_id = entity_registry.async_get_entity_id( "sensor", "ipp", f"{init_integration.unique_id}_uptime" ) - assert entry is not None + assert entity_id is not None - state = hass.states.get(entry) + state = hass.states.get(entity_id) assert state is None - entity_entry = entity_registry.async_get(entry) + entity_entry = entity_registry.async_get(entity_id) assert entity_entry assert entity_entry.disabled assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION From effc3b2f08444a0a801ea9c96da5010091bb0149 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Sat, 16 May 2026 12:30:24 -0400 Subject: [PATCH 12/23] Use coordinator directly in IPPError test Avoids mistyping init_integration as IPPConfigEntry when it is actually a MockConfigEntry. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/components/ipp/test_sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index c48f5a33ffebe6..c48d0d6aaf4326 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -6,7 +6,6 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.ipp.coordinator import IPPConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -98,8 +97,7 @@ async def test_page_counts_retained_on_fetch_failure( assert hass.states.get("sensor.test_ha_1000_series_pages_completed").state == "1234" mock_ipp.execute.side_effect = IPPError("boom") - entry: IPPConfigEntry = init_integration - await entry.runtime_data.async_refresh() + 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 == "1234" From 728ccdef17378d6d70ca5ac6dc548f02432a277a Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Mon, 18 May 2026 14:01:15 -0400 Subject: [PATCH 13/23] Split page count attribute parsing by expected shape Separate int-typed attributes from the collection attribute (printer-impressions-completed-col) so each is parsed explicitly rather than using a generic isinstance dispatch. Co-Authored-By: Claude Opus 4.7 (1M context) --- homeassistant/components/ipp/coordinator.py | 36 +++++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index ccf334f7059e9d..259e859db21376 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -18,11 +18,19 @@ SCAN_INTERVAL = timedelta(seconds=60) -PAGE_COUNT_ATTRIBUTES = ( +# Integer page-count attributes returned by Get-Printer-Attributes +PAGE_COUNT_INT_ATTRIBUTES = ( "printer-impressions-completed", "printer-pages-completed", "printer-media-sheets-completed", - "printer-impressions-completed-col", +) + +# 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__) @@ -85,7 +93,7 @@ async def _async_fetch_page_counts( IppOperation.GET_PRINTER_ATTRIBUTES, { "operation-attributes-tag": { - "requested-attributes": PAGE_COUNT_ATTRIBUTES, + "requested-attributes": REQUESTED_PAGE_COUNT_ATTRIBUTES, }, }, ) @@ -99,18 +107,18 @@ async def _async_fetch_page_counts( # returning a subset of attributes) doesn't drop known keys page_counts: dict[str, int] = dict(previous_page_counts) parsed: dict[str, Any] = next(iter(response.get("printers") or []), {}) - for attr in PAGE_COUNT_ATTRIBUTES: - if attr not in parsed: - continue - value = parsed[attr] + + for attr in PAGE_COUNT_INT_ATTRIBUTES: + value = parsed.get(attr) if isinstance(value, int): page_counts[attr] = value - elif isinstance(value, dict): - # Handle collection attributes like - # printer-impressions-completed-col - # which contain sub-keys like "monochrome" and "full-color" - for sub_key, sub_value in value.items(): - if isinstance(sub_value, int): - page_counts[f"{attr}/{sub_key}"] = sub_value + + for attr in PAGE_COUNT_COLLECTION_ATTRIBUTES: + collection = parsed.get(attr) + if not isinstance(collection, dict): + continue + for sub_key, sub_value in collection.items(): + if isinstance(sub_value, int): + page_counts[f"{attr}/{sub_key}"] = sub_value return page_counts From c5a16ca1db1055d53e61c6b9db1a29b18a18ddeb Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 28 May 2026 05:57:22 -0400 Subject: [PATCH 14/23] Simplify page count attribute parsing - remove defensive isinstance checks --- homeassistant/components/ipp/coordinator.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 259e859db21376..db7b7d49eb54cd 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -109,16 +109,13 @@ async def _async_fetch_page_counts( parsed: dict[str, Any] = next(iter(response.get("printers") or []), {}) for attr in PAGE_COUNT_INT_ATTRIBUTES: - value = parsed.get(attr) - if isinstance(value, int): + if (value := parsed.get(attr)) is not None: page_counts[attr] = value for attr in PAGE_COUNT_COLLECTION_ATTRIBUTES: - collection = parsed.get(attr) - if not isinstance(collection, dict): - continue - for sub_key, sub_value in collection.items(): - if isinstance(sub_value, int): - page_counts[f"{attr}/{sub_key}"] = sub_value + if isinstance(collection := parsed.get(attr), dict): + for sub_key, sub_value in collection.items(): + if sub_value is not None: + page_counts[f"{attr}/{sub_key}"] = sub_value return page_counts From ac8c031dd765173ede58cda32089217d045023a1 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 28 May 2026 06:06:29 -0400 Subject: [PATCH 15/23] Document why page counts require a separate IPP request - will be consolidated when pyipp #715 is available --- homeassistant/components/ipp/coordinator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index db7b7d49eb54cd..2f6b1fdd354bb1 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -79,6 +79,10 @@ async def _async_update_data(self) -> IPPData: 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) From b0cfde1f5f5fe69ca5aec3a1f9bbfb9406b96c8e Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 28 May 2026 06:15:05 -0400 Subject: [PATCH 16/23] Create page count entities unconditionally - show unknown state if unsupported --- homeassistant/components/ipp/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 65d4f0bcd1492d..0ce94924d01b23 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -171,7 +171,6 @@ 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) From 27c52689d17b2f64a32ddf94c5217bd9a2c2db87 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 28 May 2026 06:21:58 -0400 Subject: [PATCH 17/23] Fix entity creation and exception handling - Restore conditional entity creation based on page_counts (fixes broken test) - Broaden exception handling to catch all errors from execute, not just IPPError, to prevent coordinator crashes --- homeassistant/components/ipp/coordinator.py | 2 +- homeassistant/components/ipp/sensor.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 2f6b1fdd354bb1..81ee6990a92142 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -101,7 +101,7 @@ async def _async_fetch_page_counts( }, }, ) - except IPPError: + except Exception: _LOGGER.debug( "Failed to fetch page count attributes from printer", exc_info=True ) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 0ce94924d01b23..65d4f0bcd1492d 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -171,6 +171,7 @@ 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) From 7d611da3775c00dc96f39880afd988677c824ff9 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 28 May 2026 06:25:10 -0400 Subject: [PATCH 18/23] Fix stale values, remove unnecessary init, add test coverage - Don't retain previous page counts on successful responses (fixes stale value persistence) - Remove unnecessary __init__ that just forwards to super - Parametrize unsupported printer test to cover empty list and missing printers key cases --- homeassistant/components/ipp/coordinator.py | 4 +--- homeassistant/components/ipp/sensor.py | 8 -------- tests/components/ipp/test_sensor.py | 12 +++++++++++- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 81ee6990a92142..f00f36c0f5c459 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -107,10 +107,8 @@ async def _async_fetch_page_counts( ) return previous_page_counts - # Start from previous values so a partial response (e.g. printer - # returning a subset of attributes) doesn't drop known keys - page_counts: dict[str, int] = dict(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: diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 65d4f0bcd1492d..6f68c151e585d0 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -200,14 +200,6 @@ class IPPPageCountSensor(IPPEntity, SensorEntity): entity_description: IPPPageCountSensorEntityDescription - def __init__( - self, - coordinator: IPPDataUpdateCoordinator, - description: IPPPageCountSensorEntityDescription, - ) -> None: - """Initialize the page count sensor.""" - super().__init__(coordinator, description) - @property def native_value(self) -> StateType: """Return the state of the sensor.""" diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index c48d0d6aaf4326..0c4c8b727931b4 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the IPP sensor platform.""" +from typing import Any from unittest.mock import AsyncMock, MagicMock from pyipp import IPPError @@ -62,14 +63,23 @@ async def test_missing_entry_unique_id( 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 = {"printers": [{}]} + 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) From efd26dafebbc025fd5e6765e34a686c272fc6a95 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 28 May 2026 06:34:55 -0400 Subject: [PATCH 19/23] Fix prek BLE001 - catch specific exceptions instead of broad Exception --- homeassistant/components/ipp/coordinator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index f00f36c0f5c459..c74dfc0073b891 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -1,11 +1,12 @@ """Coordinator for The Internet Printing Protocol (IPP) integration.""" +import asyncio from dataclasses import dataclass from datetime import timedelta import logging from typing import Any, override -from pyipp import IPP, IPPError, Printer as IPPPrinter +from pyipp import IPP, IPPConnectionError, IPPError, Printer as IPPPrinter from pyipp.enums import IppOperation from homeassistant.config_entries import ConfigEntry @@ -101,7 +102,7 @@ async def _async_fetch_page_counts( }, }, ) - except Exception: + except (IPPError, IPPConnectionError, asyncio.TimeoutError): _LOGGER.debug( "Failed to fetch page count attributes from printer", exc_info=True ) From a92c6a5036efe23031c839e75797f67e46b4d91e Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 28 May 2026 06:46:40 -0400 Subject: [PATCH 20/23] Expand exception handling to catch IPPResponseError in addition to other IPP exceptions --- homeassistant/components/ipp/coordinator.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index c74dfc0073b891..b11532e044f70e 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -6,7 +6,13 @@ import logging from typing import Any, override -from pyipp import IPP, IPPConnectionError, IPPError, Printer as IPPPrinter +from pyipp import ( + IPP, + IPPConnectionError, + IPPError, + IPPResponseError, + Printer as IPPPrinter, +) from pyipp.enums import IppOperation from homeassistant.config_entries import ConfigEntry @@ -102,7 +108,12 @@ async def _async_fetch_page_counts( }, }, ) - except (IPPError, IPPConnectionError, asyncio.TimeoutError): + except ( + IPPError, + IPPConnectionError, + IPPResponseError, + asyncio.TimeoutError, + ): _LOGGER.debug( "Failed to fetch page count attributes from printer", exc_info=True ) From ecbbee99a081001f0e699e95a0e9d626bc3d7878 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 28 May 2026 07:49:53 -0400 Subject: [PATCH 21/23] Fix prek formatting: use Python 3.14 except syntax, remove unused imports --- homeassistant/components/ipp/coordinator.py | 8 +------- homeassistant/components/ipp/sensor.py | 2 +- tests/components/ipp/test_sensor.py | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index b11532e044f70e..d54fd98f6489eb 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -1,6 +1,5 @@ """Coordinator for The Internet Printing Protocol (IPP) integration.""" -import asyncio from dataclasses import dataclass from datetime import timedelta import logging @@ -108,12 +107,7 @@ async def _async_fetch_page_counts( }, }, ) - except ( - IPPError, - IPPConnectionError, - IPPResponseError, - asyncio.TimeoutError, - ): + except IPPError, IPPConnectionError, IPPResponseError, TimeoutError: _LOGGER.debug( "Failed to fetch page count attributes from printer", exc_info=True ) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 6f68c151e585d0..1d9c3f54e631d2 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -29,7 +29,7 @@ ATTR_STATE_REASON, ATTR_URI_SUPPORTED, ) -from .coordinator import IPPConfigEntry, IPPDataUpdateCoordinator +from .coordinator import IPPConfigEntry from .entity import IPPEntity diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 0c4c8b727931b4..5d1de4a14ba38e 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -67,8 +67,8 @@ async def test_missing_entry_unique_id( "execute_response", [ {"printers": [{}]}, # Empty printer dict - {"printers": []}, # Empty printers list - {}, # Missing printers key + {"printers": []}, # Empty printers list + {}, # Missing printers key ], ) async def test_no_page_count_sensors_when_unsupported( From bf0ce473b7cf7b1b1451bfef0244b9c76cc56c10 Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 30 Jul 2026 20:30:43 -0400 Subject: [PATCH 22/23] Remove defensive isinstance check and redundant exception subclasses pyipp's parser guarantees collection attributes parse to dicts, so the isinstance check is unnecessary. IPPConnectionError and IPPResponseError are subclasses of IPPError, so catching them separately is redundant. Co-Authored-By: Claude Fable 5 --- homeassistant/components/ipp/coordinator.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index d54fd98f6489eb..c4ac08c7cadacf 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -5,13 +5,7 @@ import logging from typing import Any, override -from pyipp import ( - IPP, - IPPConnectionError, - IPPError, - IPPResponseError, - Printer as IPPPrinter, -) +from pyipp import IPP, IPPError, Printer as IPPPrinter from pyipp.enums import IppOperation from homeassistant.config_entries import ConfigEntry @@ -107,7 +101,7 @@ async def _async_fetch_page_counts( }, }, ) - except IPPError, IPPConnectionError, IPPResponseError, TimeoutError: + except IPPError, TimeoutError: _LOGGER.debug( "Failed to fetch page count attributes from printer", exc_info=True ) @@ -120,10 +114,9 @@ async def _async_fetch_page_counts( 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: - if isinstance(collection := parsed.get(attr), dict): - for sub_key, sub_value in collection.items(): - if sub_value is not None: - page_counts[f"{attr}/{sub_key}"] = sub_value + for sub_key, sub_value in parsed.get(attr, {}).items(): + page_counts[f"{attr}/{sub_key}"] = sub_value return page_counts From de099b779db47896befcf7d6713f043df3519a7e Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Fri, 31 Jul 2026 19:28:32 -0400 Subject: [PATCH 23/23] Update snapshots for new format, add override decorator, cover empty page count response in tests Co-Authored-By: Claude Fable 5 --- homeassistant/components/ipp/sensor.py | 1 + .../components/ipp/snapshots/test_sensor.ambr | 92 +++++++++---------- tests/components/ipp/test_sensor.py | 27 ++++-- 3 files changed, 68 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 1d9c3f54e631d2..77cd2cf218ee92 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -201,6 +201,7 @@ class IPPPageCountSensor(IPPEntity, SensorEntity): entity_description: IPPPageCountSensorEntityDescription @property + @override def native_value(self) -> StateType: """Return the state of the sensor.""" return self.coordinator.data.page_counts.get( diff --git a/tests/components/ipp/snapshots/test_sensor.ambr b/tests/components/ipp/snapshots/test_sensor.ambr index 5e47d1e575a761..454d9fb0f04077 100644 --- a/tests/components/ipp/snapshots/test_sensor.ambr +++ b/tests/components/ipp/snapshots/test_sensor.ambr @@ -6,7 +6,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'options': list([ + : list([ 'idle', 'printing', 'stopped', @@ -46,11 +46,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF', - 'device_class': 'enum', - 'friendly_name': 'Test HA-1000 Series', + : 'enum', + : 'Test HA-1000 Series', 'info': 'Test HA-1000 Series', 'location': None, - 'options': list([ + : list([ 'idle', 'printing', 'stopped', @@ -75,7 +75,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -110,12 +110,12 @@ # name: test_sensors[sensor.test_ha_1000_series_black_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Black ink', + : 'Test HA-1000 Series Black ink', 'marker_high_level': 100, 'marker_low_level': 10, 'marker_type': 'ink-cartridge', - 'state_class': , - 'unit_of_measurement': '%', + : , + : '%', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_black_ink', @@ -132,7 +132,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -167,9 +167,9 @@ # name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Color impressions completed', - 'state_class': , - 'unit_of_measurement': 'impressions', + : 'Test HA-1000 Series Color impressions completed', + : , + : 'impressions', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed', @@ -186,7 +186,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -221,12 +221,12 @@ # name: test_sensors[sensor.test_ha_1000_series_cyan_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Cyan ink', + : 'Test HA-1000 Series Cyan ink', 'marker_high_level': 100, 'marker_low_level': 10, 'marker_type': 'ink-cartridge', - 'state_class': , - 'unit_of_measurement': '%', + : , + : '%', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_cyan_ink', @@ -243,7 +243,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -278,9 +278,9 @@ # name: test_sensors[sensor.test_ha_1000_series_impressions_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Impressions completed', - 'state_class': , - 'unit_of_measurement': 'impressions', + : 'Test HA-1000 Series Impressions completed', + : , + : 'impressions', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_impressions_completed', @@ -297,7 +297,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -332,12 +332,12 @@ # name: test_sensors[sensor.test_ha_1000_series_magenta_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Magenta ink', + : 'Test HA-1000 Series Magenta ink', 'marker_high_level': 100, 'marker_low_level': 10, 'marker_type': 'ink-cartridge', - 'state_class': , - 'unit_of_measurement': '%', + : , + : '%', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_magenta_ink', @@ -354,7 +354,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -389,9 +389,9 @@ # name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Media sheets completed', - 'state_class': , - 'unit_of_measurement': 'sheets', + : 'Test HA-1000 Series Media sheets completed', + : , + : 'sheets', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed', @@ -408,7 +408,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -443,9 +443,9 @@ # name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Monochrome impressions completed', - 'state_class': , - 'unit_of_measurement': 'impressions', + : 'Test HA-1000 Series Monochrome impressions completed', + : , + : 'impressions', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed', @@ -462,7 +462,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -497,9 +497,9 @@ # name: test_sensors[sensor.test_ha_1000_series_pages_completed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Pages completed', - 'state_class': , - 'unit_of_measurement': 'pages', + : 'Test HA-1000 Series Pages completed', + : , + : 'pages', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_pages_completed', @@ -516,7 +516,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -551,12 +551,12 @@ # name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Photo black ink', + : 'Test HA-1000 Series Photo black ink', 'marker_high_level': 100, 'marker_low_level': 10, 'marker_type': 'ink-cartridge', - 'state_class': , - 'unit_of_measurement': '%', + : , + : '%', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_photo_black_ink', @@ -606,8 +606,8 @@ # name: test_sensors[sensor.test_ha_1000_series_uptime-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'timestamp', - 'friendly_name': 'Test HA-1000 Series Uptime', + : 'timestamp', + : 'Test HA-1000 Series Uptime', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_uptime', @@ -624,7 +624,7 @@ ]), 'area_id': None, 'capabilities': dict({ - 'state_class': , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -659,12 +659,12 @@ # name: test_sensors[sensor.test_ha_1000_series_yellow_ink-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test HA-1000 Series Yellow ink', + : 'Test HA-1000 Series Yellow ink', 'marker_high_level': 100, 'marker_low_level': 10, 'marker_type': 'ink-cartridge', - 'state_class': , - 'unit_of_measurement': '%', + : , + : '%', }), 'context': , 'entity_id': 'sensor.test_ha_1000_series_yellow_ink', diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 5d1de4a14ba38e..7915d783b9c945 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -7,6 +7,7 @@ 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 @@ -98,20 +99,34 @@ async def test_no_page_count_sensors_when_unsupported( ) -async def test_page_counts_retained_on_fetch_failure( +@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 sensors keep previous values when fetch fails.""" + """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 = IPPError("boom") + 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 == "1234" assert ( - hass.states.get("sensor.test_ha_1000_series_impressions_completed").state - == "2468" + hass.states.get("sensor.test_ha_1000_series_pages_completed").state + == expected_state )