From 20c0c5f655dcce219daebfa4f824770b21f5fc3f Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:42:20 +0100 Subject: [PATCH] Add more sensors to Mastodon integration (#160835) --- homeassistant/components/mastodon/entity.py | 1 + homeassistant/components/mastodon/icons.json | 6 + homeassistant/components/mastodon/sensor.py | 69 ++++++++++-- .../components/mastodon/strings.json | 17 +++ .../mastodon/snapshots/test_sensor.ambr | 105 ++++++++++++++++++ 5 files changed, 190 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mastodon/entity.py b/homeassistant/components/mastodon/entity.py index 60224e75e41..29e31e7deac 100644 --- a/homeassistant/components/mastodon/entity.py +++ b/homeassistant/components/mastodon/entity.py @@ -45,3 +45,4 @@ class MastodonEntity(CoordinatorEntity[MastodonCoordinator]): ) self.entity_description = entity_description + self.instance = data.runtime_data.instance diff --git a/homeassistant/components/mastodon/icons.json b/homeassistant/components/mastodon/icons.json index 2ea8b34788f..ba1f748b3c1 100644 --- a/homeassistant/components/mastodon/icons.json +++ b/homeassistant/components/mastodon/icons.json @@ -20,8 +20,14 @@ "following": { "default": "mdi:account-multiple" }, + "last_post": { + "default": "mdi:message-text-clock" + }, "posts": { "default": "mdi:message-text" + }, + "username": { + "default": "mdi:account" } } }, diff --git a/homeassistant/components/mastodon/sensor.py b/homeassistant/components/mastodon/sensor.py index bfdc9c90333..2b5ed7843be 100644 --- a/homeassistant/components/mastodon/sensor.py +++ b/homeassistant/components/mastodon/sensor.py @@ -2,12 +2,15 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass +from datetime import datetime +from typing import Any -from mastodon.Mastodon import Account +from mastodon.Mastodon import Account, Instance, InstanceV2 from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, @@ -15,9 +18,11 @@ from homeassistant.components.sensor import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util from .coordinator import MastodonConfigEntry from .entity import MastodonEntity +from .utils import construct_mastodon_username # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -27,7 +32,20 @@ PARALLEL_UPDATES = 0 class MastodonSensorEntityDescription(SensorEntityDescription): """Describes Mastodon sensor entity.""" - value_fn: Callable[[Account], StateType] + value_fn: Callable[[Account, InstanceV2 | Instance], StateType | datetime] + attributes_fn: Callable[[Account], Mapping[str, Any]] | None = None + entity_picture_fn: Callable[[Account], str] | None = None + + +def account_meta(data: Account) -> Mapping[str, Any]: + """Account attributes.""" + + return { + "display_name": data.display_name, + "bio": data.note, + "created": dt_util.as_local(data.created_at).date(), + **{f.name: f.value for f in data.fields}, + } ENTITY_DESCRIPTIONS = ( @@ -35,19 +53,36 @@ ENTITY_DESCRIPTIONS = ( key="followers", translation_key="followers", state_class=SensorStateClass.TOTAL, - value_fn=lambda data: data.followers_count, + value_fn=lambda data, _: data.followers_count, ), MastodonSensorEntityDescription( key="following", translation_key="following", state_class=SensorStateClass.TOTAL, - value_fn=lambda data: data.following_count, + value_fn=lambda data, _: data.following_count, ), MastodonSensorEntityDescription( key="posts", translation_key="posts", state_class=SensorStateClass.TOTAL, - value_fn=lambda data: data.statuses_count, + value_fn=lambda data, _: data.statuses_count, + ), + MastodonSensorEntityDescription( + key="last_post", + translation_key="last_post", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=( + lambda data, _: dt_util.as_local(data.last_status_at) + if data.last_status_at + else None + ), + ), + MastodonSensorEntityDescription( + key="username", + translation_key="username", + value_fn=lambda data, instance: construct_mastodon_username(instance, data), + attributes_fn=account_meta, + entity_picture_fn=lambda data: data.avatar, ), ) @@ -76,6 +111,24 @@ class MastodonSensorEntity(MastodonEntity, SensorEntity): entity_description: MastodonSensorEntityDescription @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the native value of the sensor.""" - return self.entity_description.value_fn(self.coordinator.data) + return self.entity_description.value_fn(self.coordinator.data, self.instance) + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + return ( + fn(self.coordinator.data) + if (fn := self.entity_description.attributes_fn) + else super().extra_state_attributes + ) + + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend, if any.""" + return ( + fn(self.coordinator.data) + if (fn := self.entity_description.entity_picture_fn) + else super().entity_picture + ) diff --git a/homeassistant/components/mastodon/strings.json b/homeassistant/components/mastodon/strings.json index 5c42cdefcf3..0b149919289 100644 --- a/homeassistant/components/mastodon/strings.json +++ b/homeassistant/components/mastodon/strings.json @@ -45,9 +45,26 @@ "name": "Following", "unit_of_measurement": "[%key:component::mastodon::entity::sensor::followers::unit_of_measurement%]" }, + "last_post": { + "name": "Last post" + }, "posts": { "name": "Posts", "unit_of_measurement": "posts" + }, + "username": { + "name": "Username", + "state_attributes": { + "bio": { + "name": "Bio" + }, + "created": { + "name": "Created" + }, + "display_name": { + "name": "Display name" + } + } } } }, diff --git a/tests/components/mastodon/snapshots/test_sensor.ambr b/tests/components/mastodon/snapshots/test_sensor.ambr index cc3d7c090e7..f6d5e86ac25 100644 --- a/tests/components/mastodon/snapshots/test_sensor.ambr +++ b/tests/components/mastodon/snapshots/test_sensor.ambr @@ -105,6 +105,55 @@ 'state': '328', }) # --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_last_post-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.mastodon_trwnh_mastodon_social_last_post', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last post', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_post', + 'unique_id': 'trwnh_mastodon_social_last_post', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_last_post-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Mastodon @trwnh@mastodon.social Last post', + }), + 'context': , + 'entity_id': 'sensor.mastodon_trwnh_mastodon_social_last_post', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-03-04T08:00:00+00:00', + }) +# --- # name: test_sensors[sensor.mastodon_trwnh_mastodon_social_posts-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -158,3 +207,59 @@ 'state': '69523', }) # --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_username-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.mastodon_trwnh_mastodon_social_username', + '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': 'Username', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'username', + 'unique_id': 'trwnh_mastodon_social_username', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_username-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'Fan of:': 'Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo's Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)', + 'Portfolio': 'abdullahtarawneh.com', + 'Website': 'trwnh.com', + 'What to expect:': 'talking about various things i find interesting, and otherwise being a genuine and decent wholesome poster. i'm just here to hang out and talk to cool people! and to spill my thoughts.', + 'bio': '

i have approximate knowledge of many things. perpetual student. (nb/ace/they)

xmpp/email: a@trwnh.com
trwnh.com
help me live:
- donate.stripe.com/4gwcPCaMpcQ1
- liberapay.com/trwnh

notes:
- my triggers are moths and glitter
- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise
- dm me if i did something wrong, so i can improve
- purest person on fedi, do not lewd in my presence

', + 'created': datetime.date(2016, 11, 23), + 'display_name': 'infinite love ⴳ', + 'entity_picture': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png', + 'friendly_name': 'Mastodon @trwnh@mastodon.social Username', + }), + 'context': , + 'entity_id': 'sensor.mastodon_trwnh_mastodon_social_username', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '@trwnh@mastodon.social', + }) +# ---