Add page count sensors to IPP integration (#165570)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Brian Egge
2026-09-16 16:15:39 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 3ef4d1c750
commit 59e386fbf2
7 changed files with 497 additions and 11 deletions
+15
View File
@@ -1,9 +1,24 @@
{
"entity": {
"sensor": {
"impressions_completed": {
"default": "mdi:file-document-outline"
},
"impressions_completed_full_color": {
"default": "mdi:file-document-outline"
},
"impressions_completed_monochrome": {
"default": "mdi:file-document-outline"
},
"marker": {
"default": "mdi:water"
},
"media_sheets_completed": {
"default": "mdi:file-document-outline"
},
"pages_completed": {
"default": "mdi:file-document-outline"
},
"printer": {
"default": "mdi:printer"
},
+60
View File
@@ -39,6 +39,7 @@ class IPPSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[Printer], StateType | datetime]
attributes_fn: Callable[[Printer], dict[Any, StateType]] = lambda _: {}
exists_fn: Callable[[Printer], bool] = lambda _: True
def _get_marker_attributes_fn(
@@ -81,6 +82,59 @@ PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = (
),
)
PAGE_COUNT_SENSORS: tuple[IPPSensorEntityDescription, ...] = (
IPPSensorEntityDescription(
key="pages_completed",
translation_key="pages_completed",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
exists_fn=lambda printer: "pages_completed" in printer.counters.supported,
value_fn=lambda printer: printer.counters.pages_completed,
),
IPPSensorEntityDescription(
key="impressions_completed",
translation_key="impressions_completed",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
exists_fn=lambda printer: "impressions_completed" in printer.counters.supported,
value_fn=lambda printer: printer.counters.impressions_completed,
),
IPPSensorEntityDescription(
key="media_sheets_completed",
translation_key="media_sheets_completed",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
exists_fn=lambda printer: (
"media_sheets_completed" in printer.counters.supported
),
value_fn=lambda printer: printer.counters.media_sheets_completed,
),
IPPSensorEntityDescription(
key="impressions_completed_monochrome",
translation_key="impressions_completed_monochrome",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
exists_fn=lambda printer: (
"monochrome" in printer.counters.impressions_completed_col
),
value_fn=lambda printer: printer.counters.impressions_completed_col.get(
"monochrome"
),
),
IPPSensorEntityDescription(
key="impressions_completed_full_color",
translation_key="impressions_completed_full_color",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
exists_fn=lambda printer: (
"full-color" in printer.counters.impressions_completed_col
),
value_fn=lambda printer: printer.counters.impressions_completed_col.get(
"full-color"
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -123,6 +177,12 @@ async def async_setup_entry(
)
)
sensors.extend(
IPPSensor(coordinator, description)
for description in PAGE_COUNT_SENSORS
if description.exists_fn(coordinator.data)
)
async_add_entities(sensors, True)
+20
View File
@@ -34,6 +34,26 @@
},
"entity": {
"sensor": {
"impressions_completed": {
"name": "Impressions completed",
"unit_of_measurement": "impressions"
},
"impressions_completed_full_color": {
"name": "Color impressions completed",
"unit_of_measurement": "[%key:component::ipp::entity::sensor::impressions_completed::unit_of_measurement%]"
},
"impressions_completed_monochrome": {
"name": "Monochrome impressions completed",
"unit_of_measurement": "[%key:component::ipp::entity::sensor::impressions_completed::unit_of_measurement%]"
},
"media_sheets_completed": {
"name": "Media sheets completed",
"unit_of_measurement": "sheets"
},
"pages_completed": {
"name": "Pages completed",
"unit_of_measurement": "pages"
},
"printer": {
"state": {
"idle": "[%key:common::state::idle%]",
+8 -1
View File
@@ -32,5 +32,12 @@
"marker-colors": ["#000000", "#000000", "#00FFFF", "#FFFF00", "#FF00FF"],
"marker-levels": [58, 98, 91, 95, 73],
"marker-low-levels": [10, 10, 10, 10, 10],
"marker-high-levels": [100, 100, 100, 100, 100]
"marker-high-levels": [100, 100, 100, 100, 100],
"printer-impressions-completed": 2468,
"printer-impressions-completed-col": {
"monochrome": 1500,
"full-color": 968
},
"printer-media-sheets-completed": 1234,
"printer-pages-completed": 1234
}
@@ -4,12 +4,17 @@
'data': dict({
'booted_at': '2019-11-11T09:10:02+00:00',
'counters': dict({
'impressions_completed': None,
'impressions_completed': 2468,
'impressions_completed_col': dict({
'full-color': 968,
'monochrome': 1500,
}),
'media_sheets_completed': None,
'pages_completed': None,
'media_sheets_completed': 1234,
'pages_completed': 1234,
'supported': list([
'impressions_completed',
'pages_completed',
'media_sheets_completed',
]),
}),
'info': dict({
@@ -125,6 +125,60 @@
'state': '58',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Color impressions completed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Color impressions completed',
'platform': 'ipp',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'impressions_completed_full_color',
'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_full_color',
'unit_of_measurement': 'impressions',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_color_impressions_completed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test HA-1000 Series Color impressions completed',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'impressions',
}),
'context': <ANY>,
'entity_id': 'sensor.test_ha_1000_series_color_impressions_completed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '968',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_cyan_ink-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -182,6 +236,60 @@
'state': '91',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_ha_1000_series_impressions_completed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Impressions completed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Impressions completed',
'platform': 'ipp',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'impressions_completed',
'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed',
'unit_of_measurement': 'impressions',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_impressions_completed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test HA-1000 Series Impressions completed',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'impressions',
}),
'context': <ANY>,
'entity_id': 'sensor.test_ha_1000_series_impressions_completed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2468',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_magenta_ink-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -239,6 +347,168 @@
'state': '73',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Media sheets completed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Media sheets completed',
'platform': 'ipp',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'media_sheets_completed',
'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_media_sheets_completed',
'unit_of_measurement': 'sheets',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_media_sheets_completed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test HA-1000 Series Media sheets completed',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'sheets',
}),
'context': <ANY>,
'entity_id': 'sensor.test_ha_1000_series_media_sheets_completed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1234',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Monochrome impressions completed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Monochrome impressions completed',
'platform': 'ipp',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'impressions_completed_monochrome',
'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_impressions_completed_monochrome',
'unit_of_measurement': 'impressions',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_monochrome_impressions_completed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test HA-1000 Series Monochrome impressions completed',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'impressions',
}),
'context': <ANY>,
'entity_id': 'sensor.test_ha_1000_series_monochrome_impressions_completed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1500',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_pages_completed-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_ha_1000_series_pages_completed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Pages completed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Pages completed',
'platform': 'ipp',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pages_completed',
'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_pages_completed',
'unit_of_measurement': 'pages',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_pages_completed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test HA-1000 Series Pages completed',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'pages',
}),
'context': <ANY>,
'entity_id': 'sensor.test_ha_1000_series_pages_completed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1234',
})
# ---
# name: test_sensors[sensor.test_ha_1000_series_photo_black_ink-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
+116 -7
View File
@@ -1,14 +1,19 @@
"""Tests for the IPP sensor platform."""
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock
from freezegun.api import FrozenDateTimeFactory
from pyipp import Counters, Printer
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.ipp.coordinator import SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.mark.freeze_time("2019-11-11 09:10:32+00:00")
@@ -29,13 +34,18 @@ async def test_disabled_by_default_sensors(
init_integration: MockConfigEntry,
) -> None:
"""Test the disabled by default IPP sensors."""
state = hass.states.get("sensor.test_ha_1000_series_uptime")
entity_id = entity_registry.async_get_entity_id(
"sensor", "ipp", f"{init_integration.unique_id}_uptime"
)
assert entity_id is not None
state = hass.states.get(entity_id)
assert state is None
entry = entity_registry.async_get("sensor.test_ha_1000_series_uptime")
assert entry
assert entry.disabled
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry
assert entity_entry.disabled
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
async def test_missing_entry_unique_id(
@@ -54,3 +64,102 @@ 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_printer: Printer,
mock_ipp: MagicMock,
) -> None:
"""Test that page count sensors are not created when printer doesn't support them."""
mock_printer.counters = Counters(
impressions_completed=None,
impressions_completed_col={},
pages_completed=None,
media_sheets_completed=None,
supported=(),
)
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 mock_config_entry.state is ConfigEntryState.LOADED
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}"
)
async def test_page_count_sensors_with_partial_counters(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_printer: Printer,
mock_ipp: MagicMock,
) -> None:
"""Test sensors follow which counters the printer supports.
A supported counter with an unknown value gets a sensor in the unknown
state; a counter the printer does not report gets no sensor.
"""
mock_printer.counters = Counters(
impressions_completed=None,
impressions_completed_col={"monochrome": 1500, "full-color": None},
pages_completed=None,
media_sheets_completed=None,
supported=("impressions_completed", "pages_completed"),
)
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 entity_registry.async_get_entity_id(
"sensor", "ipp", f"{mock_config_entry.unique_id}_media_sheets_completed"
)
state = hass.states.get(
"sensor.test_ha_1000_series_monochrome_impressions_completed"
)
assert state
assert state.state == "1500"
for entity_id in (
"sensor.test_ha_1000_series_pages_completed",
"sensor.test_ha_1000_series_impressions_completed",
"sensor.test_ha_1000_series_color_impressions_completed",
):
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNKNOWN
async def test_page_count_unknown_when_counter_missing(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
init_integration: MockConfigEntry,
mock_printer: Printer,
) -> None:
"""Test a page count sensor becomes unknown when the printer stops reporting it."""
assert hass.states.get("sensor.test_ha_1000_series_pages_completed").state == "1234"
mock_printer.counters.pages_completed = None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
hass.states.get("sensor.test_ha_1000_series_pages_completed").state
== STATE_UNKNOWN
)