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

Add Lichess Integration (#166051)

This commit is contained in:
aryanhasgithub
2026-03-20 17:05:51 +05:30
committed by GitHub
parent 66b1728c13
commit 01b873f3bc
22 changed files with 1158 additions and 0 deletions

2
CODEOWNERS generated
View File

@@ -947,6 +947,8 @@ build.json @home-assistant/supervisor
/tests/components/lg_thinq/ @LG-ThinQ-Integration
/homeassistant/components/libre_hardware_monitor/ @Sab44
/tests/components/libre_hardware_monitor/ @Sab44
/homeassistant/components/lichess/ @aryanhasgithub
/tests/components/lichess/ @aryanhasgithub
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/liebherr/ @mettolen

View File

@@ -0,0 +1,31 @@
"""The Lichess integration."""
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import LichessConfigEntry, LichessCoordinator
_PLATFORMS: list[Platform] = [
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: LichessConfigEntry) -> bool:
"""Set up Lichess from a config entry."""
coordinator = LichessCoordinator(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: LichessConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

View File

@@ -0,0 +1,52 @@
"""Config flow for the Lichess integration."""
from __future__ import annotations
import logging
from typing import Any
from aiolichess import AioLichess
from aiolichess.exceptions import AioLichessError, AuthError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class LichessConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Lichess."""
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 = AioLichess(session=session)
try:
user = await client.get_all(token=user_input[CONF_API_TOKEN])
except AuthError:
errors["base"] = "invalid_auth"
except AioLichessError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
username = user.username
player_id = user.id
await self.async_set_unique_id(player_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=username, data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
errors=errors,
)

View File

@@ -0,0 +1,3 @@
"""Constants for the Lichess integration."""
DOMAIN = "lichess"

View File

@@ -0,0 +1,44 @@
"""Coordinator for Lichess."""
from datetime import timedelta
import logging
from aiolichess import AioLichess
from aiolichess.exceptions import AioLichessError
from aiolichess.models import LichessStatistics
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN
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 LichessConfigEntry = ConfigEntry[LichessCoordinator]
class LichessCoordinator(DataUpdateCoordinator[LichessStatistics]):
"""Coordinator for Lichess."""
config_entry: LichessConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: LichessConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=config_entry.title,
update_interval=timedelta(hours=1),
)
self.client = AioLichess(session=async_get_clientsession(hass))
async def _async_update_data(self) -> LichessStatistics:
"""Update data for Lichess."""
try:
return await self.client.get_statistics(
token=self.config_entry.data[CONF_API_TOKEN]
)
except AioLichessError as err:
raise UpdateFailed("Error in communicating with Lichess") from err

View File

@@ -0,0 +1,26 @@
"""Base entity for Lichess 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 LichessCoordinator
class LichessEntity(CoordinatorEntity[LichessCoordinator]):
"""Base entity for Lichess integration."""
_attr_has_entity_name = True
def __init__(self, coordinator: LichessCoordinator) -> 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="Lichess",
)

View File

@@ -0,0 +1,30 @@
{
"entity": {
"sensor": {
"blitz_games": {
"default": "mdi:chess-pawn"
},
"blitz_rating": {
"default": "mdi:chart-line"
},
"bullet_games": {
"default": "mdi:chess-pawn"
},
"bullet_rating": {
"default": "mdi:chart-line"
},
"classical_games": {
"default": "mdi:chess-pawn"
},
"classical_rating": {
"default": "mdi:chart-line"
},
"rapid_games": {
"default": "mdi:chess-pawn"
},
"rapid_rating": {
"default": "mdi:chart-line"
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"domain": "lichess",
"name": "Lichess",
"codeowners": ["@aryanhasgithub"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lichess",
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["aiolichess==1.2.0"]
}

View File

@@ -0,0 +1,72 @@
rules:
# Bronze
action-setup:
status: exempt
comment: There are no custom actions present
appropriate-polling: done
brands: done
common-modules: done
config-flow: done
config-flow-test-coverage: done
dependency-transparency: done
docs-actions:
status: exempt
comment: There are no custom actions present
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: The 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:
status: exempt
comment: There are no custom actions
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: The integration does not use discovery
discovery:
status: exempt
comment: The integration does not use discovery
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: done
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -0,0 +1,116 @@
"""Sensor platform for Lichess integration."""
from collections.abc import Callable
from dataclasses import dataclass
from aiolichess.models import LichessStatistics
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 LichessConfigEntry
from .coordinator import LichessCoordinator
from .entity import LichessEntity
@dataclass(kw_only=True, frozen=True)
class LichessEntityDescription(SensorEntityDescription):
"""Sensor description for Lichess player."""
value_fn: Callable[[LichessStatistics], int | None]
SENSORS: tuple[LichessEntityDescription, ...] = (
LichessEntityDescription(
key="bullet_rating",
translation_key="bullet_rating",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda state: state.bullet_rating,
),
LichessEntityDescription(
key="bullet_games",
translation_key="bullet_games",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: state.bullet_games,
),
LichessEntityDescription(
key="blitz_rating",
translation_key="blitz_rating",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda state: state.blitz_rating,
),
LichessEntityDescription(
key="blitz_games",
translation_key="blitz_games",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: state.blitz_games,
),
LichessEntityDescription(
key="rapid_rating",
translation_key="rapid_rating",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda state: state.rapid_rating,
),
LichessEntityDescription(
key="rapid_games",
translation_key="rapid_games",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: state.rapid_games,
),
LichessEntityDescription(
key="classical_rating",
translation_key="classical_rating",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda state: state.classical_rating,
),
LichessEntityDescription(
key="classical_games",
translation_key="classical_games",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: state.classical_games,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: LichessConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize the entries."""
coordinator = entry.runtime_data
async_add_entities(
LichessPlayerSensor(coordinator, description) for description in SENSORS
)
class LichessPlayerSensor(LichessEntity, SensorEntity):
"""Lichess sensor."""
entity_description: LichessEntityDescription
def __init__(
self,
coordinator: LichessCoordinator,
description: LichessEntityDescription,
) -> 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) -> int | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -0,0 +1,54 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]"
},
"data_description": {
"api_token": "The Lichess API token of the player."
}
}
}
},
"entity": {
"sensor": {
"blitz_games": {
"name": "Blitz games",
"unit_of_measurement": "[%key:component::lichess::entity::sensor::bullet_games::unit_of_measurement%]"
},
"blitz_rating": {
"name": "Blitz rating"
},
"bullet_games": {
"name": "Bullet games",
"unit_of_measurement": "games"
},
"bullet_rating": {
"name": "Bullet rating"
},
"classical_games": {
"name": "Classical games",
"unit_of_measurement": "[%key:component::lichess::entity::sensor::bullet_games::unit_of_measurement%]"
},
"classical_rating": {
"name": "Classical rating"
},
"rapid_games": {
"name": "Rapid games",
"unit_of_measurement": "[%key:component::lichess::entity::sensor::bullet_games::unit_of_measurement%]"
},
"rapid_rating": {
"name": "Rapid rating"
}
}
}
}

View File

@@ -388,6 +388,7 @@ FLOWS = {
"lg_soundbar",
"lg_thinq",
"libre_hardware_monitor",
"lichess",
"lidarr",
"liebherr",
"lifx",

View File

@@ -3683,6 +3683,12 @@
"config_flow": true,
"iot_class": "local_polling"
},
"lichess": {
"name": "Lichess",
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling"
},
"lidarr": {
"name": "Lidarr",
"integration_type": "service",

3
requirements_all.txt generated
View File

@@ -305,6 +305,9 @@ aiokef==0.2.16
# homeassistant.components.rehlko
aiokem==1.0.1
# homeassistant.components.lichess
aiolichess==1.2.0
# homeassistant.components.lifx
aiolifx-effects==0.3.2

View File

@@ -290,6 +290,9 @@ aiokafka==0.10.0
# homeassistant.components.rehlko
aiokem==1.0.1
# homeassistant.components.lichess
aiolichess==1.2.0
# homeassistant.components.lifx
aiolifx-effects==0.3.2

View File

@@ -0,0 +1,12 @@
"""Tests for the Lichess integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Set up the Lichess integration for testing."""
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,68 @@
"""Common fixtures for the Lichess tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from aiolichess.models import LichessStatistics, LichessUser
import pytest
from homeassistant.components.lichess.const import DOMAIN
from homeassistant.const import CONF_API_TOKEN
from tests.common import MockConfigEntry
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.lichess.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="DrNykterstein",
unique_id="drnykterstien",
data={CONF_API_TOKEN: "my_secret_token"},
)
@pytest.fixture
def mock_lichess_client() -> Generator[AsyncMock]:
"""Mock Lichess client."""
with (
patch(
"homeassistant.components.lichess.coordinator.AioLichess",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.lichess.config_flow.AioLichess",
new=mock_client,
),
):
client = mock_client.return_value
client.get_all.return_value = LichessUser(
id="drnykterstien",
username="DrNykterstein",
url="https://lichess.org/@/DrNykterstein",
created_at=1420502920988,
seen_at=1747342929853,
play_time=999999,
)
client.get_user_id.return_value = "drnykterstien"
client.get_statistics.return_value = LichessStatistics(
blitz_rating=944,
rapid_rating=1050,
bullet_rating=1373,
classical_rating=888,
blitz_games=31,
rapid_games=324,
bullet_games=7,
classical_games=1,
)
yield client

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(
'lichess',
'drnykterstien',
),
}),
'labels': set({
}),
'manufacturer': 'Lichess',
'model': None,
'model_id': None,
'name': 'DrNykterstein',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---

View File

@@ -0,0 +1,429 @@
# serializer version: 1
# name: test_all_entities[sensor.drnykterstein_blitz_games-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.drnykterstein_blitz_games',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Blitz games',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Blitz games',
'platform': 'lichess',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'blitz_games',
'unique_id': 'drnykterstien.blitz_games',
'unit_of_measurement': 'games',
})
# ---
# name: test_all_entities[sensor.drnykterstein_blitz_games-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'DrNykterstein Blitz games',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'games',
}),
'context': <ANY>,
'entity_id': 'sensor.drnykterstein_blitz_games',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '31',
})
# ---
# name: test_all_entities[sensor.drnykterstein_blitz_rating-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.drnykterstein_blitz_rating',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Blitz rating',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Blitz rating',
'platform': 'lichess',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'blitz_rating',
'unique_id': 'drnykterstien.blitz_rating',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sensor.drnykterstein_blitz_rating-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'DrNykterstein Blitz rating',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.drnykterstein_blitz_rating',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '944',
})
# ---
# name: test_all_entities[sensor.drnykterstein_bullet_games-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.drnykterstein_bullet_games',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Bullet games',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Bullet games',
'platform': 'lichess',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'bullet_games',
'unique_id': 'drnykterstien.bullet_games',
'unit_of_measurement': 'games',
})
# ---
# name: test_all_entities[sensor.drnykterstein_bullet_games-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'DrNykterstein Bullet games',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'games',
}),
'context': <ANY>,
'entity_id': 'sensor.drnykterstein_bullet_games',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '7',
})
# ---
# name: test_all_entities[sensor.drnykterstein_bullet_rating-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.drnykterstein_bullet_rating',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Bullet rating',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Bullet rating',
'platform': 'lichess',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'bullet_rating',
'unique_id': 'drnykterstien.bullet_rating',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sensor.drnykterstein_bullet_rating-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'DrNykterstein Bullet rating',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.drnykterstein_bullet_rating',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1373',
})
# ---
# name: test_all_entities[sensor.drnykterstein_classical_games-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.drnykterstein_classical_games',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Classical games',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Classical games',
'platform': 'lichess',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'classical_games',
'unique_id': 'drnykterstien.classical_games',
'unit_of_measurement': 'games',
})
# ---
# name: test_all_entities[sensor.drnykterstein_classical_games-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'DrNykterstein Classical games',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'games',
}),
'context': <ANY>,
'entity_id': 'sensor.drnykterstein_classical_games',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---
# name: test_all_entities[sensor.drnykterstein_classical_rating-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.drnykterstein_classical_rating',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Classical rating',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Classical rating',
'platform': 'lichess',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'classical_rating',
'unique_id': 'drnykterstien.classical_rating',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sensor.drnykterstein_classical_rating-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'DrNykterstein Classical rating',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.drnykterstein_classical_rating',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '888',
})
# ---
# name: test_all_entities[sensor.drnykterstein_rapid_games-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.drnykterstein_rapid_games',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Rapid games',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Rapid games',
'platform': 'lichess',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'rapid_games',
'unique_id': 'drnykterstien.rapid_games',
'unit_of_measurement': 'games',
})
# ---
# name: test_all_entities[sensor.drnykterstein_rapid_games-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'DrNykterstein Rapid games',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'games',
}),
'context': <ANY>,
'entity_id': 'sensor.drnykterstein_rapid_games',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '324',
})
# ---
# name: test_all_entities[sensor.drnykterstein_rapid_rating-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.drnykterstein_rapid_rating',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Rapid rating',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Rapid rating',
'platform': 'lichess',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'rapid_rating',
'unique_id': 'drnykterstien.rapid_rating',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sensor.drnykterstein_rapid_rating-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'DrNykterstein Rapid rating',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.drnykterstein_rapid_rating',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1050',
})
# ---

