1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Add sensors for more game modes to Chess.com (#166331)

This commit is contained in:
Joost Lekkerkerker
2026-03-25 16:27:58 +01:00
committed by GitHub
parent 7a4f953fa6
commit 686ab66a52
8 changed files with 1268 additions and 75 deletions

View File

@@ -1,20 +1,68 @@
{
"entity": {
"sensor": {
"chess960_daily_draw": {
"default": "mdi:chess-pawn"
},
"chess960_daily_lost": {
"default": "mdi:chess-pawn"
},
"chess960_daily_rating": {
"default": "mdi:chart-line"
},
"chess960_daily_won": {
"default": "mdi:chess-pawn"
},
"chess_blitz_draw": {
"default": "mdi:chess-pawn"
},
"chess_blitz_lost": {
"default": "mdi:chess-pawn"
},
"chess_blitz_rating": {
"default": "mdi:chart-line"
},
"chess_blitz_won": {
"default": "mdi:chess-pawn"
},
"chess_bullet_draw": {
"default": "mdi:chess-pawn"
},
"chess_bullet_lost": {
"default": "mdi:chess-pawn"
},
"chess_bullet_rating": {
"default": "mdi:chart-line"
},
"chess_bullet_won": {
"default": "mdi:chess-pawn"
},
"chess_daily_draw": {
"default": "mdi:chess-pawn"
},
"chess_daily_lost": {
"default": "mdi:chess-pawn"
},
"chess_daily_rating": {
"default": "mdi:chart-line"
},
"chess_daily_won": {
"default": "mdi:chess-pawn"
},
"chess_rapid_draw": {
"default": "mdi:chess-pawn"
},
"chess_rapid_lost": {
"default": "mdi:chess-pawn"
},
"chess_rapid_rating": {
"default": "mdi:chart-line"
},
"chess_rapid_won": {
"default": "mdi:chess-pawn"
},
"followers": {
"default": "mdi:account-multiple"
},
"total_daily_draw": {
"default": "mdi:chess-pawn"
},
"total_daily_lost": {
"default": "mdi:chess-pawn"
},
"total_daily_won": {
"default": "mdi:chess-pawn"
}
}
}

View File

@@ -2,6 +2,9 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from chess_com_api import PlayerStats
from homeassistant.components.sensor import (
SensorEntity,
@@ -24,7 +27,14 @@ class ChessEntityDescription(SensorEntityDescription):
value_fn: Callable[[ChessData], float]
SENSORS: tuple[ChessEntityDescription, ...] = (
@dataclass(kw_only=True, frozen=True)
class ChessModeEntityDescription(SensorEntityDescription):
"""Sensor description for a Chess.com game mode."""
value_fn: Callable[[dict[str, Any]], float]
PLAYER_SENSORS: tuple[ChessEntityDescription, ...] = (
ChessEntityDescription(
key="followers",
translation_key="followers",
@@ -33,35 +43,46 @@ SENSORS: tuple[ChessEntityDescription, ...] = (
value_fn=lambda state: state.player.followers,
entity_registry_enabled_default=False,
),
ChessEntityDescription(
key="chess_daily_rating",
translation_key="chess_daily_rating",
)
GAME_MODE_SENSORS: tuple[ChessModeEntityDescription, ...] = (
ChessModeEntityDescription(
key="rating",
translation_key="rating",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda state: state.stats.chess_daily["last"]["rating"],
value_fn=lambda mode: mode["last"]["rating"],
),
ChessEntityDescription(
key="total_daily_won",
translation_key="total_daily_won",
ChessModeEntityDescription(
key="won",
translation_key="won",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["win"],
value_fn=lambda mode: mode["record"]["win"],
),
ChessEntityDescription(
key="total_daily_lost",
translation_key="total_daily_lost",
ChessModeEntityDescription(
key="lost",
translation_key="lost",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["loss"],
value_fn=lambda mode: mode["record"]["loss"],
),
ChessEntityDescription(
key="total_daily_draw",
translation_key="total_daily_draw",
ChessModeEntityDescription(
key="draw",
translation_key="draw",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["draw"],
value_fn=lambda mode: mode["record"]["draw"],
),
)
GAME_MODES: dict[str, Callable[[PlayerStats], dict[str, Any] | None]] = {
"chess_daily": lambda stats: stats.chess_daily,
"chess_rapid": lambda stats: stats.chess_rapid,
"chess_bullet": lambda stats: stats.chess_bullet,
"chess_blitz": lambda stats: stats.chess_blitz,
"chess960_daily": lambda stats: stats.chess960_daily,
}
async def async_setup_entry(
hass: HomeAssistant,
@@ -71,13 +92,22 @@ async def async_setup_entry(
"""Initialize the entries."""
coordinator = entry.runtime_data
async_add_entities(
ChessPlayerSensor(coordinator, description) for description in SENSORS
)
entities: list[SensorEntity] = [
ChessPlayerSensor(coordinator, description) for description in PLAYER_SENSORS
]
for game_mode, stats_fn in GAME_MODES.items():
if stats_fn(coordinator.data.stats) is not None:
entities.extend(
ChessGameModeSensor(coordinator, description, game_mode, stats_fn)
for description in GAME_MODE_SENSORS
)
async_add_entities(entities)
class ChessPlayerSensor(ChessEntity, SensorEntity):
"""Chess.com sensor."""
"""Chess.com player sensor."""
entity_description: ChessEntityDescription
@@ -95,3 +125,33 @@ class ChessPlayerSensor(ChessEntity, SensorEntity):
def native_value(self) -> float:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)
class ChessGameModeSensor(ChessEntity, SensorEntity):
"""Chess.com game mode sensor."""
entity_description: ChessModeEntityDescription
def __init__(
self,
coordinator: ChessCoordinator,
description: ChessModeEntityDescription,
game_mode: str,
stats_fn: Callable[[PlayerStats], dict[str, Any] | None],
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._stats_fn = stats_fn
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}.{game_mode}.{description.key}"
)
self._attr_translation_key = f"{game_mode}_{description.translation_key}"
@property
def native_value(self) -> float:
"""Return the state of the sensor."""
mode_data = self._stats_fn(self.coordinator.data.stats)
if TYPE_CHECKING:
assert mode_data is not None
return self.entity_description.value_fn(mode_data)

View File

@@ -23,24 +23,84 @@
},
"entity": {
"sensor": {
"chess960_daily_draw": {
"name": "Total daily Chess960 games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess960_daily_lost": {
"name": "Total daily Chess960 games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess960_daily_rating": {
"name": "Daily Chess960 rating"
},
"chess960_daily_won": {
"name": "Total daily Chess960 games won",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_blitz_draw": {
"name": "Total blitz chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_blitz_lost": {
"name": "Total blitz chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_blitz_rating": {
"name": "Blitz chess rating"
},
"chess_blitz_won": {
"name": "Total blitz chess games won",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_bullet_draw": {
"name": "Total bullet chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_bullet_lost": {
"name": "Total bullet chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_bullet_rating": {
"name": "Bullet chess rating"
},
"chess_bullet_won": {
"name": "Total bullet chess games won",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_daily_draw": {
"name": "Total daily chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_daily_lost": {
"name": "Total daily chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_daily_rating": {
"name": "Daily chess rating"
},
"chess_daily_won": {
"name": "Total daily chess games won",
"unit_of_measurement": "games"
},
"chess_rapid_draw": {
"name": "Total rapid chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_rapid_lost": {
"name": "Total rapid chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_rapid_rating": {
"name": "Rapid chess rating"
},
"chess_rapid_won": {
"name": "Total rapid chess games won",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"followers": {
"name": "Followers",
"unit_of_measurement": "followers"
},
"total_daily_draw": {
"name": "Total chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
},
"total_daily_lost": {
"name": "Total chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
},
"total_daily_won": {
"name": "Total chess games won",
"unit_of_measurement": "games"
}
}
}

View File

@@ -13,6 +13,62 @@
"timeout_percent": 0
}
},
"chess_rapid": {
"last": {
"rating": 812,
"date": 1772800350,
"rd": 74
},
"record": {
"win": 5,
"loss": 3,
"draw": 1,
"time_per_move": 42,
"timeout_percent": 0
}
},
"chess_bullet": {
"last": {
"rating": 650,
"date": 1772800350,
"rd": 120
},
"record": {
"win": 2,
"loss": 6,
"draw": 0,
"time_per_move": 8,
"timeout_percent": 0
}
},
"chess_blitz": {
"last": {
"rating": 734,
"date": 1772800350,
"rd": 95
},
"record": {
"win": 10,
"loss": 8,
"draw": 2,
"time_per_move": 18,
"timeout_percent": 0
}
},
"chess960_daily": {
"last": {
"rating": 521,
"date": 1772800350,
"rd": 200
},
"record": {
"win": 1,
"loss": 2,
"draw": 0,
"time_per_move": 7200,
"timeout_percent": 0
}
},
"fide": 0,
"tactics": {
"highest": {

View File

@@ -0,0 +1,33 @@
{
"chess_daily": {
"last": {
"rating": 495,
"date": 1772800350,
"rd": 196
},
"record": {
"win": 0,
"loss": 4,
"draw": 0,
"time_per_move": 6974,
"timeout_percent": 0
}
},
"fide": 0,
"tactics": {
"highest": {
"rating": 764,
"date": 1772782351
},
"lowest": {
"rating": 400,
"date": 1771584762
}
},
"puzzle_rush": {
"best": {
"total_attempts": 11,
"score": 8
}
}
}

View File

@@ -19,9 +19,48 @@
'username': 'joostlek',
}),
'stats': dict({
'chess960_daily': None,
'chess_blitz': None,
'chess_bullet': None,
'chess960_daily': dict({
'last': dict({
'date': 1772800350,
'rating': 521,
'rd': 200,
}),
'record': dict({
'draw': 0,
'loss': 2,
'time_per_move': 7200,
'timeout_percent': 0,
'win': 1,
}),
}),
'chess_blitz': dict({
'last': dict({
'date': 1772800350,
'rating': 734,
'rd': 95,
}),
'record': dict({
'draw': 2,
'loss': 8,
'time_per_move': 18,
'timeout_percent': 0,
'win': 10,
}),
}),
'chess_bullet': dict({
'last': dict({
'date': 1772800350,
'rating': 650,
'rd': 120,
}),
'record': dict({
'draw': 0,
'loss': 6,
'time_per_move': 8,
'timeout_percent': 0,
'win': 2,
}),
}),
'chess_daily': dict({
'last': dict({
'date': 1772800350,
@@ -36,7 +75,20 @@
'win': 0,
}),
}),
'chess_rapid': None,
'chess_rapid': dict({
'last': dict({
'date': 1772800350,
'rating': 812,
'rd': 74,
}),
'record': dict({
'draw': 1,
'loss': 3,
'time_per_move': 42,
'timeout_percent': 0,
'win': 5,
}),
}),
'lessons': None,
'puzzle_rush': dict({
'best': dict({

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,22 @@
from unittest.mock import AsyncMock, patch
from chess_com_api import PlayerStats
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.chess_com.const import DOMAIN
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
from tests.common import (
MockConfigEntry,
async_load_json_object_fixture,
snapshot_platform,
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@@ -27,3 +33,21 @@ async def test_all_entities(
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_daily_only(
hass: HomeAssistant,
mock_chess_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that entities for unplayed game modes are not created."""
mock_chess_client.get_player_stats.return_value = PlayerStats.from_dict(
await async_load_json_object_fixture(hass, "stats_daily_only.json", DOMAIN)
)
with patch("homeassistant.components.chess_com._PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
assert hass.states.get("sensor.joost_rapid_chess_rating") is None
assert hass.states.get("sensor.joost_bullet_chess_rating") is None
assert hass.states.get("sensor.joost_blitz_chess_rating") is None
assert hass.states.get("sensor.joost_daily_chess960_rating") is None