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

Create Chess.com integration (#164960)

This commit is contained in:
Joost Lekkerkerker
2026-03-06 15:55:59 +01:00
committed by GitHub
parent fc68828c78
commit 0ab62dabde
24 changed files with 990 additions and 0 deletions

2
CODEOWNERS generated
View File

@@ -281,6 +281,8 @@ build.json @home-assistant/supervisor
/tests/components/cert_expiry/ @jjlawren
/homeassistant/components/chacon_dio/ @cnico
/tests/components/chacon_dio/ @cnico
/homeassistant/components/chess_com/ @joostlek
/tests/components/chess_com/ @joostlek
/homeassistant/components/cisco_ios/ @fbradyirl
/homeassistant/components/cisco_mobility_express/ @fbradyirl
/homeassistant/components/cisco_webex_teams/ @fbradyirl

View File

@@ -0,0 +1,31 @@
"""The Chess.com integration."""
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import ChessConfigEntry, ChessCoordinator
_PLATFORMS: list[Platform] = [
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: ChessConfigEntry) -> bool:
"""Set up Chess.com from a config entry."""
coordinator = ChessCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ChessConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

View File

@@ -0,0 +1,47 @@
"""Config flow for the Chess.com integration."""
from __future__ import annotations
import logging
from typing import Any
from chess_com_api import ChessComClient, NotFoundError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Chess.com."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
session = async_get_clientsession(self.hass)
client = ChessComClient(session=session)
try:
user = await client.get_player(user_input[CONF_USERNAME])
except NotFoundError:
errors["base"] = "player_not_found"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(str(user.player_id))
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user.name, data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_USERNAME): str}),
errors=errors,
)

View File

@@ -0,0 +1,3 @@
"""Constants for the Chess.com integration."""
DOMAIN = "chess_com"

View File

@@ -0,0 +1,57 @@
"""Coordinator for Chess.com."""
from dataclasses import dataclass
from datetime import timedelta
import logging
from chess_com_api import ChessComAPIError, ChessComClient, Player, PlayerStats
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
type ChessConfigEntry = ConfigEntry[ChessCoordinator]
@dataclass
class ChessData:
"""Data for Chess.com."""
player: Player
stats: PlayerStats
class ChessCoordinator(DataUpdateCoordinator[ChessData]):
"""Coordinator for Chess.com."""
config_entry: ChessConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ChessConfigEntry,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=config_entry.title,
update_interval=timedelta(hours=1),
)
self.client = ChessComClient(session=async_get_clientsession(hass))
async def _async_update_data(self) -> ChessData:
"""Update data from Chess.com."""
try:
player = await self.client.get_player(self.config_entry.data[CONF_USERNAME])
stats = await self.client.get_player_stats(
self.config_entry.data[CONF_USERNAME]
)
except ChessComAPIError as err:
raise UpdateFailed(f"Error communicating with Chess.com: {err}") from err
return ChessData(player=player, stats=stats)

View File

@@ -0,0 +1,26 @@
"""Base entity for Chess.com integration."""
from typing import TYPE_CHECKING
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ChessCoordinator
class ChessEntity(CoordinatorEntity[ChessCoordinator]):
"""Base entity for Chess.com integration."""
_attr_has_entity_name = True
def __init__(self, coordinator: ChessCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
if TYPE_CHECKING:
assert coordinator.config_entry.unique_id is not None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.unique_id)},
entry_type=DeviceEntryType.SERVICE,
manufacturer="Chess.com",
)

View File

