Add more sensors to Mastodon integration (#160835)

This commit is contained in:
Manu
2026-01-26 15:42:20 +01:00
committed by GitHub
parent 9fb1612dac
commit 20c0c5f655
5 changed files with 190 additions and 8 deletions

View File

@@ -45,3 +45,4 @@ class MastodonEntity(CoordinatorEntity[MastodonCoordinator]):
)
self.entity_description = entity_description
self.instance = data.runtime_data.instance

View File

@@ -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"
}
}
},

View File

@@ -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
)

View File

@@ -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"
}
}
}
}
},

View File

@@ -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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'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': <ANY>,
'entity_id': 'sensor.mastodon_trwnh_mastodon_social_last_post',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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&#39;s Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)',
'Portfolio': '<a href="https://abdullahtarawneh.com" target="_blank" rel="nofollow noopener me" translate="no"><span class="invisible">https://</span><span class="">abdullahtarawneh.com</span><span class="invisible"></span></a>',
'Website': '<a href="https://trwnh.com" target="_blank" rel="nofollow noopener me" translate="no"><span class="invisible">https://</span><span class="">trwnh.com</span><span class="invisible"></span></a>',
'What to expect:': 'talking about various things i find interesting, and otherwise being a genuine and decent wholesome poster. i&#39;m just here to hang out and talk to cool people! and to spill my thoughts.',
'bio': '<p>i have approximate knowledge of many things. perpetual student. (nb/ace/they)</p><p>xmpp/email: a@trwnh.com<br /><a href="https://trwnh.com" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="">trwnh.com</span><span class="invisible"></span></a><br />help me live:<br />- <a href="https://donate.stripe.com/4gwcPCaMpcQ19RC4gg" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="ellipsis">donate.stripe.com/4gwcPCaMpcQ1</span><span class="invisible">9RC4gg</span></a><br />- <a href="https://liberapay.com/trwnh" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="">liberapay.com/trwnh</span><span class="invisible"></span></a></p><p>notes:<br />- my triggers are moths and glitter<br />- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise<br />- dm me if i did something wrong, so i can improve<br />- purest person on fedi, do not lewd in my presence</p>',
'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': <ANY>,
'entity_id': 'sensor.mastodon_trwnh_mastodon_social_username',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '@trwnh@mastodon.social',
})
# ---