From 646ddf9c2d6144cf70fb6b67eb8ba593aba67f60 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:17:43 +0200 Subject: [PATCH] Add sensors to ntfy integration (#145262) * Add sensors * small changes * test coverage * changes * update snapshot --- homeassistant/components/ntfy/__init__.py | 11 +- homeassistant/components/ntfy/coordinator.py | 74 ++ homeassistant/components/ntfy/icons.json | 62 + homeassistant/components/ntfy/notify.py | 5 +- homeassistant/components/ntfy/sensor.py | 272 +++++ homeassistant/components/ntfy/strings.json | 82 ++ .../ntfy/snapshots/test_sensor.ambr | 1029 +++++++++++++++++ tests/components/ntfy/test_init.py | 34 + tests/components/ntfy/test_sensor.py | 42 + 9 files changed, 1603 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/ntfy/coordinator.py create mode 100644 homeassistant/components/ntfy/sensor.py create mode 100644 tests/components/ntfy/snapshots/test_sensor.ambr create mode 100644 tests/components/ntfy/test_sensor.py diff --git a/homeassistant/components/ntfy/__init__.py b/homeassistant/components/ntfy/__init__.py index cd9c35ca4e6..72dbb4d2afb 100644 --- a/homeassistant/components/ntfy/__init__.py +++ b/homeassistant/components/ntfy/__init__.py @@ -12,19 +12,16 @@ from aiontfy.exceptions import ( NtfyUnauthorizedAuthenticationError, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +from .coordinator import NtfyConfigEntry, NtfyDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.NOTIFY] - - -type NtfyConfigEntry = ConfigEntry[Ntfy] +PLATFORMS: list[Platform] = [Platform.NOTIFY, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: NtfyConfigEntry) -> bool: @@ -59,7 +56,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: NtfyConfigEntry) -> bool translation_key="timeout_error", ) from e - entry.runtime_data = ntfy + coordinator = NtfyDataUpdateCoordinator(hass, entry, ntfy) + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/ntfy/coordinator.py b/homeassistant/components/ntfy/coordinator.py new file mode 100644 index 00000000000..a52f1b06f41 --- /dev/null +++ b/homeassistant/components/ntfy/coordinator.py @@ -0,0 +1,74 @@ +"""DataUpdateCoordinator for ntfy integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging + +from aiontfy import Account as NtfyAccount, Ntfy +from aiontfy.exceptions import ( + NtfyConnectionError, + NtfyHTTPError, + NtfyTimeoutError, + NtfyUnauthorizedAuthenticationError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +type NtfyConfigEntry = ConfigEntry[NtfyDataUpdateCoordinator] + + +class NtfyDataUpdateCoordinator(DataUpdateCoordinator[NtfyAccount]): + """Ntfy data update coordinator.""" + + config_entry: NtfyConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: NtfyConfigEntry, ntfy: Ntfy + ) -> None: + """Initialize the ntfy data update coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=timedelta(minutes=15), + ) + + self.ntfy = ntfy + + async def _async_update_data(self) -> NtfyAccount: + """Fetch account data from ntfy.""" + + try: + return await self.ntfy.account() + except NtfyUnauthorizedAuthenticationError as e: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="authentication_error", + ) from e + except NtfyHTTPError as e: + _LOGGER.debug("Error %s: %s [%s]", e.code, e.error, e.link) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="server_error", + translation_placeholders={"error_msg": str(e.error)}, + ) from e + except NtfyConnectionError as e: + _LOGGER.debug("Error", exc_info=True) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="connection_error", + ) from e + except NtfyTimeoutError as e: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="timeout_error", + ) from e diff --git a/homeassistant/components/ntfy/icons.json b/homeassistant/components/ntfy/icons.json index 9fe617880af..66489413b5b 100644 --- a/homeassistant/components/ntfy/icons.json +++ b/homeassistant/components/ntfy/icons.json @@ -4,6 +4,68 @@ "publish": { "default": "mdi:console-line" } + }, + "sensor": { + "messages": { + "default": "mdi:message-arrow-right-outline" + }, + "messages_remaining": { + "default": "mdi:message-plus-outline" + }, + "messages_limit": { + "default": "mdi:message-alert-outline" + }, + "messages_expiry_duration": { + "default": "mdi:message-text-clock" + }, + "emails": { + "default": "mdi:email-arrow-right-outline" + }, + "emails_remaining": { + "default": "mdi:email-plus-outline" + }, + "emails_limit": { + "default": "mdi:email-alert-outline" + }, + "calls": { + "default": "mdi:phone-outgoing" + }, + "calls_remaining": { + "default": "mdi:phone-plus" + }, + "calls_limit": { + "default": "mdi:phone-alert" + }, + "reservations": { + "default": "mdi:lock" + }, + "reservations_remaining": { + "default": "mdi:lock-plus" + }, + "reservations_limit": { + "default": "mdi:lock-alert" + }, + "attachment_total_size": { + "default": "mdi:database-arrow-right" + }, + "attachment_total_size_remaining": { + "default": "mdi:database-plus" + }, + "attachment_total_size_limit": { + "default": "mdi:database-alert" + }, + "attachment_expiry_duration": { + "default": "mdi:cloud-clock" + }, + "attachment_file_size": { + "default": "mdi:file-alert" + }, + "attachment_bandwidth": { + "default": "mdi:cloud-upload" + }, + "tier": { + "default": "mdi:star" + } } } } diff --git a/homeassistant/components/ntfy/notify.py b/homeassistant/components/ntfy/notify.py index 7328a1533c2..e10e64caf23 100644 --- a/homeassistant/components/ntfy/notify.py +++ b/homeassistant/components/ntfy/notify.py @@ -22,8 +22,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import NtfyConfigEntry from .const import CONF_TOPIC, DOMAIN +from .coordinator import NtfyConfigEntry PARALLEL_UPDATES = 0 @@ -69,9 +69,10 @@ class NtfyNotifyEntity(NotifyEntity): name=subentry.data.get(CONF_NAME, self.topic), configuration_url=URL(config_entry.data[CONF_URL]) / self.topic, identifiers={(DOMAIN, f"{config_entry.entry_id}_{subentry.subentry_id}")}, + via_device=(DOMAIN, config_entry.entry_id), ) self.config_entry = config_entry - self.ntfy = config_entry.runtime_data + self.ntfy = config_entry.runtime_data.ntfy async def async_send_message(self, message: str, title: str | None = None) -> None: """Publish a message to a topic.""" diff --git a/homeassistant/components/ntfy/sensor.py b/homeassistant/components/ntfy/sensor.py new file mode 100644 index 00000000000..0180d9fce72 --- /dev/null +++ b/homeassistant/components/ntfy/sensor.py @@ -0,0 +1,272 @@ +"""Sensor platform for ntfy integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import StrEnum + +from aiontfy import Account as NtfyAccount +from yarl import URL + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import CONF_URL, EntityCategory, UnitOfInformation, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import NtfyConfigEntry, NtfyDataUpdateCoordinator + +PARALLEL_UPDATES = 0 + + +@dataclass(kw_only=True, frozen=True) +class NtfySensorEntityDescription(SensorEntityDescription): + """Ntfy Sensor Description.""" + + value_fn: Callable[[NtfyAccount], StateType] + + +class NtfySensor(StrEnum): + """Ntfy sensors.""" + + MESSAGES = "messages" + MESSAGES_REMAINING = "messages_remaining" + MESSAGES_LIMIT = "messages_limit" + MESSAGES_EXPIRY_DURATION = "messages_expiry_duration" + EMAILS = "emails" + EMAILS_REMAINING = "emails_remaining" + EMAILS_LIMIT = "emails_limit" + CALLS = "calls" + CALLS_REMAINING = "calls_remaining" + CALLS_LIMIT = "calls_limit" + RESERVATIONS = "reservations" + RESERVATIONS_REMAINING = "reservations_remaining" + RESERVATIONS_LIMIT = "reservations_limit" + ATTACHMENT_TOTAL_SIZE = "attachment_total_size" + ATTACHMENT_TOTAL_SIZE_REMAINING = "attachment_total_size_remaining" + ATTACHMENT_TOTAL_SIZE_LIMIT = "attachment_total_size_limit" + ATTACHMENT_EXPIRY_DURATION = "attachment_expiry_duration" + ATTACHMENT_BANDWIDTH = "attachment_bandwidth" + ATTACHMENT_FILE_SIZE = "attachment_file_size" + TIER = "tier" + + +SENSOR_DESCRIPTIONS: tuple[NtfySensorEntityDescription, ...] = ( + NtfySensorEntityDescription( + key=NtfySensor.MESSAGES, + translation_key=NtfySensor.MESSAGES, + value_fn=lambda account: account.stats.messages, + ), + NtfySensorEntityDescription( + key=NtfySensor.MESSAGES_REMAINING, + translation_key=NtfySensor.MESSAGES_REMAINING, + value_fn=lambda account: account.stats.messages_remaining, + entity_registry_enabled_default=False, + ), + NtfySensorEntityDescription( + key=NtfySensor.MESSAGES_LIMIT, + translation_key=NtfySensor.MESSAGES_LIMIT, + value_fn=lambda account: account.limits.messages if account.limits else None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + NtfySensorEntityDescription( + key=NtfySensor.MESSAGES_EXPIRY_DURATION, + translation_key=NtfySensor.MESSAGES_EXPIRY_DURATION, + value_fn=( + lambda account: account.limits.messages_expiry_duration + if account.limits + else None + ), + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, + ), + NtfySensorEntityDescription( + key=NtfySensor.EMAILS, + translation_key=NtfySensor.EMAILS, + value_fn=lambda account: account.stats.emails, + ), + NtfySensorEntityDescription( + key=NtfySensor.EMAILS_REMAINING, + translation_key=NtfySensor.EMAILS_REMAINING, + value_fn=lambda account: account.stats.emails_remaining, + entity_registry_enabled_default=False, + ), + NtfySensorEntityDescription( + key=NtfySensor.EMAILS_LIMIT, + translation_key=NtfySensor.EMAILS_LIMIT, + value_fn=lambda account: account.limits.emails if account.limits else None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + NtfySensorEntityDescription( + key=NtfySensor.CALLS, + translation_key=NtfySensor.CALLS, + value_fn=lambda account: account.stats.calls, + ), + NtfySensorEntityDescription( + key=NtfySensor.CALLS_REMAINING, + translation_key=NtfySensor.CALLS_REMAINING, + value_fn=lambda account: account.stats.calls_remaining, + entity_registry_enabled_default=False, + ), + NtfySensorEntityDescription( + key=NtfySensor.CALLS_LIMIT, + translation_key=NtfySensor.CALLS_LIMIT, + value_fn=lambda account: account.limits.calls if account.limits else None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + NtfySensorEntityDescription( + key=NtfySensor.RESERVATIONS, + translation_key=NtfySensor.RESERVATIONS, + value_fn=lambda account: account.stats.reservations, + ), + NtfySensorEntityDescription( + key=NtfySensor.RESERVATIONS_REMAINING, + translation_key=NtfySensor.RESERVATIONS_REMAINING, + value_fn=lambda account: account.stats.reservations_remaining, + entity_registry_enabled_default=False, + ), + NtfySensorEntityDescription( + key=NtfySensor.RESERVATIONS_LIMIT, + translation_key=NtfySensor.RESERVATIONS_LIMIT, + value_fn=( + lambda account: account.limits.reservations if account.limits else None + ), + entity_category=EntityCategory.DIAGNOSTIC, + ), + NtfySensorEntityDescription( + key=NtfySensor.ATTACHMENT_EXPIRY_DURATION, + translation_key=NtfySensor.ATTACHMENT_EXPIRY_DURATION, + value_fn=( + lambda account: account.limits.attachment_expiry_duration + if account.limits + else None + ), + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, + ), + NtfySensorEntityDescription( + key=NtfySensor.ATTACHMENT_TOTAL_SIZE, + translation_key=NtfySensor.ATTACHMENT_TOTAL_SIZE, + value_fn=lambda account: account.stats.attachment_total_size, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, + suggested_display_precision=0, + ), + NtfySensorEntityDescription( + key=NtfySensor.ATTACHMENT_TOTAL_SIZE_REMAINING, + translation_key=NtfySensor.ATTACHMENT_TOTAL_SIZE_REMAINING, + value_fn=lambda account: account.stats.attachment_total_size_remaining, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, + suggested_display_precision=0, + entity_registry_enabled_default=False, + ), + NtfySensorEntityDescription( + key=NtfySensor.ATTACHMENT_TOTAL_SIZE_LIMIT, + translation_key=NtfySensor.ATTACHMENT_TOTAL_SIZE_LIMIT, + value_fn=( + lambda account: account.limits.attachment_total_size + if account.limits + else None + ), + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, + suggested_display_precision=0, + ), + NtfySensorEntityDescription( + key=NtfySensor.ATTACHMENT_FILE_SIZE, + translation_key=NtfySensor.ATTACHMENT_FILE_SIZE, + value_fn=( + lambda account: account.limits.attachment_file_size + if account.limits + else None + ), + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, + suggested_display_precision=0, + ), + NtfySensorEntityDescription( + key=NtfySensor.ATTACHMENT_BANDWIDTH, + translation_key=NtfySensor.ATTACHMENT_BANDWIDTH, + value_fn=( + lambda account: account.limits.attachment_bandwidth + if account.limits + else None + ), + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, + suggested_display_precision=0, + ), + NtfySensorEntityDescription( + key=NtfySensor.TIER, + translation_key=NtfySensor.TIER, + value_fn=lambda account: account.tier.name if account.tier else "free", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: NtfyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + coordinator = config_entry.runtime_data + async_add_entities( + NtfySensorEntity(coordinator, description) + for description in SENSOR_DESCRIPTIONS + ) + + +class NtfySensorEntity(CoordinatorEntity[NtfyDataUpdateCoordinator], SensorEntity): + """Representation of a ntfy sensor entity.""" + + entity_description: NtfySensorEntityDescription + coordinator: NtfyDataUpdateCoordinator + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: NtfyDataUpdateCoordinator, + description: NtfySensorEntityDescription, + ) -> None: + """Initialize a sensor entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer="ntfy LLC", + model="ntfy", + configuration_url=URL(coordinator.config_entry.data[CONF_URL]) / "app", + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + ) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/ntfy/strings.json b/homeassistant/components/ntfy/strings.json index cef662d6f2f..08a0a20a30a 100644 --- a/homeassistant/components/ntfy/strings.json +++ b/homeassistant/components/ntfy/strings.json @@ -120,6 +120,88 @@ } } }, + "entity": { + "sensor": { + "messages": { + "name": "Messages published", + "unit_of_measurement": "messages" + }, + "messages_remaining": { + "name": "Messages remaining", + "unit_of_measurement": "[%key:component::ntfy::entity::sensor::messages::unit_of_measurement%]" + }, + "messages_limit": { + "name": "Messages usage limit", + "unit_of_measurement": "[%key:component::ntfy::entity::sensor::messages::unit_of_measurement%]" + }, + "messages_expiry_duration": { + "name": "Messages expiry duration" + }, + "emails": { + "name": "Emails sent", + "unit_of_measurement": "emails" + }, + "emails_remaining": { + "name": "Emails remaining", + "unit_of_measurement": "[%key:component::ntfy::entity::sensor::emails::unit_of_measurement%]" + }, + "emails_limit": { + "name": "Email usage limit", + "unit_of_measurement": "[%key:component::ntfy::entity::sensor::emails::unit_of_measurement%]" + }, + "calls": { + "name": "Phone calls made", + "unit_of_measurement": "calls" + }, + "calls_remaining": { + "name": "Phone calls remaining", + "unit_of_measurement": "[%key:component::ntfy::entity::sensor::calls::unit_of_measurement%]" + }, + "calls_limit": { + "name": "Phone calls usage limit", + "unit_of_measurement": "[%key:component::ntfy::entity::sensor::calls::unit_of_measurement%]" + }, + "reservations": { + "name": "Reserved topics", + "unit_of_measurement": "topics" + }, + "reservations_remaining": { + "name": "Reserved topics remaining", + "unit_of_measurement": "[%key:component::ntfy::entity::sensor::reservations::unit_of_measurement%]" + }, + "reservations_limit": { + "name": "Reserved topics limit", + "unit_of_measurement": "[%key:component::ntfy::entity::sensor::reservations::unit_of_measurement%]" + }, + "attachment_total_size": { + "name": "Attachment storage" + }, + "attachment_total_size_remaining": { + "name": "Attachment storage remaining" + }, + "attachment_total_size_limit": { + "name": "Attachment storage limit" + }, + "attachment_expiry_duration": { + "name": "Attachment expiry duration" + }, + "attachment_file_size": { + "name": "Attachment file size limit" + }, + "attachment_bandwidth": { + "name": "Attachment bandwidth limit" + }, + "tier": { + "name": "Subscription tier", + "state": { + "free": "Free", + "supporter": "Supporter", + "pro": "Pro", + "business": "Business" + } + } + } + }, "exceptions": { "publish_failed_request_error": { "message": "Failed to publish notification: {error_msg}" diff --git a/tests/components/ntfy/snapshots/test_sensor.ambr b/tests/components/ntfy/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..fd0dd3c4bd4 --- /dev/null +++ b/tests/components/ntfy/snapshots/test_sensor.ambr @@ -0,0 +1,1029 @@ +# serializer version: 1 +# name: test_setup[sensor.ntfy_sh_attachment_bandwidth_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_attachment_bandwidth_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Attachment bandwidth limit', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_attachment_bandwidth', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_bandwidth_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'ntfy.sh Attachment bandwidth limit', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_attachment_bandwidth_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1024.0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_expiry_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_attachment_expiry_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Attachment expiry duration', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_attachment_expiry_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_expiry_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'ntfy.sh Attachment expiry duration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_attachment_expiry_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_file_size_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_attachment_file_size_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Attachment file size limit', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_attachment_file_size', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_file_size_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'ntfy.sh Attachment file size limit', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_attachment_file_size_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_storage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_attachment_storage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Attachment storage', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_attachment_total_size', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_storage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'ntfy.sh Attachment storage', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_attachment_storage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_storage_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_attachment_storage_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Attachment storage limit', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_attachment_total_size_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_storage_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'ntfy.sh Attachment storage limit', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_attachment_storage_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_storage_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_attachment_storage_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Attachment storage remaining', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_attachment_total_size_remaining', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.ntfy_sh_attachment_storage_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'ntfy.sh Attachment storage remaining', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_attachment_storage_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_email_usage_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_email_usage_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Email usage limit', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_emails_limit', + 'unit_of_measurement': 'emails', + }) +# --- +# name: test_setup[sensor.ntfy_sh_email_usage_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Email usage limit', + 'unit_of_measurement': 'emails', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_email_usage_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_setup[sensor.ntfy_sh_emails_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_emails_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Emails remaining', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_emails_remaining', + 'unit_of_measurement': 'emails', + }) +# --- +# name: test_setup[sensor.ntfy_sh_emails_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Emails remaining', + 'unit_of_measurement': 'emails', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_emails_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_setup[sensor.ntfy_sh_emails_sent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_emails_sent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Emails sent', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_emails', + 'unit_of_measurement': 'emails', + }) +# --- +# name: test_setup[sensor.ntfy_sh_emails_sent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Emails sent', + 'unit_of_measurement': 'emails', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_emails_sent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_messages_expiry_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_messages_expiry_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Messages expiry duration', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_messages_expiry_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.ntfy_sh_messages_expiry_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'ntfy.sh Messages expiry duration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_messages_expiry_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_messages_published-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_messages_published', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Messages published', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_messages', + 'unit_of_measurement': 'messages', + }) +# --- +# name: test_setup[sensor.ntfy_sh_messages_published-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Messages published', + 'unit_of_measurement': 'messages', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_messages_published', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_setup[sensor.ntfy_sh_messages_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_messages_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Messages remaining', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_messages_remaining', + 'unit_of_measurement': 'messages', + }) +# --- +# name: test_setup[sensor.ntfy_sh_messages_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Messages remaining', + 'unit_of_measurement': 'messages', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_messages_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4990', + }) +# --- +# name: test_setup[sensor.ntfy_sh_messages_usage_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_messages_usage_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Messages usage limit', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_messages_limit', + 'unit_of_measurement': 'messages', + }) +# --- +# name: test_setup[sensor.ntfy_sh_messages_usage_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Messages usage limit', + 'unit_of_measurement': 'messages', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_messages_usage_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5000', + }) +# --- +# name: test_setup[sensor.ntfy_sh_phone_calls_made-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_phone_calls_made', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Phone calls made', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_calls', + 'unit_of_measurement': 'calls', + }) +# --- +# name: test_setup[sensor.ntfy_sh_phone_calls_made-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Phone calls made', + 'unit_of_measurement': 'calls', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_phone_calls_made', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_phone_calls_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_phone_calls_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Phone calls remaining', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_calls_remaining', + 'unit_of_measurement': 'calls', + }) +# --- +# name: test_setup[sensor.ntfy_sh_phone_calls_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Phone calls remaining', + 'unit_of_measurement': 'calls', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_phone_calls_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_phone_calls_usage_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_phone_calls_usage_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Phone calls usage limit', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_calls_limit', + 'unit_of_measurement': 'calls', + }) +# --- +# name: test_setup[sensor.ntfy_sh_phone_calls_usage_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Phone calls usage limit', + 'unit_of_measurement': 'calls', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_phone_calls_usage_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_setup[sensor.ntfy_sh_reserved_topics-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_reserved_topics', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reserved topics', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_reservations', + 'unit_of_measurement': 'topics', + }) +# --- +# name: test_setup[sensor.ntfy_sh_reserved_topics-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Reserved topics', + 'unit_of_measurement': 'topics', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_reserved_topics', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_setup[sensor.ntfy_sh_reserved_topics_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_reserved_topics_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reserved topics limit', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_reservations_limit', + 'unit_of_measurement': 'topics', + }) +# --- +# name: test_setup[sensor.ntfy_sh_reserved_topics_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Reserved topics limit', + 'unit_of_measurement': 'topics', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_reserved_topics_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_setup[sensor.ntfy_sh_reserved_topics_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ntfy_sh_reserved_topics_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reserved topics remaining', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_reservations_remaining', + 'unit_of_measurement': 'topics', + }) +# --- +# name: test_setup[sensor.ntfy_sh_reserved_topics_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Reserved topics remaining', + 'unit_of_measurement': 'topics', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_reserved_topics_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_setup[sensor.ntfy_sh_subscription_tier-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ntfy_sh_subscription_tier', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Subscription tier', + 'platform': 'ntfy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '123456789_tier', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.ntfy_sh_subscription_tier-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ntfy.sh Subscription tier', + }), + 'context': , + 'entity_id': 'sensor.ntfy_sh_subscription_tier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'starter', + }) +# --- diff --git a/tests/components/ntfy/test_init.py b/tests/components/ntfy/test_init.py index b80badd8581..b5b73d1272c 100644 --- a/tests/components/ntfy/test_init.py +++ b/tests/components/ntfy/test_init.py @@ -65,3 +65,37 @@ async def test_config_entry_not_ready( await hass.async_block_till_done() assert config_entry.state is state + + +@pytest.mark.parametrize( + ("exception", "state"), + [ + ( + NtfyUnauthorizedAuthenticationError( + 40101, + 401, + "unauthorized", + "https://ntfy.sh/docs/publish/#authentication", + ), + ConfigEntryState.SETUP_ERROR, + ), + (NtfyHTTPError(418001, 418, "I'm a teapot", ""), ConfigEntryState.SETUP_RETRY), + (NtfyConnectionError, ConfigEntryState.SETUP_RETRY), + (NtfyTimeoutError, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_coordinator_update_exceptions( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_aiontfy: AsyncMock, + exception: Exception, + state: ConfigEntryState, +) -> None: + """Test config entry not ready from update failed in _async_update_data.""" + mock_aiontfy.account.side_effect = [None, exception] + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is state diff --git a/tests/components/ntfy/test_sensor.py b/tests/components/ntfy/test_sensor.py new file mode 100644 index 00000000000..4685cf946ee --- /dev/null +++ b/tests/components/ntfy/test_sensor.py @@ -0,0 +1,42 @@ +"""Tests for the ntfy sensor platform.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +def sensor_only() -> Generator[None]: + """Enable only the sensor platform.""" + with patch( + "homeassistant.components.ntfy.PLATFORMS", + [Platform.SENSOR], + ): + yield + + +@pytest.mark.usefixtures("mock_aiontfy", "entity_registry_enabled_by_default") +async def test_setup( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Snapshot test states of sensor platform.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)