diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index 531b88ac38a..8e4910d937a 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -28,7 +28,7 @@ from .coordinator import MastodonConfigEntry, MastodonCoordinator, MastodonData from .services import async_setup_services from .utils import construct_mastodon_username, create_mastodon_client -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/mastodon/binary_sensor.py b/homeassistant/components/mastodon/binary_sensor.py new file mode 100644 index 00000000000..42400c8b238 --- /dev/null +++ b/homeassistant/components/mastodon/binary_sensor.py @@ -0,0 +1,128 @@ +"""Binary sensor platform for the Mastodon integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import StrEnum + +from mastodon.Mastodon import Account + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import MastodonConfigEntry +from .entity import MastodonEntity + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + + +class MastodonBinarySensor(StrEnum): + """Mastodon binary sensors.""" + + BOT = "bot" + SUSPENDED = "suspended" + DISCOVERABLE = "discoverable" + LOCKED = "locked" + INDEXABLE = "indexable" + LIMITED = "limited" + MEMORIAL = "memorial" + MOVED = "moved" + + +@dataclass(frozen=True, kw_only=True) +class MastodonBinarySensorEntityDescription(BinarySensorEntityDescription): + """Mastodon binary sensor description.""" + + is_on_fn: Callable[[Account], bool | None] + + +ENTITY_DESCRIPTIONS: tuple[MastodonBinarySensorEntityDescription, ...] = ( + MastodonBinarySensorEntityDescription( + key=MastodonBinarySensor.BOT, + translation_key=MastodonBinarySensor.BOT, + is_on_fn=lambda account: account.bot, + entity_category=EntityCategory.DIAGNOSTIC, + ), + MastodonBinarySensorEntityDescription( + key=MastodonBinarySensor.DISCOVERABLE, + translation_key=MastodonBinarySensor.DISCOVERABLE, + is_on_fn=lambda account: account.discoverable, + entity_category=EntityCategory.DIAGNOSTIC, + ), + MastodonBinarySensorEntityDescription( + key=MastodonBinarySensor.LOCKED, + translation_key=MastodonBinarySensor.LOCKED, + is_on_fn=lambda account: account.locked, + entity_category=EntityCategory.DIAGNOSTIC, + ), + MastodonBinarySensorEntityDescription( + key=MastodonBinarySensor.MOVED, + translation_key=MastodonBinarySensor.MOVED, + is_on_fn=lambda account: account.moved is not None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + MastodonBinarySensorEntityDescription( + key=MastodonBinarySensor.INDEXABLE, + translation_key=MastodonBinarySensor.INDEXABLE, + is_on_fn=lambda account: account.indexable, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + MastodonBinarySensorEntityDescription( + key=MastodonBinarySensor.LIMITED, + translation_key=MastodonBinarySensor.LIMITED, + is_on_fn=lambda account: account.limited is True, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + MastodonBinarySensorEntityDescription( + key=MastodonBinarySensor.MEMORIAL, + translation_key=MastodonBinarySensor.MEMORIAL, + is_on_fn=lambda account: account.memorial is True, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + MastodonBinarySensorEntityDescription( + key=MastodonBinarySensor.SUSPENDED, + translation_key=MastodonBinarySensor.SUSPENDED, + is_on_fn=lambda account: account.suspended is True, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: MastodonConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the binary sensor platform.""" + coordinator = entry.runtime_data.coordinator + + async_add_entities( + MastodonBinarySensorEntity( + coordinator=coordinator, + entity_description=entity_description, + data=entry, + ) + for entity_description in ENTITY_DESCRIPTIONS + ) + + +class MastodonBinarySensorEntity(MastodonEntity, BinarySensorEntity): + """Mastodon binary sensor entity.""" + + entity_description: MastodonBinarySensorEntityDescription + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.is_on_fn(self.coordinator.data) diff --git a/homeassistant/components/mastodon/icons.json b/homeassistant/components/mastodon/icons.json index e7272c2b6f8..50bfcb1040d 100644 --- a/homeassistant/components/mastodon/icons.json +++ b/homeassistant/components/mastodon/icons.json @@ -1,5 +1,18 @@ { "entity": { + "binary_sensor": { + "bot": { "default": "mdi:robot" }, + "discoverable": { "default": "mdi:magnify-scan" }, + "indexable": { "default": "mdi:search-web" }, + "limited": { "default": "mdi:volume-mute" }, + "locked": { + "default": "mdi:account-lock", + "state": { "off": "mdi:account-lock-open" } + }, + "memorial": { "default": "mdi:candle" }, + "moved": { "default": "mdi:truck-delivery" }, + "suspended": { "default": "mdi:account-off" } + }, "sensor": { "followers": { "default": "mdi:account-multiple" diff --git a/homeassistant/components/mastodon/strings.json b/homeassistant/components/mastodon/strings.json index 93161a8129d..acd1c93eb28 100644 --- a/homeassistant/components/mastodon/strings.json +++ b/homeassistant/components/mastodon/strings.json @@ -26,6 +26,16 @@ } }, "entity": { + "binary_sensor": { + "bot": { "name": "Bot" }, + "discoverable": { "name": "Discoverable" }, + "indexable": { "name": "Indexable" }, + "limited": { "name": "Muted" }, + "locked": { "name": "Locked" }, + "memorial": { "name": "Memorial" }, + "moved": { "name": "Moved" }, + "suspended": { "name": "Suspended" } + }, "sensor": { "followers": { "name": "Followers", diff --git a/tests/components/mastodon/snapshots/test_binary_sensor.ambr b/tests/components/mastodon/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..c4176e92a54 --- /dev/null +++ b/tests/components/mastodon/snapshots/test_binary_sensor.ambr @@ -0,0 +1,393 @@ +# serializer version: 1 +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_bot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_bot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Bot', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bot', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'trwnh_mastodon_social_bot', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_bot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Bot', + }), + 'context': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_bot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_discoverable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_discoverable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Discoverable', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Discoverable', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'trwnh_mastodon_social_discoverable', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_discoverable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Discoverable', + }), + 'context': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_discoverable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_indexable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_indexable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Indexable', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indexable', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'trwnh_mastodon_social_indexable', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_indexable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Indexable', + }), + 'context': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_indexable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_locked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_locked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Locked', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Locked', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'trwnh_mastodon_social_locked', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_locked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Locked', + }), + 'context': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_locked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_memorial-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_memorial', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Memorial', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Memorial', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'trwnh_mastodon_social_memorial', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_memorial-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Memorial', + }), + 'context': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_memorial', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_moved-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_moved', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Moved', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Moved', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'trwnh_mastodon_social_moved', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_moved-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Moved', + }), + 'context': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_moved', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_muted-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_muted', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Muted', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Muted', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'trwnh_mastodon_social_limited', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_muted-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Muted', + }), + 'context': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_muted', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_suspended-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_suspended', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Suspended', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Suspended', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'trwnh_mastodon_social_suspended', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.mastodon_trwnh_mastodon_social_suspended-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Suspended', + }), + 'context': , + 'entity_id': 'binary_sensor.mastodon_trwnh_mastodon_social_suspended', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/mastodon/test_binary_sensor.py b/tests/components/mastodon/test_binary_sensor.py new file mode 100644 index 00000000000..212daebbd38 --- /dev/null +++ b/tests/components/mastodon/test_binary_sensor.py @@ -0,0 +1,28 @@ +"""Tests for the Mastodon binary sensors.""" + +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("mock_mastodon_client", "entity_registry_enabled_by_default") +async def test_binary_sensors( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the binary sensor entities.""" + with patch("homeassistant.components.mastodon.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)