Compare commits

...

19 Commits

Author SHA1 Message Date
Franck Nijhof 9a10c8ed3a Bump roborock dependencies 2026-06-14 18:25:19 +00:00
iluebbe b3309ef169 Add Powerline hint to username field description (#167473)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 16:57:21 +02:00
epenet caaf5f9715 Adjust pylint checker to prevent invalid use of Platform enum (#173374)
Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
2026-06-14 16:17:00 +02:00
BrettLynch123 7ce7de3650 Fix tessie setup_error on transient aiohttp.ClientError during startup (#173659) 2026-06-14 15:49:22 +02:00
fdebrus 2c14c6be75 Optimistic UI updates for Vistapool write entities (#173373)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-14 15:41:57 +02:00
Christian Lackas e020f338ab Add window state sensor for HomematicIP rotary handle (HmIP-SRH) (#173423) 2026-06-14 15:36:15 +02:00
jasonjhofmann c85c2c4cd3 Add network MAC connection to JVC Projector device (#173683)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:56:29 +02:00
Raman Gupta c4e618e990 Stop validating # of slots in zwave_js.set_credential action (#173644) 2026-06-14 14:18:32 +02:00
Åke Strandberg 5efde60d21 Remove unnecessary #pylint disable..." (#173726) 2026-06-14 14:17:16 +02:00
jasonjhofmann d9dc10ed81 Add network MAC connection to myStrom bulb devices (#173707)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:16:50 +02:00
Onero-testdev cb6ae03d21 Register SwitchBot Standing Fan device (#173577)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 13:41:17 +02:00
Allen Porter 915b78473c Improve Rainbird config flow test coverage (#173703) 2026-06-14 13:35:54 +02:00
Vincent Wolsink 559006ba19 Adjust humidity attributes to (mandatory) new controller firmware in Huum (#173702) 2026-06-14 13:22:35 +02:00
Thomas D bad2eed9fe Bump qbusmqttapi to v1.5.1 for the Qbus integration (#173714) 2026-06-14 13:14:58 +02:00
Sid 9f1a079688 Bump eheimdigital to 1.7.0 (#173716) 2026-06-14 13:00:52 +02:00
Bipin Kumar 965a96b957 Add Dry and Fan-Only modes to Panasonic CS-CU-EZ18CKYXFM AC in Matter (#173709) 2026-06-14 12:40:37 +02:00
Manu d5791ae8b4 Remove cleanup code for removed entities from Xbox integration (#173688) 2026-06-14 10:51:33 +02:00
J. Nick Koston 7b561934ea Bump aiodiscover to 3.3.2 (#173705) 2026-06-13 22:44:47 -05:00
Franck Nijhof cf60690fb7 Skip building ZHA entity log messages when the level is disabled (#173695) 2026-06-13 22:15:47 +02:00
49 changed files with 560 additions and 342 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.7",
"aiodiscover==3.3.1",
"aiodiscover==3.3.2",
"cached-ipaddress==1.1.2"
]
}
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["eheimdigital"],
"quality_scale": "platinum",
"requirements": ["eheimdigital==1.6.0"],
"requirements": ["eheimdigital==1.7.0"],
"zeroconf": [
{ "name": "eheimdigital._http._tcp.local.", "type": "_http._tcp.local." }
]
+1 -1
View File
@@ -5,7 +5,7 @@
"data_description_password": "Password for the FRITZ!Box.",
"data_description_port": "Leave empty to use the default port.",
"data_description_ssl": "Use SSL to connect to the FRITZ!Box.",
"data_description_username": "Username for the FRITZ!Box.",
"data_description_username": "Username for the FRITZ!Box. FRITZ!Powerline devices ignore this information and accept any value.",
"data_feature_device_tracking": "Enable network device tracking"
},
"config": {
@@ -27,6 +27,7 @@ from homematicip.device import (
PassageDetector,
PresenceDetectorIndoor,
RoomControlDeviceAnalog,
RotaryHandleSensor,
SmokeDetector,
SoilMoistureSensorInterface,
SwitchMeasuring,
@@ -166,6 +167,7 @@ ILLUMINATION_DEVICE_ATTRIBUTES = {
}
TILT_STATE_VALUES = ["neutral", "tilted", "non_neutral"]
WINDOW_STATE_VALUES = ["open", "closed", "tilted"]
def get_device_handlers(hap: HomematicipHAP) -> dict[type, Callable]:
@@ -204,6 +206,9 @@ def get_device_handlers(hap: HomematicipHAP) -> dict[type, Callable]:
RoomControlDeviceAnalog: lambda device: [
HomematicipTemperatureSensor(hap, device),
],
RotaryHandleSensor: lambda device: [
HomematicipWindowStateSensor(hap, device),
],
LightSensor: lambda device: [
HomematicipIlluminanceSensor(hap, device),
],
@@ -498,6 +503,24 @@ class HomematicipTiltStateSensor(HomematicipGenericEntity, SensorEntity):
return state_attr
class HomematicipWindowStateSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP rotary handle window state sensor."""
_attr_device_class = SensorDeviceClass.ENUM
_attr_options = WINDOW_STATE_VALUES
_attr_translation_key = "window_state"
def __init__(self, hap: HomematicipHAP, device: RotaryHandleSensor) -> None:
"""Initialize the window state sensor."""
super().__init__(hap, device, feature_id="window_state")
@property
def native_value(self) -> str | None:
"""Return the state."""
window_state = getattr(self._device, "windowState", None)
return window_state.lower() if window_state is not None else None
class HomematicipFloorTerminalBlockMechanicChannelValve(
HomematicipGenericEntity, SensorEntity
):
@@ -98,6 +98,14 @@
"non_neutral": "Non-neutral",
"tilted": "Tilted"
}
},
"window_state": {
"name": "Window state",
"state": {
"closed": "[%key:common::state::closed%]",
"open": "[%key:common::state::open%]",
"tilted": "Tilted"
}
}
}
},
+5 -3
View File
@@ -4,7 +4,7 @@ import logging
from huum.const import SaunaStatus
from homeassistant.components.number import NumberEntity
from homeassistant.components.number import NumberDeviceClass, NumberEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -34,7 +34,9 @@ class HuumSteamer(HuumBaseEntity, NumberEntity):
"""Representation of a steamer."""
_attr_translation_key = "humidity"
_attr_native_max_value = 10
_attr_device_class = NumberDeviceClass.HUMIDITY
_attr_native_unit_of_measurement = "%"
_attr_native_max_value = 40
_attr_native_min_value = 0
_attr_native_step = 1
@@ -47,7 +49,7 @@ class HuumSteamer(HuumBaseEntity, NumberEntity):
@property
def native_value(self) -> float | None:
"""Return the current value."""
return self.coordinator.data.target_humidity
return self.coordinator.data.humidity
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
@@ -4,7 +4,7 @@ import logging
from jvcprojector import Command, JvcProjector
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER, NAME
@@ -27,8 +27,12 @@ class JvcProjectorEntity(CoordinatorEntity[JvcProjectorDataUpdateCoordinator]):
super().__init__(coordinator, command)
self._attr_unique_id = coordinator.unique_id
# The config entry unique id is the device's formatted MAC address (set
# from the projector's MAC in the config flow), so it doubles as the
# network MAC connection.
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
connections={(CONNECTION_NETWORK_MAC, self._attr_unique_id)},
name=NAME,
model=self.device.model,
manufacturer=MANUFACTURER,
@@ -99,6 +99,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = {
# support dry mode.
(0x0001, 0x0108),
(0x0001, 0x010A),
(0x0001, 0x013F),
(0x1209, 0x8000),
(0x1209, 0x8001),
(0x1209, 0x8002),
@@ -138,6 +139,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
# support fan-only mode.
(0x0001, 0x0108),
(0x0001, 0x010A),
(0x0001, 0x013F),
(0x118C, 0x2022),
(0x1209, 0x8000),
(0x1209, 0x8001),
-2
View File
@@ -165,7 +165,6 @@ class MieleFan(MieleEntity, FanEntity):
try:
await self.api.send_action(self._device_id, {POWER_ON: True})
except ClientResponseError as ex:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_state_error",
@@ -183,7 +182,6 @@ class MieleFan(MieleEntity, FanEntity):
try:
await self.api.send_action(self._device_id, {POWER_OFF: True})
except ClientResponseError as ex:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_state_error",
+2 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.components.light import (
LightEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MANUFACTURER
@@ -57,6 +57,7 @@ class MyStromLight(LightEntity):
self._attr_hs_color = 0, 0
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, mac)},
connections={(CONNECTION_NETWORK_MAC, mac)},
name=name,
manufacturer=MANUFACTURER,
sw_version=self._bulb.firmware,
+1 -1
View File
@@ -14,5 +14,5 @@
"cloudapp/QBUSMQTTGW/+/state"
],
"quality_scale": "bronze",
"requirements": ["qbusmqttapi==1.5.0"]
"requirements": ["qbusmqttapi==1.5.1"]
}
@@ -34,11 +34,7 @@ rules:
docs-removal-instructions: todo
test-before-setup: done
docs-high-level-description: done
config-flow-test-coverage:
status: todo
comment: |
All config flow tests should finish with CREATE_ENTRY and ABORT to
test they are able to recover from errors
config-flow-test-coverage: done
docs-actions: done
runtime-data: done
@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==5.14.1",
"vacuum-map-parser-roborock==0.1.4"
"python-roborock==5.14.2",
"vacuum-map-parser-roborock==0.1.5"
]
}
@@ -96,6 +96,7 @@ PLATFORMS_BY_TYPE = {
],
SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR],
SupportedModels.CIRCULATOR_FAN.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.STANDING_FAN.value: [Platform.SENSOR],
SupportedModels.S10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.S20_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.K10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
@@ -207,6 +208,7 @@ CLASS_BY_DEVICE = {
SupportedModels.RELAY_SWITCH_1.value: switchbot.SwitchbotRelaySwitch,
SupportedModels.ROLLER_SHADE.value: switchbot.SwitchbotRollerShade,
SupportedModels.CIRCULATOR_FAN.value: switchbot.SwitchbotFan,
SupportedModels.STANDING_FAN.value: switchbot.SwitchbotStandingFan,
SupportedModels.S10_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.S20_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.K10_VACUUM.value: switchbot.SwitchbotVacuum,
@@ -71,6 +71,7 @@ class SupportedModels(StrEnum):
LOCK_VISION = "lock_vision"
LOCK_PRO_WIFI = "lock_pro_wifi"
WEATHER_STATION = "weather_station"
STANDING_FAN = "standing_fan"
CONNECTABLE_SUPPORTED_MODEL_TYPES = {
@@ -120,6 +121,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
SwitchbotModel.LOCK_VISION_PRO: SupportedModels.LOCK_VISION_PRO,
SwitchbotModel.LOCK_VISION: SupportedModels.LOCK_VISION,
SwitchbotModel.LOCK_PRO_WIFI: SupportedModels.LOCK_PRO_WIFI,
SwitchbotModel.STANDING_FAN: SupportedModels.STANDING_FAN,
}
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
+8 -3
View File
@@ -3,6 +3,7 @@
import asyncio
import logging
from aiohttp import ClientError
from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import (
Forbidden,
@@ -81,6 +82,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from e
except ClientError as e:
raise ConfigEntryNotReady from e
vehicles: list[TessieVehicleData] = []
for vehicle in state_of_all_vehicles["results"]:
@@ -124,13 +127,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
try:
scopes = await tessie.scopes()
except TeslaFleetError as e:
except (TeslaFleetError, ClientError) as e:
raise ConfigEntryNotReady from e
if Scope.ENERGY_DEVICE_DATA in scopes:
try:
products = (await tessie.products())["response"]
except TeslaFleetError as e:
except (TeslaFleetError, ClientError) as e:
raise ConfigEntryNotReady from e
for product in products:
@@ -154,7 +157,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
except (InvalidToken, Forbidden, SubscriptionRequired) as e:
raise ConfigEntryAuthFailed from e
except TeslaFleetError as e:
raise ConfigEntryNotReady(e.message) from e
raise ConfigEntryNotReady(getattr(e, "message", str(e))) from e
except ClientError as e:
raise ConfigEntryNotReady from e
powerwall = (
product["components"]["battery"] or product["components"]["solar"]
+1 -4
View File
@@ -67,7 +67,4 @@ class VistapoolLEDPulseButton(VistapoolEntity, ButtonEntity):
translation_key="set_failed",
translation_placeholders={"entity": self.entity_id},
) from err
# Optimistically reflect the just-written value so a rapid second press
# doesn't read the stale off-state before the Firestore push round-trips.
self.coordinator.data.setdefault("light", {})["status"] = 1
self.coordinator.async_set_updated_data(self.coordinator.data)
self.coordinator.apply_optimistic(_LIGHT_STATUS_PATH, 1)
@@ -81,3 +81,22 @@ class VistapoolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
def get_value(self, path: str, default: Any = None) -> Any:
"""Get nested data using dot-notation path."""
return AquariteClient.get_value(self.data, path, default)
def apply_optimistic(self, value_path: str, value: Any) -> None:
"""Reflect a just-written value before the Firestore push round-trips.
Hayward's cloud takes several seconds to acknowledge a write back
through Firestore, which would make the UI feel laggy. Writing into
coordinator.data after a successful REST call gives entities instant
feedback; the next snapshot from Firestore overwrites it harmlessly.
"""
keys = value_path.split(".")
target: dict[str, Any] = self.data
for key in keys[:-1]:
child = target.get(key)
if not isinstance(child, dict):
child = {}
target[key] = child
target = child
target[keys[-1]] = value
self.async_set_updated_data(self.data)
@@ -71,3 +71,4 @@ class VistapoolLight(VistapoolEntity, LightEntity):
translation_key="set_failed",
translation_placeholders={"entity": self.entity_id},
) from err
self.coordinator.apply_optimistic(_VALUE_PATH, value)
@@ -233,3 +233,4 @@ class VistapoolNumber(VistapoolEntity, NumberEntity):
translation_key="set_failed",
translation_placeholders={"entity": self.entity_id},
) from err
self.coordinator.apply_optimistic(self.entity_description.value_path, raw)
+1 -25
View File
@@ -9,7 +9,6 @@ from pythonxbox.api.provider.people.models import Person
from pythonxbox.api.provider.titlehub.models import Title
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorEntity,
BinarySensorEntityDescription,
)
@@ -17,12 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import XboxConfigEntry
from .entity import (
XboxBaseEntity,
XboxBaseEntityDescription,
check_deprecated_entity,
profile_pic,
)
from .entity import XboxBaseEntity, XboxBaseEntityDescription, profile_pic
PARALLEL_UPDATES = 0
@@ -31,9 +25,7 @@ class XboxBinarySensor(StrEnum):
"""Xbox binary sensor."""
ONLINE = "online"
IN_PARTY = "in_party"
IN_GAME = "in_game"
IN_MULTIPLAYER = "in_multiplayer"
HAS_GAME_PASS = "has_game_pass"
@@ -81,21 +73,11 @@ SENSOR_DESCRIPTIONS: tuple[XboxBinarySensorEntityDescription, ...] = (
entity_picture_fn=profile_pic,
attributes_fn=profile_attributes,
),
XboxBinarySensorEntityDescription(
key=XboxBinarySensor.IN_PARTY,
is_on_fn=lambda _: None,
deprecated=True,
),
XboxBinarySensorEntityDescription(
key=XboxBinarySensor.IN_GAME,
translation_key=XboxBinarySensor.IN_GAME,
is_on_fn=in_game,
),
XboxBinarySensorEntityDescription(
key=XboxBinarySensor.IN_MULTIPLAYER,
is_on_fn=lambda _: None,
deprecated=True,
),
XboxBinarySensorEntityDescription(
key=XboxBinarySensor.HAS_GAME_PASS,
translation_key=XboxBinarySensor.HAS_GAME_PASS,
@@ -118,9 +100,6 @@ async def async_setup_entry(
[
XboxBinarySensorEntity(coordinator, entry.unique_id, description)
for description in SENSOR_DESCRIPTIONS
if check_deprecated_entity(
hass, entry.unique_id, description, BINARY_SENSOR_DOMAIN
)
]
)
@@ -130,9 +109,6 @@ async def async_setup_entry(
XboxBinarySensorEntity(coordinator, subentry.unique_id, description)
for description in SENSOR_DESCRIPTIONS
if subentry.unique_id
and check_deprecated_entity(
hass, subentry.unique_id, description, BINARY_SENSOR_DOMAIN
)
and subentry.unique_id in coordinator.data.presence
and subentry.subentry_type == "friend"
],
-23
View File
@@ -9,8 +9,6 @@ from pythonxbox.api.provider.smartglass.models import ConsoleType, SmartglassCon
from pythonxbox.api.provider.titlehub.models import Title
from yarl import URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -40,7 +38,6 @@ class XboxBaseEntityDescription(EntityDescription):
attributes_fn: Callable[[Person, Title | None], Mapping[str, Any] | None] | None = (
None
)
deprecated: bool | None = None
class XboxBaseEntity(CoordinatorEntity[XboxPresenceCoordinator]):
@@ -145,26 +142,6 @@ class XboxConsoleBaseEntity(CoordinatorEntity[XboxConsoleStatusCoordinator]):
return self.coordinator.data.get(self._console.id) is not None
def check_deprecated_entity(
hass: HomeAssistant,
xuid: str,
entity_description: XboxBaseEntityDescription,
entity_domain: str,
) -> bool:
"""Check for deprecated entity and remove it."""
if not entity_description.deprecated:
return True
ent_reg = er.async_get(hass)
if entity_id := ent_reg.async_get_entity_id(
entity_domain,
DOMAIN,
f"{xuid}_{entity_description.key}",
):
ent_reg.async_remove(entity_id)
return False
def to_https(image_url: str) -> str:
"""Convert image URLs to secure URLs."""
+1 -26
View File
@@ -11,7 +11,6 @@ from pythonxbox.api.provider.smartglass.models import SmartglassConsole, Storage
from pythonxbox.api.provider.titlehub.models import Title
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
EntityCategory,
SensorDeviceClass,
SensorEntity,
@@ -27,13 +26,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import XboxConfigEntry, XboxConsolesCoordinator
from .entity import (
MAP_MODEL,
XboxBaseEntity,
XboxBaseEntityDescription,
check_deprecated_entity,
to_https,
)
from .entity import MAP_MODEL, XboxBaseEntity, XboxBaseEntityDescription, to_https
PARALLEL_UPDATES = 0
@@ -58,8 +51,6 @@ class XboxSensor(StrEnum):
STATUS = "status"
GAMER_SCORE = "gamer_score"
ACCOUNT_TIER = "account_tier"
GOLD_TENURE = "gold_tenure"
LAST_ONLINE = "last_online"
FOLLOWING = "following"
FOLLOWER = "follower"
@@ -200,16 +191,6 @@ SENSOR_DESCRIPTIONS: tuple[XboxSensorEntityDescription, ...] = (
value_fn=lambda x, _: x.gamer_score,
state_class=SensorStateClass.MEASUREMENT,
),
XboxSensorEntityDescription(
key=XboxSensor.ACCOUNT_TIER,
value_fn=lambda _, __: None,
deprecated=True,
),
XboxSensorEntityDescription(
key=XboxSensor.GOLD_TENURE,
value_fn=lambda _, __: None,
deprecated=True,
),
XboxSensorEntityDescription(
key=XboxSensor.LAST_ONLINE,
translation_key=XboxSensor.LAST_ONLINE,
@@ -304,9 +285,6 @@ async def async_setup_entry(
[
XboxSensorEntity(presence, config_entry.unique_id, description)
for description in SENSOR_DESCRIPTIONS
if check_deprecated_entity(
hass, config_entry.unique_id, description, SENSOR_DOMAIN
)
]
)
for subentry_id, subentry in config_entry.subentries.items():
@@ -315,9 +293,6 @@ async def async_setup_entry(
XboxSensorEntity(presence, subentry.unique_id, description)
for description in SENSOR_DESCRIPTIONS
if subentry.unique_id
and check_deprecated_entity(
hass, subentry.unique_id, description, SENSOR_DOMAIN
)
and subentry.unique_id in presence.data.presence
and subentry.subentry_type == "friend"
],
+4
View File
@@ -214,6 +214,10 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
def log(self, level: int, msg: str, *args, **kwargs):
"""Log a message."""
if not _LOGGER.isEnabledFor(level):
# Avoid building the prefixed message and args tuple for disabled
# levels; this runs for every entity event via _handle_entity_events.
return
msg = f"%s: {msg}"
args = (self.entity_id, *args)
_LOGGER.log(level, msg, *args, **kwargs)
@@ -432,15 +432,6 @@ async def async_set_credential(
translation_key="no_available_credential_slots",
translation_placeholders={"credential_type": cred_type_str},
)
elif not 1 <= credential_slot <= type_cap.number_of_credential_slots:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="credential_slot_out_of_range",
translation_placeholders={
"credential_type": cred_type_str,
"max_slot": str(type_cap.number_of_credential_slots),
},
)
status = await node.access_control.set_credential(
user_id, credential_type, credential_slot, credential_data
@@ -322,9 +322,6 @@
"credential_rejected_wrong_uuid": {
"message": "The device rejected the credential because the user unique identifier does not match."
},
"credential_slot_out_of_range": {
"message": "Credential slot for {credential_type} must be between 1 and {max_slot}."
},
"credential_type_not_supported": {
"message": "Credential type {credential_type} is not supported on this device"
},
+1 -1
View File
@@ -1,7 +1,7 @@
# Automatically generated by gen_requirements_all.py, do not edit
aiodhcpwatcher==1.2.7
aiodiscover==3.3.1
aiodiscover==3.3.2
aiodns==4.0.4
aiogithubapi==26.0.0
aiohttp-asyncmdnsresolver==0.2.0
@@ -1,4 +1,8 @@
"""Plugin to encourage correct use of DOMAIN constants in tests."""
"""Plugin to prevent incorrect use of Platform enum in tests.
This plugin checks for common test helper functions and methods
where a domain is expected, and ensures the argument is not a Platform.
"""
from dataclasses import dataclass
@@ -30,9 +34,6 @@ _METHOD_CHECKS: list[tuple[str, str, ArgumentCheckInfo]] = [
("hass.states", "async_entity_ids", ArgumentCheckInfo(0, "domain_filter", True)),
]
_DOMAIN_CONSTANTS: set[str] = {"DOMAIN", "domain"}
_DOMAIN_SUFFIXES: tuple[str, ...] = ("_DOMAIN", "_domain")
def _check_call_node_domain_arguments(node: nodes.Call) -> nodes.NodeNG | None:
"""Ensure the call node arguments are valid domain constant or variable.
@@ -81,10 +82,14 @@ def _check_call_node_domain_argument(
return None
def _check_domain_argument(arg_node: nodes.NodeNG, allow_iterable: bool) -> bool:
def _check_domain_argument(arg_node: nodes.NodeNG, allow_iterable: bool = True) -> bool:
"""Ensure the argument node is a domain constant or variable.
We allow:
We currently only disallow `Platform.Xyz`
The plugin can be extended in the future (see #173374) to improve
consistency and only allow certain patterns for domain arguments,
such as:
- x.DOMAIN/x.domain attribute (including *_DOMAIN/*_domain)
- DOMAIN/domain name (including *_DOMAIN/*_domain)
- string literals
@@ -94,21 +99,8 @@ def _check_domain_argument(arg_node: nodes.NodeNG, allow_iterable: bool) -> bool
"""
match arg_node:
case nodes.Attribute():
if (
attrname := arg_node.attrname
) in _DOMAIN_CONSTANTS or attrname.endswith(_DOMAIN_SUFFIXES):
return True
case nodes.Const():
if isinstance(arg_node.value, str):
return True
case nodes.Name():
if (node_name := arg_node.name) in _DOMAIN_CONSTANTS or node_name.endswith(
_DOMAIN_SUFFIXES
):
return True
case nodes.Subscript():
# Ignore subscripts like dict["key"]
return True
if arg_node.expr.as_string() == "Platform":
return False
case nodes.Tuple():
if allow_iterable:
return all(
@@ -116,7 +108,7 @@ def _check_domain_argument(arg_node: nodes.NodeNG, allow_iterable: bool) -> bool
for element in arg_node.elts
)
return False
return True
class DomainConstantChecker(BaseChecker):
+5 -5
View File
@@ -236,7 +236,7 @@ aiocomelit==2.0.3
aiodhcpwatcher==1.2.7
# homeassistant.components.dhcp
aiodiscover==3.3.1
aiodiscover==3.3.2
# homeassistant.components.dnsip
aiodns==4.0.4
@@ -889,7 +889,7 @@ ecoaliface==0.4.0
egauge-async==0.4.0
# homeassistant.components.eheimdigital
eheimdigital==1.6.0
eheimdigital==1.7.0
# homeassistant.components.ekeybionyx
ekey-bionyxpy==1.0.1
@@ -2733,7 +2733,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==5.14.1
python-roborock==5.14.2
# homeassistant.components.smarttub
python-smarttub==0.0.47
@@ -2860,7 +2860,7 @@ pyzerproc==0.4.8
qbittorrent-api==2026.5.1
# homeassistant.components.qbus
qbusmqttapi==1.5.0
qbusmqttapi==1.5.1
# homeassistant.components.qingping
qingping-ble==1.1.5
@@ -3287,7 +3287,7 @@ url-normalize==3.0.0
uvcclient==0.12.1
# homeassistant.components.roborock
vacuum-map-parser-roborock==0.1.4
vacuum-map-parser-roborock==0.1.5
# homeassistant.components.vallox
vallox-websocket-api==6.0.0
@@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(
test_devices=None, test_groups=None
)
assert len(mock_hap.hmip_device_by_entity_id) == 384
assert len(mock_hap.hmip_device_by_entity_id) == 385
async def test_hmip_remove_device(
@@ -1,6 +1,6 @@
"""Tests for HomematicIP Cloud sensor."""
from homematicip.base.enums import ValveState
from homematicip.base.enums import ValveState, WindowState
from homeassistant.components.homematicip_cloud import DOMAIN
from homeassistant.components.homematicip_cloud.entity import (
@@ -745,6 +745,38 @@ async def test_hmip_tilt_vibration_sensor_tilt_state(
assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_SECOND_TRIGGER_ANGLE] == 75
async def test_hmip_rotary_handle_window_state_sensor(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicipWindowStateSensor exposes the three-way state of HmIP-SRH."""
entity_id = "sensor.fenstergriffsensor_window_state"
entity_name = "Fenstergriffsensor Window state"
device_model = "HmIP-SRH"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Fenstergriffsensor"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "tilted"
await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN)
ha_state = hass.states.get(entity_id)
assert ha_state.state == "open"
await async_manipulate_test_data(
hass, hmip_device, "windowState", WindowState.CLOSED
)
ha_state = hass.states.get(entity_id)
assert ha_state.state == "closed"
await async_manipulate_test_data(hass, hmip_device, "windowState", None)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_UNKNOWN
async def test_hmip_tilt_vibration_sensor_tilt_angle(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
+1 -2
View File
@@ -36,8 +36,7 @@ def mock_huum_client() -> Generator[AsyncMock]:
target_temperature=80,
config=3,
light=1,
humidity=0,
target_humidity=5,
humidity=5,
sauna_config=SaunaConfig(
child_lock="OFF",
max_heating_time=3,
@@ -10,7 +10,7 @@
'door_closed': True,
'duration': None,
'end_date': None,
'humidity': 0,
'humidity': 5,
'is_private': None,
'light': 1,
'payment_end_date': None,
@@ -29,7 +29,7 @@
'start_date': None,
'status': 232,
'steamer_error': None,
'target_humidity': 5,
'target_humidity': None,
'target_temperature': 80,
'temperature': 30,
}),
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'max': 10,
'max': 40,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
@@ -29,7 +29,7 @@
'object_id_base': 'Humidity',
'options': dict({
}),
'original_device_class': None,
'original_device_class': <NumberDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'huum',
@@ -38,17 +38,19 @@
'supported_features': 0,
'translation_key': 'humidity',
'unique_id': 'AABBCC112233',
'unit_of_measurement': None,
'unit_of_measurement': '%',
})
# ---
# name: test_number_entity[number.huum_sauna_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': 'Huum sauna Humidity',
'max': 10,
'max': 40,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.huum_sauna_humidity',
@@ -0,0 +1,36 @@
# serializer version: 1
# name: test_device_registry
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'e0:da:dc:0a:12:34',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'jvc_projector',
'e0:da:dc:0a:12:34',
),
}),
'labels': set({
}),
'manufacturer': 'JVC',
'model': 'B2A2',
'model_id': None,
'name': 'JVC Projector',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
@@ -3,6 +3,7 @@
from unittest.mock import AsyncMock, patch
from jvcprojector import JvcProjectorAuthError, JvcProjectorTimeoutError
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.jvc_projector.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
@@ -30,6 +31,20 @@ async def test_init(
assert device.identifiers == {(DOMAIN, mac)}
async def test_device_registry(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_device: AsyncMock,
mock_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the device registry entry, including the network MAC connection."""
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, format_mac(MOCK_MAC))}
)
assert device_entry == snapshot
async def test_unload_config_entry(
hass: HomeAssistant,
mock_device: AsyncMock,
@@ -0,0 +1,36 @@
# serializer version: 1
# name: test_device_registry_bulb
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'60:01:94:03:76:eb',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'mystrom',
'6001940376EB',
),
}),
'labels': set({
}),
'manufacturer': 'myStrom',
'model': None,
'model_id': None,
'name': 'myStrom Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': '2.58.0',
'via_device_id': None,
})
# ---
+15
View File
@@ -4,10 +4,12 @@ from unittest.mock import AsyncMock, PropertyMock, patch
from pymystrom.exceptions import MyStromConnectionError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.mystrom.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import (
MyStromBulbMock,
@@ -68,6 +70,19 @@ async def test_init_pir_and_unload(
assert config_entry.state is ConfigEntryState.NOT_LOADED
async def test_device_registry_bulb(
hass: HomeAssistant,
config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the bulb device registry entry, including the network MAC connection."""
await init_integration(hass, config_entry, 102)
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_MAC)})
assert device_entry == snapshot
async def test_init_switch_and_unload(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
+49 -4
View File
@@ -277,10 +277,17 @@ async def test_controller_cannot_connect(
) -> None:
"""Test an error talking to the controller."""
# Controller response with a failure
# Controller response with a failure, followed by successful responses
responses.clear()
responses.append(
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE)
responses.extend(
[
AiohttpClientMockResponse(
"POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE
),
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
]
)
result = await complete_flow(hass)
@@ -290,6 +297,19 @@ async def test_controller_cannot_connect(
assert not mock_setup.mock_calls
# Correct the form and enter the password again and setup completes
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: HOST, CONF_PASSWORD: PASSWORD},
)
assert result.get("type") is FlowResultType.CREATE_ENTRY
assert result.get("title") == HOST
assert "result" in result
assert dict(result["result"].data) == CONFIG_ENTRY_DATA
assert result["result"].unique_id == MAC_ADDRESS_UNIQUE_ID
assert len(mock_setup.mock_calls) == 1
async def test_controller_invalid_auth(
hass: HomeAssistant,
@@ -347,20 +367,45 @@ async def test_controller_invalid_auth(
async def test_controller_timeout(
hass: HomeAssistant,
mock_setup: Mock,
responses: list[AiohttpClientMockResponse],
) -> None:
"""Test an error talking to the controller."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "user"
assert not result.get("errors")
assert "flow_id" in result
with patch(
"homeassistant.components.rainbird.config_flow.asyncio.timeout",
side_effect=TimeoutError,
):
result = await complete_flow(hass)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: HOST, CONF_PASSWORD: PASSWORD},
)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "user"
assert result.get("errors") == {"base": "timeout_connect"}
assert not mock_setup.mock_calls
# Enter the password again and setup completes
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: HOST, CONF_PASSWORD: PASSWORD},
)
assert result.get("type") is FlowResultType.CREATE_ENTRY
assert result.get("title") == HOST
assert "result" in result
assert dict(result["result"].data) == CONFIG_ENTRY_DATA
assert result["result"].unique_id == MAC_ADDRESS_UNIQUE_ID
assert len(mock_setup.mock_calls) == 1
@pytest.mark.parametrize(
("responses", "config_entry_data"),
+24
View File
@@ -1532,3 +1532,27 @@ CONTACT_SENSOR_SERVICE_INFO = BluetoothServiceInfoBleak(
connectable=False,
tx_power=-127,
)
STANDING_FAN_SERVICE_INFO = BluetoothServiceInfoBleak(
name="WoStandingFan",
manufacturer_data={2409: b"\xb0\xe9\xfe\x01\x02\x03~\xd3R9"},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00\x00\x00\x11\x07\x60"
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="WoStandingFan",
manufacturer_data={2409: b"\xb0\xe9\xfe\x01\x02\x03~\xd3R9"},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00\x00\x00\x11\x07\x60"
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "WoStandingFan"),
time=0,
connectable=True,
tx_power=-127,
)
+23
View File
@@ -4,6 +4,7 @@ from collections.abc import Callable
from unittest.mock import AsyncMock, patch
import pytest
import switchbot
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.switchbot.const import (
@@ -26,6 +27,7 @@ from . import (
AIR_PURIFIER_US_SERVICE_INFO,
HUBMINI_MATTER_SERVICE_INFO,
LOCK_SERVICE_INFO,
STANDING_FAN_SERVICE_INFO,
WOCURTAIN_SERVICE_INFO,
WOMETERTHPC_SERVICE_INFO,
WOSENSORTH_SERVICE_INFO,
@@ -327,3 +329,24 @@ async def test_migrate_deprecated_air_purifier_sensor_type_device_not_in_range(
# sensor_type unchanged and entry not loaded; will retry when device advertises
assert entry.data[CONF_SENSOR_TYPE] == DEPRECATED_SENSOR_TYPE_AIR_PURIFIER
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_standing_fan_setup(
hass: HomeAssistant,
mock_entry_factory: Callable[[str], MockConfigEntry],
) -> None:
"""Test the Standing Fan is recognized and set up."""
inject_bluetooth_service_info(hass, STANDING_FAN_SERVICE_INFO)
entry = mock_entry_factory(sensor_type="standing_fan")
entry.add_to_hass(hass)
with patch(
"homeassistant.components.switchbot.switchbot.SwitchbotStandingFan.get_basic_info",
new=AsyncMock(return_value=None),
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
assert isinstance(entry.runtime_data.device, switchbot.SwitchbotStandingFan)
+77
View File
@@ -2,6 +2,8 @@
from unittest.mock import AsyncMock, patch
from aiohttp import ClientConnectionError, ClientError
import pytest
from tesla_fleet_api.exceptions import (
InvalidRequest,
InvalidToken,
@@ -74,3 +76,78 @@ async def test_scopes_error(hass: HomeAssistant) -> None:
):
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.parametrize(
("patch_target", "exception"),
[
pytest.param(
None,
ClientConnectionError(),
id="list_vehicles-connection_error",
),
pytest.param(
None,
ClientError(),
id="list_vehicles-client_error",
),
pytest.param(
"homeassistant.components.tessie.Tessie.scopes",
ClientConnectionError(),
id="scopes-connection_error",
),
pytest.param(
"homeassistant.components.tessie.Tessie.scopes",
ClientError(),
id="scopes-client_error",
),
pytest.param(
"homeassistant.components.tessie.Tessie.products",
ClientConnectionError(),
id="products-connection_error",
),
pytest.param(
"homeassistant.components.tessie.Tessie.products",
ClientError(),
id="products-client_error",
),
],
)
async def test_aiohttp_client_error_retries(
hass: HomeAssistant,
mock_get_state_of_all_vehicles: AsyncMock,
patch_target: str | None,
exception: ClientError,
) -> None:
"""Test that aiohttp.ClientError on any setup call triggers SETUP_RETRY.
Covers list_vehicles(), scopes(), and products() all network calls that
tesla_fleet_api does not wrap in TeslaFleetError.
"""
if patch_target is None:
mock_get_state_of_all_vehicles.side_effect = exception
entry = await setup_platform(hass)
else:
with patch(patch_target, side_effect=exception):
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.parametrize(
"exception",
[
pytest.param(ClientConnectionError(), id="connection_error"),
pytest.param(ClientError(), id="client_error"),
],
)
async def test_aiohttp_client_error_on_live_status_retries(
hass: HomeAssistant,
exception: ClientError,
) -> None:
"""Test that aiohttp.ClientError during live_status() triggers SETUP_RETRY."""
with patch(
"tesla_fleet_api.tessie.EnergySite.live_status",
side_effect=exception,
):
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.SETUP_RETRY
+20
View File
@@ -103,6 +103,26 @@ async def test_setup_entry_subscribe_failure(
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_apply_optimistic_creates_missing_intermediate_dicts(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_vistapool_client: AsyncMock,
) -> None:
"""Test apply_optimistic walks through and creates missing intermediate dicts."""
mock_vistapool_client.fetch_pool_data.return_value = {"existing": "scalar"}
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
coordinator = next(iter(mock_config_entry.runtime_data.coordinators.values()))
coordinator.apply_optimistic("filtration.intel.temp", 27)
coordinator.apply_optimistic("existing.nested.key", 1)
assert coordinator.data["filtration"]["intel"]["temp"] == 27
assert coordinator.data["existing"] == {"nested": {"key": 1}}
async def test_unload_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
+37
View File
@@ -98,6 +98,43 @@ async def test_light_set_value(
)
@pytest.mark.parametrize(
("service", "initial_status", "initial_state", "expected_state"),
[
pytest.param(SERVICE_TURN_ON, 0, STATE_OFF, STATE_ON, id="turn_on"),
pytest.param(SERVICE_TURN_OFF, 1, STATE_ON, STATE_OFF, id="turn_off"),
],
)
async def test_light_optimistic_state(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_vistapool_client: AsyncMock,
mock_pool_data: dict[str, Any],
service: str,
initial_status: int,
initial_state: str,
expected_state: str,
) -> None:
"""Test the entity state reflects the just-written value before the Firestore push."""
mock_pool_data["light"] = {"status": initial_status}
mock_vistapool_client.fetch_pool_data.return_value = mock_pool_data
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("light.my_pool_light").state == initial_state
await hass.services.async_call(
LIGHT_DOMAIN,
service,
{ATTR_ENTITY_ID: "light.my_pool_light"},
blocking=True,
)
assert hass.states.get("light.my_pool_light").state == expected_state
async def test_light_set_value_raises_on_api_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@@ -232,6 +232,7 @@ async def test_number_set_value(
mock_vistapool_client.set_value.assert_awaited_once_with(
"ABCDEF1234567890", expected_path, expected_raw
)
assert hass.states.get(entity_id).state == str(float(user_value))
value_arg = mock_vistapool_client.set_value.await_args.args[2]
assert isinstance(value_arg, int)
@@ -6,9 +6,6 @@ from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.xbox.binary_sensor import XboxBinarySensor
from homeassistant.components.xbox.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -43,39 +40,3 @@ async def test_binary_sensors(
assert config_entry.state is ConfigEntryState.LOADED
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.parametrize(
("entity_id", "key"),
[
("gsr_ae_in_multiplayer", XboxBinarySensor.IN_MULTIPLAYER),
("gsr_ae_in_party", XboxBinarySensor.IN_PARTY),
],
)
@pytest.mark.usefixtures("xbox_live_client", "entity_registry_enabled_by_default")
async def test_binary_sensor_deprecation_remove_disabled(
hass: HomeAssistant,
config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
entity_id: str,
key: XboxBinarySensor,
) -> None:
"""Test we remove a deprecated binary sensor."""
entity_registry.async_get_or_create(
BINARY_SENSOR_DOMAIN,
DOMAIN,
f"271958441785640_{key}",
suggested_object_id=entity_id,
)
assert entity_registry is not None
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert entity_registry.async_get(f"binary_sensor.{entity_id}") is None
-39
View File
@@ -6,9 +6,6 @@ from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.xbox.const import DOMAIN
from homeassistant.components.xbox.sensor import XboxSensor
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -43,39 +40,3 @@ async def test_sensors(
assert config_entry.state is ConfigEntryState.LOADED
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.parametrize(
("entity_id", "key"),
[
("gsr_ae_account_tier", XboxSensor.ACCOUNT_TIER),
("gsr_ae_gold_tenure", XboxSensor.GOLD_TENURE),
],
)
@pytest.mark.usefixtures("xbox_live_client", "entity_registry_enabled_by_default")
async def test_sensor_deprecation_remove_entity(
hass: HomeAssistant,
config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
entity_id: str,
key: XboxSensor,
) -> None:
"""Test we remove a deprecated sensor."""
entity_registry.async_get_or_create(
SENSOR_DOMAIN,
DOMAIN,
f"271958441785640_{key}",
suggested_object_id=entity_id,
)
assert entity_registry is not None
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert entity_registry.async_get(f"sensor.{entity_id}") is None
@@ -877,48 +877,6 @@ async def test_set_credential_length_validation(
api.set_credential.assert_not_called()
async def test_set_credential_slot_out_of_range(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
client: MagicMock,
lock_schlage_be469: Node,
integration: MockConfigEntry,
) -> None:
"""Explicit credential_slot above device capacity fails fast."""
api = _mock_access_control(lock_schlage_be469)
cred_caps = api.get_credential_capabilities_cached.return_value
cred_caps.supported_credential_types[
UserCredentialType.PIN_CODE
].number_of_credential_slots = 5
with pytest.raises(HomeAssistantError) as exc:
await hass.services.async_call(
DOMAIN,
"set_credential",
{
ATTR_ENTITY_ID: _lock_entity_id(
entity_registry, device_registry, client, lock_schlage_be469
),
"user_id": 1,
"credential_type": "pin_code",
"credential_data": "1234",
"credential_slot": 6,
},
blocking=True,
return_response=True,
)
# The explicit slot exceeds the device-reported capacity, so the helper
# rejects the call with the rendered upper bound and never writes.
assert exc.value.translation_key == "credential_slot_out_of_range"
assert exc.value.translation_placeholders == {
"credential_type": "pin_code",
"max_slot": "5",
}
api.set_credential.assert_not_called()
async def test_delete_credential(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
+72 -69
View File
@@ -146,6 +146,72 @@ hass.services.unrelated("other")
""",
id="unrelated_method",
),
pytest.param(
"""
async_setup_component(hass, OTHER, {})
""",
id="name_not_domain",
),
pytest.param(
"""
async_setup_component(hass, sensor.OTHER, {})
""",
id="attribute_not_domain",
),
pytest.param(
"""
async_setup_component(hass, 5, {})
""",
id="non_string_constant",
),
pytest.param(
"""
async_mock_service(hass, OTHER, "service")
""",
id="async_mock_service_other",
),
pytest.param(
"""
MockConfigEntry(domain=OTHER)
""",
id="mock_config_entry_kwarg_other",
),
pytest.param(
"""
hass.services.async_call(OTHER, "service")
""",
id="services_async_call_other",
),
pytest.param(
"""
hass.services.call(OTHER, "service")
""",
id="services_call_other",
),
pytest.param(
"""
hass.config_entries.flow.async_init(OTHER)
""",
id="flow_async_init_positional_other",
),
pytest.param(
"""
hass.config_entries.flow.async_init(handler=OTHER)
""",
id="flow_async_init_kwarg_other",
),
pytest.param(
"""
hass.states.async_entity_ids(OTHER)
""",
id="async_entity_ids_other",
),
pytest.param(
"""
hass.states.async_entity_ids((DOMAIN, OTHER))
""",
id="async_entity_ids_tuple_other",
),
],
)
def test_no_warning(
@@ -167,80 +233,17 @@ def test_no_warning(
[
pytest.param(
"""
async_setup_component(hass, OTHER, {})
async_setup_component(hass, Platform.Something, {})
""",
("OTHER", "async_setup_component"),
id="name_not_domain",
("Platform.Something", "async_setup_component"),
id="attribute_platform",
),
pytest.param(
"""
async_setup_component(hass, sensor.OTHER, {})
hass.states.async_entity_ids((Platform.SENSOR, DOMAIN))
""",
("sensor.OTHER", "async_setup_component"),
id="attribute_not_domain",
),
pytest.param(
"""
async_setup_component(hass, 5, {})
""",
("5", "async_setup_component"),
id="non_string_constant",
),
pytest.param(
"""
async_mock_service(hass, OTHER, "service")
""",
("OTHER", "async_mock_service"),
id="async_mock_service",
),
pytest.param(
"""
MockConfigEntry(domain=OTHER)
""",
("OTHER", "MockConfigEntry"),
id="mock_config_entry_kwarg",
),
pytest.param(
"""
hass.services.async_call(OTHER, "service")
""",
("OTHER", "hass.services.async_call"),
id="services_async_call",
),
pytest.param(
"""
hass.services.call(OTHER, "service")
""",
("OTHER", "hass.services.call"),
id="services_call",
),
pytest.param(
"""
hass.config_entries.flow.async_init(OTHER)
""",
("OTHER", "hass.config_entries.flow.async_init"),
id="flow_async_init_positional",
),
pytest.param(
"""
hass.config_entries.flow.async_init(handler=OTHER)
""",
("OTHER", "hass.config_entries.flow.async_init"),
id="flow_async_init_kwarg",
),
pytest.param(
"""
hass.states.async_entity_ids(OTHER)
""",
("OTHER", "hass.states.async_entity_ids"),
id="async_entity_ids",
),
pytest.param(
"""
hass.states.async_entity_ids((DOMAIN, OTHER))
""",
("(DOMAIN, OTHER)", "hass.states.async_entity_ids"),
id="async_entity_ids_tuple",
("(Platform.SENSOR, DOMAIN)", "hass.states.async_entity_ids"),
id="attribute_platform_tuple",
),
],
)