View File

@@ -0,0 +1,92 @@
"""Test the Lichess config flow."""
from unittest.mock import AsyncMock
from aiolichess.exceptions import AioLichessError, AuthError
import pytest
from homeassistant.components.lichess.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_lichess_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_API_TOKEN: "my_secret_token"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "DrNykterstein"
assert result["data"] == {CONF_API_TOKEN: "my_secret_token"}
assert result["result"].unique_id == "drnykterstien"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("exception", "error"),
[
(AuthError, "invalid_auth"),
(AioLichessError, "cannot_connect"),
(Exception, "unknown"),
],
)
async def test_form_errors(
hass: HomeAssistant,
mock_lichess_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_lichess_client.get_all.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_TOKEN: "my_secret_token"}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error}
mock_lichess_client.get_all.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_TOKEN: "my_secret_token"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_lichess_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_API_TOKEN: "my_secret_token"}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@@ -0,0 +1,43 @@
"""Test the Lichess initialization."""
from unittest.mock import AsyncMock
from aiolichess.exceptions import AioLichessError
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.lichess.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
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_lichess_client: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Lichess device."""
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device({(DOMAIN, "drnykterstien")})
assert device
assert device == snapshot
async def test_setup_entry_failed(
hass: HomeAssistant,
mock_lichess_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setup fails when API raises an error."""
mock_lichess_client.get_statistics.side_effect = AioLichessError
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -0,0 +1,28 @@
"""Tests for the Lichess 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_lichess_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch("homeassistant.components.lichess._PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)