@@ -0,0 +1,21 @@
{
"entity": {
"sensor": {
"chess_daily_rating": {
"default": "mdi:chart-line"
},
"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

@@ -0,0 +1,12 @@
{
"domain": "chess_com",
"name": "Chess.com",
"codeowners": ["@joostlek"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/chess_com",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["chess_com_api"],
"quality_scale": "bronze",
"requirements": ["chess-com-api==1.1.0"]
}

View File

@@ -0,0 +1,74 @@
rules:
# Bronze
action-setup:
status: exempt
comment: There are no custom actions
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: There are no custom actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Entities do not explicitly subscribe to events
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: There are no configuration parameters
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: Can't detect a game
discovery:
status: exempt
comment: Can't detect a game
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: There are no repairable issues
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -0,0 +1,97 @@
"""Sensor platform for Chess.com integration."""
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ChessConfigEntry
from .coordinator import ChessCoordinator, ChessData
from .entity import ChessEntity
@dataclass(kw_only=True, frozen=True)
class ChessEntityDescription(SensorEntityDescription):
"""Sensor description for Chess.com player."""
value_fn: Callable[[ChessData], float]
SENSORS: tuple[ChessEntityDescription, ...] = (
ChessEntityDescription(
key="followers",
translation_key="followers",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda state: state.player.followers,
entity_registry_enabled_default=False,
),
ChessEntityDescription(
key="chess_daily_rating",
translation_key="chess_daily_rating",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda state: state.stats.chess_daily["last"]["rating"],
),
ChessEntityDescription(
key="total_daily_won",
translation_key="total_daily_won",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["win"],
),
ChessEntityDescription(
key="total_daily_lost",
translation_key="total_daily_lost",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["loss"],
),
ChessEntityDescription(
key="total_daily_draw",
translation_key="total_daily_draw",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["draw"],
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ChessConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize the entries."""
coordinator = entry.runtime_data
async_add_entities(
ChessPlayerSensor(coordinator, description) for description in SENSORS
)
class ChessPlayerSensor(ChessEntity, SensorEntity):
"""Chess.com sensor."""
entity_description: ChessEntityDescription
def __init__(
self,
coordinator: ChessCoordinator,
description: ChessEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}.{description.key}"
@property
def native_value(self) -> float:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -0,0 +1,47 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"player_not_found": "Player not found.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
"user": "Add player"
},
"step": {
"user": {
"data": {
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"username": "The Chess.com username of the player to monitor."
}
}
}
},
"entity": {
"sensor": {
"chess_daily_rating": {
"name": "Daily chess rating"
},
"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

@@ -121,6 +121,7 @@ FLOWS = {
"ccm15",
"cert_expiry",
"chacon_dio",
"chess_com",
"cloudflare",
"cloudflare_r2",
"co2signal",

View File

@@ -996,6 +996,12 @@
"config_flow": false,
"iot_class": "local_polling"
},
"chess_com": {
"name": "Chess.com",
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling"
},
"cisco": {
"name": "Cisco",
"integrations": {

3
requirements_all.txt generated
View File

@@ -722,6 +722,9 @@ cached-ipaddress==1.0.1
# homeassistant.components.caldav
caldav==2.1.0
# homeassistant.components.chess_com
chess-com-api==1.1.0
# homeassistant.components.cisco_mobility_express
ciscomobilityexpress==0.3.9

View File

@@ -646,6 +646,9 @@ cached-ipaddress==1.0.1
# homeassistant.components.caldav
caldav==2.1.0
# homeassistant.components.chess_com
chess-com-api==1.1.0
# homeassistant.components.coinbase
coinbase-advanced-py==1.2.2

View File

@@ -0,0 +1,13 @@
"""Tests for the Chess.com integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Method for setting up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -0,0 +1,53 @@
"""Common fixtures for the Chess.com tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from chess_com_api import Player, PlayerStats
import pytest
from homeassistant.components.chess_com.const import DOMAIN
from homeassistant.const import CONF_USERNAME
from tests.common import MockConfigEntry, load_json_object_fixture
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.chess_com.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="Joost",
unique_id="532748851",
data={CONF_USERNAME: "joostlek"},
)
@pytest.fixture
def mock_chess_client() -> Generator[AsyncMock]:
"""Mock Chess.com client."""
with (
patch(
"homeassistant.components.chess_com.coordinator.ChessComClient",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.chess_com.config_flow.ChessComClient",
new=mock_client,
),
):
client = mock_client.return_value
player_data = load_json_object_fixture("player.json", DOMAIN)
client.get_player.return_value = Player.from_dict(player_data)
stats_data = load_json_object_fixture("stats.json", DOMAIN)
client.get_player_stats.return_value = PlayerStats.from_dict(stats_data)
yield client

View File

@@ -0,0 +1,17 @@
{
"avatar": "https://images.chesscomfiles.com/uploads/v1/user/532748851.d5fefa92.200x200o.da2274e46acd.jpg",
"player_id": 532748851,
"@id": "https://api.chess.com/pub/player/joostlek",
"url": "https://www.chess.com/member/joostlek",
"name": "Joost",
"username": "joostlek",
"followers": 2,
"country": "https://api.chess.com/pub/country/NL",
"location": "Utrecht",
"last_online": 1772800379,
"joined": 1771584494,
"status": "basic",
"is_streamer": false,
"verified": false,
"streaming_platforms": []
}

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

@@ -0,0 +1,32 @@
# serializer version: 1
# name: test_device
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'chess_com',
'532748851',
),
}),
'labels': set({
}),
'manufacturer': 'Chess.com',
'model': None,
'model_id': None,
'name': 'Joost',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---

View File

@@ -0,0 +1,265 @@
# serializer version: 1
# name: test_all_entities[sensor.joost_daily_chess_rating-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'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.joost_daily_chess_rating',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Daily chess rating',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Daily chess rating',
'platform': 'chess_com',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'chess_daily_rating',
'unique_id': '532748851.chess_daily_rating',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sensor.joost_daily_chess_rating-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Joost Daily chess rating',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.joost_daily_chess_rating',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '495',
})
# ---
# name: test_all_entities[sensor.joost_followers-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.joost_followers',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Followers',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Followers',
'platform': 'chess_com',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'followers',
'unique_id': '532748851.followers',
'unit_of_measurement': 'followers',
})
# ---
# name: test_all_entities[sensor.joost_followers-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Joost Followers',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'followers',
}),
'context': <ANY>,
'entity_id': 'sensor.joost_followers',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2',
})
# ---
# name: test_all_entities[sensor.joost_total_chess_games_drawn-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.joost_total_chess_games_drawn',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Total chess games drawn',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Total chess games drawn',
'platform': 'chess_com',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_daily_draw',
'unique_id': '532748851.total_daily_draw',
'unit_of_measurement': 'games',
})
# ---
# name: test_all_entities[sensor.joost_total_chess_games_drawn-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Joost Total chess games drawn',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'games',
}),
'context': <ANY>,
'entity_id': 'sensor.joost_total_chess_games_drawn',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_all_entities[sensor.joost_total_chess_games_lost-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.joost_total_chess_games_lost',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Total chess games lost',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Total chess games lost',
'platform': 'chess_com',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_daily_lost',
'unique_id': '532748851.total_daily_lost',
'unit_of_measurement': 'games',
})
# ---
# name: test_all_entities[sensor.joost_total_chess_games_lost-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Joost Total chess games lost',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'games',
}),
'context': <ANY>,
'entity_id': 'sensor.joost_total_chess_games_lost',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4',
})
# ---
# name: test_all_entities[sensor.joost_total_chess_games_won-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.joost_total_chess_games_won',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Total chess games won',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Total chess games won',
'platform': 'chess_com',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_daily_won',
'unique_id': '532748851.total_daily_won',
'unit_of_measurement': 'games',
})
# ---
# name: test_all_entities[sensor.joost_total_chess_games_won-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Joost Total chess games won',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'games',
}),
'context': <ANY>,
'entity_id': 'sensor.joost_total_chess_games_won',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---

View File

@@ -0,0 +1,91 @@
"""Test the Chess.com config flow."""
from unittest.mock import AsyncMock
from chess_com_api import NotFoundError
import pytest
from homeassistant.components.chess_com.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_chess_client")
async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
"""Test the full flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_USERNAME: "joostlek"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Joost"
assert result["data"] == {CONF_USERNAME: "joostlek"}
assert result["result"].unique_id == "532748851"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("exception", "error"),
[
(NotFoundError, "player_not_found"),
(Exception, "unknown"),
],
)
async def test_form_errors(
hass: HomeAssistant,
mock_chess_client: AsyncMock,
mock_setup_entry: AsyncMock,
exception: Exception,
error: str,
) -> None:
"""Test we handle form errors."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
mock_chess_client.get_player.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_USERNAME: "joostlek"}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error}
mock_chess_client.get_player.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_USERNAME: "joostlek"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_chess_client")
async def test_duplicate_entry(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test we handle duplicate entries."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_USERNAME: "joostlek"}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@@ -0,0 +1,27 @@
"""Test the Chess.com initialization."""
from unittest.mock import AsyncMock
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.chess_com.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from tests.common import MockConfigEntry
async def test_device(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
mock_chess_client: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Chess.com device."""
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device({(DOMAIN, "532748851")})
assert device
assert device == snapshot

View File

@@ -0,0 +1,29 @@
"""Tests for the Chess.com sensor."""
from unittest.mock import AsyncMock, 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("entity_registry_enabled_by_default")
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_chess_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch("homeassistant.components.chess_com._PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)