mirror of
https://github.com/home-assistant/core.git
synced 2026-03-15 07:22:12 +01:00
Compare commits
8 Commits
fnv_hash_f
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56aa96a00c | ||
|
|
99c6cdbe44 | ||
|
|
1fd30b73e7 | ||
|
|
14aace0c00 | ||
|
|
6eed18623b | ||
|
|
66ca7d5782 | ||
|
|
a7436cbdc3 | ||
|
|
5e57b0272d |
22
homeassistant/components/chess_com/diagnostics.py
Normal file
22
homeassistant/components/chess_com/diagnostics.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Diagnostics support for Chess.com."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import ChessConfigEntry
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ChessConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
"player": asdict(coordinator.data.player),
|
||||
"stats": asdict(coordinator.data.stats),
|
||||
}
|
||||
@@ -41,7 +41,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Can't detect a game
|
||||
|
||||
@@ -46,10 +46,12 @@ PROGRAM_OPTIONS = {
|
||||
value,
|
||||
)
|
||||
for key, value in {
|
||||
OptionKey.BSH_COMMON_DURATION: int,
|
||||
OptionKey.BSH_COMMON_START_IN_RELATIVE: int,
|
||||
OptionKey.BSH_COMMON_FINISH_IN_RELATIVE: int,
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FILL_QUANTITY: int,
|
||||
OptionKey.BSH_COMMON_DURATION: vol.All(int, vol.Range(min=0)),
|
||||
OptionKey.BSH_COMMON_START_IN_RELATIVE: vol.All(int, vol.Range(min=0)),
|
||||
OptionKey.BSH_COMMON_FINISH_IN_RELATIVE: vol.All(int, vol.Range(min=0)),
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FILL_QUANTITY: vol.All(
|
||||
int, vol.Range(min=0)
|
||||
),
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_MULTIPLE_BEVERAGES: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_INTENSIV_ZONE: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_BRILLIANCE_DRY: bool,
|
||||
@@ -60,7 +62,10 @@ PROGRAM_OPTIONS = {
|
||||
OptionKey.DISHCARE_DISHWASHER_HYGIENE_PLUS: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_ECO_DRY: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_ZEOLITE_DRY: bool,
|
||||
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE: int,
|
||||
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_FAN_SPEED_PERCENTAGE: vol.All(
|
||||
int, vol.Range(min=1, max=100)
|
||||
),
|
||||
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE: vol.All(int, vol.Range(min=0)),
|
||||
OptionKey.COOKING_OVEN_FAST_PRE_HEAT: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_2_ACTIVE: bool,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"loggers": ["pyhap"],
|
||||
"requirements": [
|
||||
"HAP-python==5.0.0",
|
||||
"fnv-hash-fast==2.0.0",
|
||||
"fnv-hash-fast==1.6.0",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"SQLAlchemy==2.0.41",
|
||||
"fnv-hash-fast==2.0.0",
|
||||
"fnv-hash-fast==1.6.0",
|
||||
"psutil-home-assistant==0.0.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import TRMNLConfigEntry, TRMNLCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TRMNLConfigEntry) -> bool:
|
||||
|
||||
@@ -2,18 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from trmnl import TRMNLClient
|
||||
from trmnl.exceptions import TRMNLAuthenticationError, TRMNLError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
STEP_USER_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
|
||||
|
||||
class TRMNLConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""TRMNL config flow."""
|
||||
@@ -21,7 +24,7 @@ class TRMNLConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
"""Handle a flow initialized by the user or reauth."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
session = async_get_clientsession(self.hass)
|
||||
@@ -37,6 +40,12 @@ class TRMNLConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(str(user.identifier))
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=user.name,
|
||||
@@ -44,6 +53,12 @@ class TRMNLConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
|
||||
data_schema=STEP_USER_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -54,4 +55,15 @@ class TRMNLCoordinator(DataUpdateCoordinator[dict[int, Device]]):
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
return {device.identifier: device for device in devices}
|
||||
new_data = {device.identifier: device for device in devices}
|
||||
if self.data is not None:
|
||||
device_registry = dr.async_get(self.hass)
|
||||
for device_id in set(self.data) - set(new_data):
|
||||
if entry := device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, str(device_id))}
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device_id=entry.id,
|
||||
remove_config_entry_id=self.config_entry.entry_id,
|
||||
)
|
||||
return new_data
|
||||
|
||||
25
homeassistant/components/trmnl/diagnostics.py
Normal file
25
homeassistant/components/trmnl/diagnostics.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Diagnostics support for TRMNL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import TRMNLConfigEntry
|
||||
|
||||
TO_REDACT = {"mac_address"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: TRMNLConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"data": [
|
||||
async_redact_data(asdict(device), TO_REDACT)
|
||||
for device in entry.runtime_data.data.values()
|
||||
],
|
||||
}
|
||||
@@ -7,6 +7,7 @@ from trmnl.models import Device
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TRMNLCoordinator
|
||||
|
||||
|
||||
@@ -22,6 +23,7 @@ class TRMNLEntity(CoordinatorEntity[TRMNLCoordinator]):
|
||||
device = self._device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, device.mac_address)},
|
||||
identifiers={(DOMAIN, str(device_id))},
|
||||
name=device.name,
|
||||
manufacturer="TRMNL",
|
||||
)
|
||||
|
||||
12
homeassistant/components/trmnl/icons.json
Normal file
12
homeassistant/components/trmnl/icons.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"sleep_mode": {
|
||||
"default": "mdi:sleep-off",
|
||||
"state": {
|
||||
"on": "mdi:sleep"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,12 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Uses the cloud API
|
||||
@@ -66,7 +66,7 @@ rules:
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: There are no repairable issues
|
||||
stale-devices: todo
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unique_id_mismatch": "The API key belongs to a different account. Please use the API key for the original account."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -19,6 +21,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"sleep_mode": {
|
||||
"name": "Sleep mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"authentication_error": {
|
||||
"message": "Authentication failed. Please check your API key."
|
||||
|
||||
91
homeassistant/components/trmnl/switch.py
Normal file
91
homeassistant/components/trmnl/switch.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Support for TRMNL switch entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from trmnl.models import Device
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import TRMNLConfigEntry
|
||||
from .coordinator import TRMNLCoordinator
|
||||
from .entity import TRMNLEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TRMNLSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes a TRMNL switch entity."""
|
||||
|
||||
value_fn: Callable[[Device], bool]
|
||||
set_value_fn: Callable[[TRMNLCoordinator, int, bool], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
SWITCH_DESCRIPTIONS: tuple[TRMNLSwitchEntityDescription, ...] = (
|
||||
TRMNLSwitchEntityDescription(
|
||||
key="sleep_mode",
|
||||
translation_key="sleep_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda device: device.sleep_mode_enabled,
|
||||
set_value_fn=lambda coordinator, device_id, value: (
|
||||
coordinator.client.update_device(device_id, sleep_mode_enabled=value)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: TRMNLConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up TRMNL switch entities based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
TRMNLSwitchEntity(coordinator, device_id, description)
|
||||
for device_id in coordinator.data
|
||||
for description in SWITCH_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class TRMNLSwitchEntity(TRMNLEntity, SwitchEntity):
|
||||
"""Defines a TRMNL switch entity."""
|
||||
|
||||
entity_description: TRMNLSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TRMNLCoordinator,
|
||||
device_id: int,
|
||||
description: TRMNLSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize TRMNL switch entity."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if sleep mode is enabled."""
|
||||
return self.entity_description.value_fn(self._device)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Enable sleep mode."""
|
||||
await self.entity_description.set_value_fn(
|
||||
self.coordinator, self._device_id, True
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Disable sleep mode."""
|
||||
await self.entity_description.set_value_fn(
|
||||
self.coordinator, self._device_id, False
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["unifi_access_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["py-unifi-access==1.0.0"]
|
||||
"requirements": ["py-unifi-access==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -72,6 +73,58 @@ class WaterFurnaceConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
if user_input is not None:
|
||||
username = user_input[CONF_USERNAME]
|
||||
password = user_input[CONF_PASSWORD]
|
||||
|
||||
client = WaterFurnace(username, password)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(client.login)
|
||||
except WFCredentialError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except WFException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error during reauthentication")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
# Treat no gwid as a connection failure
|
||||
if not errors and not client.gwid:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
if not errors:
|
||||
await self.async_set_unique_id(client.gwid)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
title=f"WaterFurnace {username}",
|
||||
data_updates={**reauth_entry.data, **user_input},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA,
|
||||
{CONF_USERNAME: reauth_entry.data[CONF_USERNAME]},
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle import from YAML configuration."""
|
||||
username = import_data[CONF_USERNAME]
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"cannot_connect": "Please verify your credentials.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "Unexpected error, please try again."
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "Unexpected error, please try again.",
|
||||
"wrong_account": "You must reauthenticate with the same WaterFurnace account that was originally configured."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to WaterFurnace service",
|
||||
@@ -12,6 +14,18 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::waterfurnace::config::step::user::data_description::password%]",
|
||||
"username": "[%key:component::waterfurnace::config::step::user::data_description::username%]"
|
||||
},
|
||||
"description": "Please re-enter your WaterFurnace Symphony account credentials.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -33,7 +33,7 @@ cronsim==2.7
|
||||
cryptography==46.0.5
|
||||
dbus-fast==3.1.2
|
||||
file-read-backwards==2.0.0
|
||||
fnv-hash-fast==2.0.0
|
||||
fnv-hash-fast==1.6.0
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==5.9.1
|
||||
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"certifi>=2021.5.30",
|
||||
"ciso8601==2.3.3",
|
||||
"cronsim==2.7",
|
||||
"fnv-hash-fast==2.0.0",
|
||||
"fnv-hash-fast==1.6.0",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==2.0.0",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -23,7 +23,7 @@ certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
cryptography==46.0.5
|
||||
fnv-hash-fast==2.0.0
|
||||
fnv-hash-fast==1.6.0
|
||||
ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==2.0.0
|
||||
hassil==3.5.0
|
||||
|
||||
4
requirements_all.txt
generated
4
requirements_all.txt
generated
@@ -997,7 +997,7 @@ flux-led==1.2.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
# homeassistant.components.recorder
|
||||
fnv-hash-fast==2.0.0
|
||||
fnv-hash-fast==1.6.0
|
||||
|
||||
# homeassistant.components.foobot
|
||||
foobot_async==1.0.0
|
||||
@@ -1883,7 +1883,7 @@ py-sucks==0.9.11
|
||||
py-synologydsm-api==2.7.3
|
||||
|
||||
# homeassistant.components.unifi_access
|
||||
py-unifi-access==1.0.0
|
||||
py-unifi-access==1.1.0
|
||||
|
||||
# homeassistant.components.atome
|
||||
pyAtome==0.1.1
|
||||
|
||||
4
requirements_test_all.txt
generated
4
requirements_test_all.txt
generated
@@ -882,7 +882,7 @@ flux-led==1.2.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
# homeassistant.components.recorder
|
||||
fnv-hash-fast==2.0.0
|
||||
fnv-hash-fast==1.6.0
|
||||
|
||||
# homeassistant.components.foobot
|
||||
foobot_async==1.0.0
|
||||
@@ -1632,7 +1632,7 @@ py-sucks==0.9.11
|
||||
py-synologydsm-api==2.7.3
|
||||
|
||||
# homeassistant.components.unifi_access
|
||||
py-unifi-access==1.0.0
|
||||
py-unifi-access==1.1.0
|
||||
|
||||
# homeassistant.components.hdmi_cec
|
||||
pyCEC==0.5.2
|
||||
|
||||
59
tests/components/chess_com/snapshots/test_diagnostics.ambr
Normal file
59
tests/components/chess_com/snapshots/test_diagnostics.ambr
Normal file
@@ -0,0 +1,59 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'player': dict({
|
||||
'_country': None,
|
||||
'avatar': 'https://images.chesscomfiles.com/uploads/v1/user/532748851.d5fefa92.200x200o.da2274e46acd.jpg',
|
||||
'country_url': 'https://api.chess.com/pub/country/NL',
|
||||
'fide': None,
|
||||
'followers': 2,
|
||||
'is_streamer': False,
|
||||
'joined': '2026-02-20T10:48:14',
|
||||
'last_online': '2026-03-06T12:32:59',
|
||||
'location': 'Utrecht',
|
||||
'name': 'Joost',
|
||||
'player_id': 532748851,
|
||||
'status': 'basic',
|
||||
'title': None,
|
||||
'twitch_url': None,
|
||||
'username': 'joostlek',
|
||||
}),
|
||||
'stats': dict({
|
||||
'chess960_daily': None,
|
||||
'chess_blitz': None,
|
||||
'chess_bullet': None,
|
||||
'chess_daily': dict({
|
||||
'last': dict({
|
||||
'date': 1772800350,
|
||||
'rating': 495,
|
||||
'rd': 196,
|
||||
}),
|
||||
'record': dict({
|
||||
'draw': 0,
|
||||
'loss': 4,
|
||||
'time_per_move': 6974,
|
||||
'timeout_percent': 0,
|
||||
'win': 0,
|
||||
}),
|
||||
}),
|
||||
'chess_rapid': None,
|
||||
'lessons': None,
|
||||
'puzzle_rush': dict({
|
||||
'best': dict({
|
||||
'score': 8,
|
||||
'total_attempts': 11,
|
||||
}),
|
||||
}),
|
||||
'tactics': dict({
|
||||
'highest': dict({
|
||||
'date': 1772782351,
|
||||
'rating': 764,
|
||||
}),
|
||||
'lowest': dict({
|
||||
'date': 1771584762,
|
||||
'rating': 400,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
29
tests/components/chess_com/test_diagnostics.py
Normal file
29
tests/components/chess_com/test_diagnostics.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Tests for the Chess.com diagnostics."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_chess_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
|
||||
== snapshot
|
||||
)
|
||||
20
tests/components/trmnl/snapshots/test_diagnostics.ambr
Normal file
20
tests/components/trmnl/snapshots/test_diagnostics.ambr
Normal file
@@ -0,0 +1,20 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'data': list([
|
||||
dict({
|
||||
'battery_voltage': 3.87,
|
||||
'friendly_id': '1RJXS4',
|
||||
'identifier': 42793,
|
||||
'mac_address': '**REDACTED**',
|
||||
'name': 'Test TRMNL',
|
||||
'percent_charged': 72.5,
|
||||
'rssi': -64,
|
||||
'sleep_end_time': 480,
|
||||
'sleep_mode_enabled': False,
|
||||
'sleep_start_time': 1320,
|
||||
'wifi_strength': 50,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
@@ -16,6 +16,10 @@
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'trmnl',
|
||||
'42793',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
|
||||
50
tests/components/trmnl/snapshots/test_switch.ambr
Normal file
50
tests/components/trmnl/snapshots/test_switch.ambr
Normal file
@@ -0,0 +1,50 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[switch.test_trmnl_sleep_mode-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.test_trmnl_sleep_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Sleep mode',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Sleep mode',
|
||||
'platform': 'trmnl',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'sleep_mode',
|
||||
'unique_id': '42793_sleep_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[switch.test_trmnl_sleep_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test TRMNL Sleep mode',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.test_trmnl_sleep_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
@@ -90,3 +90,83 @@ async def test_duplicate_entry(
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_reauth_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_trmnl_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the reauth flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: "user_bbbbbbbbbb"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.data == {CONF_API_KEY: "user_bbbbbbbbbb"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(TRMNLAuthenticationError, "invalid_auth"),
|
||||
(TRMNLError, "cannot_connect"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_reauth_flow_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_trmnl_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: type[Exception],
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test reauth flow error handling."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
mock_trmnl_client.get_me.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: "user_bbbbbbbbbb"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_trmnl_client.get_me.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: "user_bbbbbbbbbb"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
async def test_reauth_flow_wrong_account(
|
||||
hass: HomeAssistant,
|
||||
mock_trmnl_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reauth aborts when the API key belongs to a different account."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
mock_trmnl_client.get_me.return_value.identifier = 99999
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: "user_cccccccccc"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "unique_id_mismatch"
|
||||
|
||||
29
tests/components/trmnl/test_diagnostics.py
Normal file
29
tests/components/trmnl/test_diagnostics.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Tests for the TRMNL diagnostics."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_trmnl_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
|
||||
== snapshot
|
||||
)
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Test the TRMNL initialization."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -11,7 +13,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_load_unload_entry(
|
||||
@@ -45,3 +47,27 @@ async def test_device(
|
||||
)
|
||||
assert device
|
||||
assert device == snapshot
|
||||
|
||||
|
||||
async def test_stale_device_removed(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_trmnl_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that a device is removed from the device registry when it disappears."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert device_registry.async_get_device(
|
||||
connections={(CONNECTION_NETWORK_MAC, "B0:A6:04:AA:BB:CC")}
|
||||
)
|
||||
|
||||
mock_trmnl_client.get_devices.return_value = []
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not device_registry.async_get_device(
|
||||
connections={(CONNECTION_NETWORK_MAC, "B0:A6:04:AA:BB:CC")}
|
||||
)
|
||||
|
||||
60
tests/components/trmnl/test_switch.py
Normal file
60
tests/components/trmnl/test_switch.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Tests for the TRMNL switch platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.const import ATTR_ENTITY_ID, 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
|
||||
|
||||
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_trmnl_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all switch entities."""
|
||||
with patch("homeassistant.components.trmnl.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "expected_value"),
|
||||
[
|
||||
(SERVICE_TURN_ON, True),
|
||||
(SERVICE_TURN_OFF, False),
|
||||
],
|
||||
)
|
||||
async def test_set_switch(
|
||||
hass: HomeAssistant,
|
||||
mock_trmnl_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service: str,
|
||||
expected_value: bool,
|
||||
) -> None:
|
||||
"""Test turning the sleep mode switch on and off."""
|
||||
with patch("homeassistant.components.trmnl.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "switch.test_trmnl_sleep_mode"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_trmnl_client.update_device.assert_called_once_with(
|
||||
42793, sleep_mode_enabled=expected_value
|
||||
)
|
||||
assert mock_trmnl_client.get_devices.call_count == 2
|
||||
@@ -222,3 +222,114 @@ async def test_import_flow_no_gwid(
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_reauth_flow_success(
|
||||
hass: HomeAssistant,
|
||||
mock_waterfurnace_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test successful reauth flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "new_user", CONF_PASSWORD: "new_password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.title == "WaterFurnace new_user"
|
||||
assert mock_config_entry.data[CONF_USERNAME] == "new_user"
|
||||
assert mock_config_entry.data[CONF_PASSWORD] == "new_password"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(WFCredentialError("Invalid credentials"), "invalid_auth"),
|
||||
(WFException("Connection failed"), "cannot_connect"),
|
||||
(Exception("Unexpected error"), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_reauth_flow_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_waterfurnace_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: AsyncMock,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test reauth flow with errors and recovery."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
mock_waterfurnace_client.login.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "test_user", CONF_PASSWORD: "bad_password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_waterfurnace_client.login.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "test_user", CONF_PASSWORD: "new_password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
async def test_reauth_flow_wrong_account(
|
||||
hass: HomeAssistant,
|
||||
mock_waterfurnace_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow aborts when a different account is used."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
mock_waterfurnace_client.gwid = "DIFFERENT_GWID"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "other_user", CONF_PASSWORD: "other_password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "wrong_account"
|
||||
|
||||
|
||||
async def test_reauth_flow_no_gwid(
|
||||
hass: HomeAssistant,
|
||||
mock_waterfurnace_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow when no GWID is returned."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
mock_waterfurnace_client.gwid = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "test_user", CONF_PASSWORD: "new_password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
Reference in New Issue
Block a user