mirror of
https://github.com/home-assistant/core.git
synced 2026-05-26 02:35:12 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d58d51e51 |
@@ -193,11 +193,7 @@ async def async_setup_entry(
|
||||
Aranet4BluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_register_processor(
|
||||
processor, AranetSensorEntityDescription
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
|
||||
|
||||
|
||||
class Aranet4BluetoothSensorEntity(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Mapping
|
||||
from ipaddress import ip_address
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -49,9 +49,6 @@ from .const import (
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
from .hub import AxisHub, get_axis_api
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import axis
|
||||
|
||||
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f", "e8:27:25"}
|
||||
DEFAULT_PORT = 443
|
||||
DEFAULT_PROTOCOL = "https"
|
||||
@@ -96,8 +93,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
else:
|
||||
if (serial := self._get_serial_number(api)) is None:
|
||||
return self.async_abort(reason="no_serial_number")
|
||||
serial = api.vapix.serial_number
|
||||
config = {
|
||||
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
@@ -262,19 +258,6 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
@staticmethod
|
||||
def _get_serial_number(api: axis.AxisDevice) -> str | None:
|
||||
"""Retrieve the device serial number from the Axis API.
|
||||
|
||||
Tries basic_device_info first, then property_handler. Returns None if not found.
|
||||
"""
|
||||
vapix = api.vapix
|
||||
if vapix.basic_device_info.initialized:
|
||||
return vapix.basic_device_info["0"].serial_number
|
||||
if vapix.params.property_handler.initialized:
|
||||
return vapix.params.property_handler["0"].system_serial_number
|
||||
return None
|
||||
|
||||
|
||||
class AxisOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Axis device options."""
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"link_local_address": "Link local addresses are not supported",
|
||||
"no_serial_number": "Could not retrieve a serial number from the device. Please check device connectivity and try again.",
|
||||
"not_axis_device": "Discovered device not an Axis device",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
|
||||
@@ -124,9 +124,7 @@ async def async_setup_entry(
|
||||
BlueMaestroBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class BlueMaestroBluetoothSensorEntity(
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
"requirements": [
|
||||
"bleak==3.0.2",
|
||||
"bleak-retry-connector==4.6.1",
|
||||
"bluetooth-adapters==2.3.0",
|
||||
"bluetooth-adapters==2.2.0",
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.11",
|
||||
"habluetooth==6.7.4"
|
||||
"dbus-fast==5.0.9",
|
||||
"habluetooth==6.7.3"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_LOCATION_NAME,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
CONF_ASSOCIATED_ZONE,
|
||||
CONF_CONSIDER_HOME,
|
||||
CONF_NEW_DEVICE_DEFAULTS,
|
||||
CONF_SCAN_INTERVAL,
|
||||
|
||||
@@ -17,8 +17,19 @@ from homeassistant.const import (
|
||||
STATE_NOT_HOME,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import (
|
||||
DeviceInfo,
|
||||
EventDeviceRegistryUpdatedData,
|
||||
@@ -27,6 +38,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -36,6 +48,7 @@ from .const import (
|
||||
ATTR_IP,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
CONF_ASSOCIATED_ZONE,
|
||||
CONNECTED_DEVICE_REGISTERED,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
@@ -343,14 +356,116 @@ class BaseScannerEntity(BaseTrackerEntity):
|
||||
addresses being used to identify the device.
|
||||
"""
|
||||
|
||||
_scanner_option_associated_zone: str = zone.ENTITY_ID_HOME
|
||||
_scanner_option_associated_zone_unsub: CALLBACK_TYPE | None = None
|
||||
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Call when the scanner entity is added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
if not self.registry_entry:
|
||||
return
|
||||
self._async_read_entity_options()
|
||||
|
||||
async def async_internal_will_remove_from_hass(self) -> None:
|
||||
"""Call when the scanner entity is about to be removed from hass."""
|
||||
if self._scanner_option_associated_zone_unsub is not None:
|
||||
self._scanner_option_associated_zone_unsub()
|
||||
self._scanner_option_associated_zone_unsub = None
|
||||
self._async_clear_associated_zone_issue()
|
||||
await super().async_internal_will_remove_from_hass()
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self) -> None:
|
||||
"""Run when the entity registry entry has been updated."""
|
||||
self._async_read_entity_options()
|
||||
|
||||
@callback
|
||||
def _async_read_entity_options(self) -> None:
|
||||
"""Read entity options from the entity registry.
|
||||
|
||||
Called when the entity registry entry has been updated and before the
|
||||
scanner entity is added to the state machine.
|
||||
"""
|
||||
assert self.registry_entry
|
||||
if (scanner_options := self.registry_entry.options.get(DOMAIN)) and (
|
||||
associated_zone := scanner_options.get(CONF_ASSOCIATED_ZONE)
|
||||
):
|
||||
new_zone = associated_zone
|
||||
else:
|
||||
new_zone = zone.ENTITY_ID_HOME
|
||||
|
||||
if new_zone == self._scanner_option_associated_zone:
|
||||
return
|
||||
|
||||
# Tear down tracking for the previous zone.
|
||||
if self._scanner_option_associated_zone_unsub is not None:
|
||||
self._scanner_option_associated_zone_unsub()
|
||||
self._scanner_option_associated_zone_unsub = None
|
||||
self._async_clear_associated_zone_issue()
|
||||
|
||||
self._scanner_option_associated_zone = new_zone
|
||||
|
||||
# zone.home is always present so no tracking or issue handling needed.
|
||||
if new_zone == zone.ENTITY_ID_HOME:
|
||||
return
|
||||
|
||||
self._scanner_option_associated_zone_unsub = async_track_state_change_event(
|
||||
self.hass, new_zone, self._async_associated_zone_state_changed
|
||||
)
|
||||
if self.hass.states.get(new_zone) is None:
|
||||
self._async_create_associated_zone_issue()
|
||||
|
||||
@callback
|
||||
def _async_associated_zone_state_changed(
|
||||
self, event: Event[EventStateChangedData]
|
||||
) -> None:
|
||||
"""Open or clear the repair issue when the associated zone appears or disappears."""
|
||||
if event.data["new_state"] is None:
|
||||
self._async_create_associated_zone_issue()
|
||||
else:
|
||||
self._async_clear_associated_zone_issue()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_create_associated_zone_issue(self) -> None:
|
||||
"""Create a repair issue prompting the user to reconfigure the scanner."""
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
self._associated_zone_issue_id,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="associated_zone_missing",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"zone": self._scanner_option_associated_zone,
|
||||
},
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_clear_associated_zone_issue(self) -> None:
|
||||
"""Clear the associated-zone-missing repair issue if it exists."""
|
||||
ir.async_delete_issue(self.hass, DOMAIN, self._associated_zone_issue_id)
|
||||
|
||||
@property
|
||||
def _associated_zone_issue_id(self) -> str:
|
||||
"""Return the issue id for the associated-zone-missing repair."""
|
||||
return f"associated_zone_missing_{self.entity_id}"
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the device."""
|
||||
if self.is_connected is None:
|
||||
return None
|
||||
if self.is_connected:
|
||||
if not self.is_connected:
|
||||
return STATE_NOT_HOME
|
||||
associated_zone = self._scanner_option_associated_zone
|
||||
if associated_zone == zone.ENTITY_ID_HOME:
|
||||
return STATE_HOME
|
||||
return STATE_NOT_HOME
|
||||
if zone_state := self.hass.states.get(associated_zone):
|
||||
return zone_state.name
|
||||
# Configured zone has been removed; state is unknown.
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool | None:
|
||||
@@ -367,9 +482,18 @@ class BaseScannerEntity(BaseTrackerEntity):
|
||||
if not self.is_connected:
|
||||
return attr
|
||||
|
||||
associated_zone = self._scanner_option_associated_zone
|
||||
# If the configured zone has been removed, in_zones stays empty so the
|
||||
# attribute does not claim membership in a zone that no longer exists.
|
||||
if (
|
||||
associated_zone != zone.ENTITY_ID_HOME
|
||||
and self.hass.states.get(associated_zone) is None
|
||||
):
|
||||
return attr
|
||||
|
||||
attr[ATTR_IN_ZONES] = [
|
||||
zone.ENTITY_ID_HOME,
|
||||
*zone.async_get_enclosing_zones(self.hass, zone.ENTITY_ID_HOME),
|
||||
associated_zone,
|
||||
*zone.async_get_enclosing_zones(self.hass, associated_zone),
|
||||
]
|
||||
|
||||
return attr
|
||||
|
||||
@@ -36,6 +36,8 @@ DEFAULT_CONSIDER_HOME: Final = timedelta(seconds=180)
|
||||
|
||||
CONF_NEW_DEVICE_DEFAULTS: Final = "new_device_defaults"
|
||||
|
||||
CONF_ASSOCIATED_ZONE: Final = "associated_zone"
|
||||
|
||||
ATTR_ATTRIBUTES: Final = "attributes"
|
||||
ATTR_BATTERY: Final = "battery"
|
||||
ATTR_DEV_ID: Final = "dev_id"
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"associated_zone_missing": {
|
||||
"description": "The scanner entity `{entity_id}` is associated with the zone `{zone}`, but that zone has been removed.\n\nTo fix this, reconfigure the scanner to use a different zone or recreate the missing zone.",
|
||||
"title": "Scanner is associated with a removed zone"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"see": {
|
||||
"description": "Manually update the records of a seen legacy device tracker in the known_devices.yaml file.",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.7",
|
||||
"aiodhcpwatcher==1.2.6",
|
||||
"aiodiscover==3.2.3",
|
||||
"cached-ipaddress==1.1.1"
|
||||
]
|
||||
|
||||
@@ -27,7 +27,7 @@ DEFAULT_BLUETOOTH_SCANNING_MODE = BluetoothScanningMode.AUTO.value
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
STABLE_BLE_VERSION_STR = "2026.5.1"
|
||||
STABLE_BLE_VERSION_STR = "2025.11.0"
|
||||
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
|
||||
PROJECT_URLS = {
|
||||
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
|
||||
|
||||
@@ -53,7 +53,7 @@ def async_static_info_updated(
|
||||
platform: entity_platform.EntityPlatform,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
info_type: type[_InfoT],
|
||||
entity_type: Callable[[RuntimeEntryData, EntityInfo, type[_StateT]], _EntityT],
|
||||
entity_type: type[_EntityT],
|
||||
state_type: type[_StateT],
|
||||
infos: list[EntityInfo],
|
||||
) -> None:
|
||||
@@ -188,7 +188,7 @@ async def platform_async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
*,
|
||||
info_type: type[_InfoT],
|
||||
entity_type: Callable[[RuntimeEntryData, EntityInfo, type[_StateT]], _EntityT],
|
||||
entity_type: type[_EntityT],
|
||||
state_type: type[_StateT],
|
||||
info_filter: Callable[[_InfoT], bool] | None = None,
|
||||
) -> None:
|
||||
@@ -196,11 +196,6 @@ async def platform_async_setup_entry(
|
||||
|
||||
This method is in charge of receiving, distributing and storing
|
||||
info and state updates.
|
||||
|
||||
`entity_type` is any callable that builds an entity from
|
||||
`(entry_data, info, state_type)`. A regular entity class satisfies this,
|
||||
and platforms with multiple entity classes can pass a factory function
|
||||
that picks the class per static info.
|
||||
"""
|
||||
entry_data = entry.runtime_data
|
||||
entry_data.info[info_type] = {}
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
"""Infrared platform for ESPHome."""
|
||||
|
||||
import functools
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aioesphomeapi import EntityInfo, EntityState, InfraredCapability, InfraredInfo
|
||||
from aioesphomeapi.client import InfraredRFReceiveEventModel
|
||||
from aioesphomeapi import EntityState, InfraredCapability, InfraredInfo
|
||||
|
||||
from homeassistant.components.infrared import (
|
||||
InfraredCommand,
|
||||
InfraredEmitterEntity,
|
||||
InfraredReceivedSignal,
|
||||
InfraredReceiverEntity,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, callback
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEmitterEntity
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .entity import (
|
||||
EsphomeEntity,
|
||||
convert_api_error_ha_error,
|
||||
platform_async_setup_entry,
|
||||
)
|
||||
from .entry_data import RuntimeEntryData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class _EsphomeInfraredEntity(EsphomeEntity[InfraredInfo, EntityState]):
|
||||
"""Common base for ESPHome infrared entities."""
|
||||
class EsphomeInfraredEntity(
|
||||
EsphomeEntity[InfraredInfo, EntityState], InfraredEmitterEntity
|
||||
):
|
||||
"""ESPHome infrared emitter entity using native API."""
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
@@ -38,10 +32,6 @@ class _EsphomeInfraredEntity(EsphomeEntity[InfraredInfo, EntityState]):
|
||||
# Infrared entities should go available as soon as the device comes online
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class EsphomeInfraredEmitterEntity(_EsphomeInfraredEntity, InfraredEmitterEntity):
|
||||
"""ESPHome infrared emitter entity using native API."""
|
||||
|
||||
@convert_api_error_ha_error
|
||||
async def async_send_command(self, command: InfraredCommand) -> None:
|
||||
"""Send an IR command."""
|
||||
@@ -56,77 +46,10 @@ class EsphomeInfraredEmitterEntity(_EsphomeInfraredEntity, InfraredEmitterEntity
|
||||
)
|
||||
|
||||
|
||||
class EsphomeInfraredReceiverEntity(_EsphomeInfraredEntity, InfraredReceiverEntity):
|
||||
"""ESPHome infrared receiver entity using native API."""
|
||||
|
||||
_unsub_receive: CALLBACK_TYPE | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks including IR receive subscription."""
|
||||
await super().async_added_to_hass()
|
||||
self._async_subscribe_receive()
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe from the device on entity removal."""
|
||||
await super().async_will_remove_from_hass()
|
||||
if self._unsub_receive is not None:
|
||||
self._unsub_receive()
|
||||
self._unsub_receive = None
|
||||
|
||||
@callback
|
||||
def _async_subscribe_receive(self) -> None:
|
||||
"""Subscribe to IR receive events if the device is connected."""
|
||||
# Subscribing requires an active API connection; defer to
|
||||
# _on_device_update when the device is not (yet) available.
|
||||
if self._unsub_receive is not None or not self._entry_data.available:
|
||||
return
|
||||
self._unsub_receive = self._client.subscribe_infrared_rf_receive(
|
||||
self._on_infrared_rf_receive
|
||||
)
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
"""Call when device updates or entry data changes."""
|
||||
super()._on_device_update()
|
||||
if self._entry_data.available:
|
||||
self._async_subscribe_receive()
|
||||
elif self._unsub_receive is not None:
|
||||
self._unsub_receive = None
|
||||
|
||||
@callback
|
||||
def _on_infrared_rf_receive(self, event: InfraredRFReceiveEventModel) -> None:
|
||||
"""Handle a received IR signal from the device."""
|
||||
if (
|
||||
event.key != self._static_info.key
|
||||
or event.device_id != self._static_info.device_id
|
||||
):
|
||||
return
|
||||
self._handle_received_signal(InfraredReceivedSignal(timings=event.timings))
|
||||
|
||||
|
||||
def _make_infrared_entity(
|
||||
entry_data: RuntimeEntryData,
|
||||
info: EntityInfo,
|
||||
state_type: type[EntityState],
|
||||
) -> _EsphomeInfraredEntity:
|
||||
"""Build the right infrared entity based on the InfraredInfo capabilities."""
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(info, InfraredInfo)
|
||||
cls = (
|
||||
EsphomeInfraredReceiverEntity
|
||||
if info.capabilities & InfraredCapability.RECEIVER
|
||||
else EsphomeInfraredEmitterEntity
|
||||
)
|
||||
return cls(entry_data, info, state_type)
|
||||
|
||||
|
||||
async_setup_entry = functools.partial(
|
||||
async_setup_entry = partial(
|
||||
platform_async_setup_entry,
|
||||
info_type=InfraredInfo,
|
||||
entity_type=_make_infrared_entity,
|
||||
entity_type=EsphomeInfraredEntity,
|
||||
state_type=EntityState,
|
||||
info_filter=lambda info: bool(
|
||||
info.capabilities
|
||||
& (InfraredCapability.TRANSMITTER | InfraredCapability.RECEIVER)
|
||||
),
|
||||
info_filter=lambda info: bool(info.capabilities & InfraredCapability.TRANSMITTER),
|
||||
)
|
||||
|
||||
@@ -289,12 +289,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
"""Service handler for reloading core config."""
|
||||
try:
|
||||
conf = await conf_util.async_hass_config_yaml(hass)
|
||||
except (HomeAssistantError, FileNotFoundError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="core_config_reload_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
# auth only processed during startup
|
||||
await core_config.async_process_ha_core_config(hass, conf.get(DOMAIN) or {})
|
||||
|
||||
@@ -183,12 +183,10 @@ async def async_setup_platform(
|
||||
"""Reload the scene config."""
|
||||
try:
|
||||
config = await conf_util.async_hass_config_yaml(hass)
|
||||
except (HomeAssistantError, FileNotFoundError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="scene_config_reload_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
integration = await async_get_integration(hass, SCENE_DOMAIN)
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"config_validator_unknown_err": {
|
||||
"message": "Unknown error calling {domain} config validator - {error}."
|
||||
},
|
||||
"core_config_reload_failed": {
|
||||
"message": "Failed to reload the Home Assistant Core configuration - {error}"
|
||||
},
|
||||
"max_length_exceeded": {
|
||||
"message": "Value {value} for property {property_name} has a maximum length of {max_length} characters."
|
||||
},
|
||||
@@ -51,9 +48,6 @@
|
||||
"platform_schema_validator_err": {
|
||||
"message": "Unknown error when validating config for {domain} from integration {p_name} - {error}."
|
||||
},
|
||||
"scene_config_reload_failed": {
|
||||
"message": "Failed to reload the Home Assistant scene platform configuration - {error}"
|
||||
},
|
||||
"service_config_entry_not_found": {
|
||||
"message": "Integration {domain} config entry with ID {entry_id} was not found."
|
||||
},
|
||||
|
||||
@@ -116,7 +116,6 @@ SENSOR_KEYS: Final[dict[int, list[str]]] = {
|
||||
IndevoltBattery.PACK_3_TEMPERATURE,
|
||||
IndevoltBattery.PACK_4_TEMPERATURE,
|
||||
IndevoltBattery.PACK_5_TEMPERATURE,
|
||||
IndevoltBattery.MAIN_MOS_TEMPERATURE,
|
||||
IndevoltBattery.PACK_1_MOS_TEMPERATURE,
|
||||
IndevoltBattery.PACK_2_MOS_TEMPERATURE,
|
||||
IndevoltBattery.PACK_3_MOS_TEMPERATURE,
|
||||
|
||||
@@ -612,16 +612,6 @@ SENSORS: Final = (
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# Battery Pack MOS Temperature
|
||||
IndevoltSensorEntityDescription(
|
||||
key=IndevoltBattery.MAIN_MOS_TEMPERATURE,
|
||||
generation=(2,),
|
||||
translation_key="main_mos_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
IndevoltSensorEntityDescription(
|
||||
key=IndevoltBattery.PACK_1_MOS_TEMPERATURE,
|
||||
generation=(2,),
|
||||
|
||||
@@ -295,9 +295,6 @@
|
||||
"main_current": {
|
||||
"name": "Main current"
|
||||
},
|
||||
"main_mos_temperature": {
|
||||
"name": "Main MOS temperature"
|
||||
},
|
||||
"main_serial_number": {
|
||||
"name": "Main serial number"
|
||||
},
|
||||
|
||||
@@ -27,11 +27,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FALLBACK_POLL_INTERVAL = timedelta(seconds=180)
|
||||
|
||||
# IBS-TH2 broadcasts every ~20-30s and only carries sensor data in the scan
|
||||
# response, so the default 10s active window misses the device most cycles.
|
||||
# 25s covers one full broadcast interval with margin to absorb jitter.
|
||||
ACTIVE_SCAN_DURATION = 25.0
|
||||
|
||||
|
||||
class INKBIRDActiveBluetoothProcessorCoordinator(
|
||||
ActiveBluetoothProcessorCoordinator[SensorUpdate]
|
||||
@@ -62,7 +57,6 @@ class INKBIRDActiveBluetoothProcessorCoordinator(
|
||||
needs_poll_method=self._async_needs_poll,
|
||||
poll_method=self._async_poll_data,
|
||||
connectable=False, # Polling only happens if active scanning is disabled
|
||||
scan_duration=ACTIVE_SCAN_DURATION,
|
||||
)
|
||||
|
||||
async def async_init(self) -> None:
|
||||
|
||||
@@ -63,5 +63,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==1.4.0"]
|
||||
"requirements": ["inkbird-ble==1.2.3"]
|
||||
}
|
||||
|
||||
@@ -117,9 +117,7 @@ async def async_setup_entry(
|
||||
INKBIRDBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
|
||||
|
||||
|
||||
class INKBIRDBluetoothSensorEntity(
|
||||
|
||||
@@ -116,9 +116,7 @@ async def async_setup_entry(
|
||||
KegtronBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class KegtronBluetoothSensorEntity(
|
||||
|
||||
@@ -111,9 +111,7 @@ async def async_setup_entry(
|
||||
MoatBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class MoatBluetoothSensorEntity(
|
||||
|
||||
@@ -123,9 +123,7 @@ async def async_setup_entry(
|
||||
MopekaBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class MopekaBluetoothSensorEntity(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["icmplib"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["icmplib==3.0.4"]
|
||||
"requirements": ["icmplib==3.0"]
|
||||
}
|
||||
|
||||
@@ -27,15 +27,6 @@ ERROR_NO_SCHEDULE = "set_schedule_first"
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def _check_for_schedule(active: bool, last_active: str | None) -> None:
|
||||
"""Raise a HAError when no thermostat schedule has been set."""
|
||||
if not active and last_active is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=ERROR_NO_SCHEDULE,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlugwiseClimateExtraStoredData(ExtraStoredData):
|
||||
"""Object to hold extra stored data."""
|
||||
@@ -94,6 +85,22 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_translation_key = DOMAIN
|
||||
|
||||
_last_active_schedule: str | None = None
|
||||
_previous_action_mode: str | None = HVACAction.HEATING.value
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if extra_data := await self.async_get_last_extra_data():
|
||||
plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
|
||||
extra_data.as_dict()
|
||||
)
|
||||
self._last_active_schedule = plugwise_extra_data.last_active_schedule
|
||||
self._previous_action_mode = (
|
||||
plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PlugwiseDataUpdateCoordinator,
|
||||
@@ -103,18 +110,18 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = f"{device_id}-climate"
|
||||
|
||||
self._api = coordinator.api
|
||||
gateway_id: str = self._api.gateway_id
|
||||
gateway_id: str = coordinator.api.gateway_id
|
||||
self._gateway_data = coordinator.data[gateway_id]
|
||||
self._last_active_schedule: str | None = None
|
||||
self._location = device_id
|
||||
if (location := self.device.get("location")) is not None:
|
||||
self._location = location
|
||||
self._previous_action_mode = HVACAction.HEATING.value
|
||||
|
||||
# Determine supported features
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if self._api.cooling_present and self._api.smile.name != "Adam":
|
||||
if (
|
||||
self.coordinator.api.cooling_present
|
||||
and coordinator.api.smile.name != "Adam"
|
||||
):
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
@@ -133,18 +140,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
||||
self.device["thermostat"]["resolution"], 0.1
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if extra_data := await self.async_get_last_extra_data():
|
||||
plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
|
||||
extra_data.as_dict()
|
||||
)
|
||||
self._last_active_schedule = plugwise_extra_data.last_active_schedule
|
||||
self._previous_action_mode = (
|
||||
plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
|
||||
)
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""Return the current temperature."""
|
||||
return self.device["sensors"]["temperature"]
|
||||
|
||||
@property
|
||||
def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
|
||||
@@ -154,11 +153,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
||||
previous_action_mode=self._previous_action_mode,
|
||||
)
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""Return the current temperature."""
|
||||
return self.device["sensors"]["temperature"]
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Return the temperature we try to reach.
|
||||
@@ -203,7 +197,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
||||
if self.device.get("available_schedules"):
|
||||
hvac_modes.append(HVACMode.AUTO)
|
||||
|
||||
if self._api.cooling_present:
|
||||
if self.coordinator.api.cooling_present:
|
||||
if "regulation_modes" in self._gateway_data:
|
||||
if "heating" in self._gateway_data["regulation_modes"]:
|
||||
hvac_modes.append(HVACMode.HEAT)
|
||||
@@ -253,69 +247,79 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
||||
if mode := kwargs.get(ATTR_HVAC_MODE):
|
||||
await self.async_set_hvac_mode(mode)
|
||||
|
||||
await self._api.set_temperature(self._location, data)
|
||||
await self.coordinator.api.set_temperature(self._location, data)
|
||||
|
||||
def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
|
||||
"""Return the API regulation value for a manual HVAC mode, or None.
|
||||
|
||||
The function inputs are limited to the HVACModes HEAT and COOL.
|
||||
"""
|
||||
def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str | None:
|
||||
"""Return the API regulation value for a manual HVAC mode, or None."""
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
mode = HVACAction.HEATING.value
|
||||
return HVACAction.HEATING.value
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
mode = HVACAction.COOLING.value
|
||||
return mode
|
||||
return HVACAction.COOLING.value
|
||||
return None
|
||||
|
||||
@plugwise_command
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
|
||||
# Early exit if no mode change
|
||||
if hvac_mode == self.hvac_mode:
|
||||
return
|
||||
|
||||
# Adam only: set to HVACMode.OFF
|
||||
api = self.coordinator.api
|
||||
current_schedule = self.device.get("select_schedule")
|
||||
|
||||
# OFF: single API call
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self._api.set_regulation_mode(hvac_mode.value)
|
||||
await api.set_regulation_mode(hvac_mode.value)
|
||||
return
|
||||
|
||||
current_schedule = self.device.get("select_schedule")
|
||||
schedule_is_active = current_schedule not in (None, "off")
|
||||
desired_schedule = (
|
||||
current_schedule if schedule_is_active else self._last_active_schedule
|
||||
)
|
||||
# Adam only: transition from HVACMode.OFF
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
if hvac_mode == HVACMode.AUTO:
|
||||
_check_for_schedule(schedule_is_active, self._last_active_schedule)
|
||||
await self._api.set_schedule_state(
|
||||
self._location, STATE_ON, desired_schedule
|
||||
)
|
||||
await self._api.set_regulation_mode(self._previous_action_mode)
|
||||
return
|
||||
# Manual mode (heat/cool/heat_cool) without a schedule: set regulation only
|
||||
if (
|
||||
current_schedule is None
|
||||
and hvac_mode != HVACMode.AUTO
|
||||
and (
|
||||
regulation := self._regulation_mode_for_hvac(hvac_mode)
|
||||
or self._previous_action_mode
|
||||
)
|
||||
):
|
||||
await api.set_regulation_mode(regulation)
|
||||
return
|
||||
|
||||
# Transition to manual mode
|
||||
if schedule_is_active:
|
||||
await self._api.set_schedule_state(
|
||||
# Manual mode: ensure regulation and turn off schedule when needed
|
||||
if hvac_mode in (HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL):
|
||||
regulation = self._regulation_mode_for_hvac(hvac_mode) or (
|
||||
self._previous_action_mode
|
||||
if self.hvac_mode in (HVACMode.HEAT_COOL, HVACMode.OFF)
|
||||
else None
|
||||
)
|
||||
if regulation:
|
||||
await api.set_regulation_mode(regulation)
|
||||
|
||||
if (
|
||||
self.hvac_mode == HVACMode.OFF and current_schedule not in (None, "off")
|
||||
) or (self.hvac_mode == HVACMode.AUTO and current_schedule is not None):
|
||||
await api.set_schedule_state(
|
||||
self._location, STATE_OFF, current_schedule
|
||||
)
|
||||
self._last_active_schedule = current_schedule
|
||||
regulation = self._regulation_mode_for_hvac(hvac_mode)
|
||||
await self._api.set_regulation_mode(regulation)
|
||||
return
|
||||
|
||||
# Common - transition from auto = schedule off
|
||||
if self.hvac_mode == HVACMode.AUTO:
|
||||
await self._api.set_schedule_state(
|
||||
self._location, STATE_OFF, current_schedule
|
||||
# AUTO: restore schedule and regulation
|
||||
desired_schedule = current_schedule
|
||||
if desired_schedule and desired_schedule != "off":
|
||||
self._last_active_schedule = desired_schedule
|
||||
elif desired_schedule == "off":
|
||||
desired_schedule = self._last_active_schedule
|
||||
|
||||
if not desired_schedule:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=ERROR_NO_SCHEDULE,
|
||||
)
|
||||
self._last_active_schedule = current_schedule
|
||||
return
|
||||
|
||||
# Common - transition to auto = schedule on
|
||||
_check_for_schedule(schedule_is_active, self._last_active_schedule)
|
||||
await self._api.set_schedule_state(self._location, STATE_ON, desired_schedule)
|
||||
if self._previous_action_mode:
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
await api.set_regulation_mode(self._previous_action_mode)
|
||||
await api.set_schedule_state(self._location, STATE_ON, desired_schedule)
|
||||
|
||||
@plugwise_command
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode."""
|
||||
await self._api.set_preset(self._location, preset_mode)
|
||||
await self.coordinator.api.set_preset(self._location, preset_mode)
|
||||
|
||||
@@ -105,9 +105,7 @@ async def async_setup_entry(
|
||||
RAPTPillBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class RAPTPillBluetoothSensorEntity(
|
||||
|
||||
@@ -207,9 +207,7 @@ async def async_setup_entry(
|
||||
RuuvitagBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class RuuvitagBluetoothSensorEntity(
|
||||
|
||||
@@ -112,9 +112,7 @@ async def async_setup_entry(
|
||||
SensirionBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class SensirionBluetoothSensorEntity(
|
||||
|
||||
@@ -123,9 +123,7 @@ async def async_setup_entry(
|
||||
SensorProBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class SensorProBluetoothSensorEntity(
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"quality_scale": "bronze",
|
||||
"requirements": [
|
||||
"defusedxml==0.7.1",
|
||||
"soco==0.31.1",
|
||||
"soco==0.30.15",
|
||||
"sonos-websocket==0.1.3"
|
||||
],
|
||||
"ssdp": [
|
||||
|
||||
@@ -119,9 +119,7 @@ async def async_setup_entry(
|
||||
ThermoBeaconBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class ThermoBeaconBluetoothSensorEntity(
|
||||
|
||||
@@ -92,9 +92,7 @@ async def async_setup_entry(
|
||||
TiltBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class TiltBluetoothSensorEntity(
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiounifi==91"]
|
||||
"requirements": ["aiounifi==90"]
|
||||
}
|
||||
|
||||
@@ -519,11 +519,7 @@ async def async_setup_entry(
|
||||
VictronBLESensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(
|
||||
processor, VictronBLESensorEntityDescription
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class VictronBLESensorEntity(PassiveBluetoothProcessorEntity, SensorEntity):
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Automatically generated by gen_requirements_all.py, do not edit
|
||||
|
||||
aiodhcpwatcher==1.2.7
|
||||
aiodhcpwatcher==1.2.6
|
||||
aiodiscover==3.2.3
|
||||
aiodns==4.0.4
|
||||
aiogithubapi==26.0.0
|
||||
aiohttp-asyncmdnsresolver==0.2.0
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
aiohttp==3.13.5
|
||||
aiohttp_cors==0.8.1
|
||||
@@ -22,7 +22,7 @@ awesomeversion==25.8.0
|
||||
bcrypt==5.0.0
|
||||
bleak-retry-connector==4.6.1
|
||||
bleak==3.0.2
|
||||
bluetooth-adapters==2.3.0
|
||||
bluetooth-adapters==2.2.0
|
||||
bluetooth-auto-recovery==1.6.4
|
||||
bluetooth-data-tools==1.29.18
|
||||
cached-ipaddress==1.1.1
|
||||
@@ -30,12 +30,12 @@ certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
cryptography==48.0.0
|
||||
dbus-fast==5.0.11
|
||||
dbus-fast==5.0.9
|
||||
file-read-backwards==2.0.0
|
||||
fnv-hash-fast==2.0.3
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==6.7.4
|
||||
habluetooth==6.7.3
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
@@ -68,7 +68,7 @@ SQLAlchemy==2.0.49
|
||||
standard-aifc==3.13.0
|
||||
standard-telnetlib==3.13.0
|
||||
typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==2.2.9
|
||||
ulid-transform==2.2.1
|
||||
urllib3>=2.0
|
||||
uv==0.11.14
|
||||
voluptuous-openapi==0.3.0
|
||||
|
||||
@@ -99,7 +99,6 @@ Every check has a code following the
|
||||
| `W7407` | [`home-assistant-config-flow-polling-field`](#w7407-home-assistant-config-flow-polling-field) | Config flow should not include polling interval fields |
|
||||
| `W7408` | [`home-assistant-config-flow-name-field`](#w7408-home-assistant-config-flow-name-field) | Config flow should not include name fields |
|
||||
| `R7402` | [`home-assistant-unused-test-fixture-argument`](#r7402-home-assistant-unused-test-fixture-argument) | Unused test function argument should use `@pytest.mark.usefixtures` |
|
||||
| `W7422` | [`home-assistant-tests-direct-async-setup`](#w7422-home-assistant-tests-direct-async-setup) | Tests should not call an integration's `async_setup` directly |
|
||||
|
||||
|
||||
## `home_assistant_logger` checker
|
||||
@@ -341,22 +340,3 @@ only needed for its side effects.
|
||||
|
||||
This rule only applies to `test_*` functions, not to fixture functions.
|
||||
|
||||
|
||||
|
||||
## `home_assistant_tests_direct_async_setup` checker
|
||||
|
||||
Detects tests that call an integration's `async_setup` directly.
|
||||
|
||||
### `W7422`: `home-assistant-tests-direct-async-setup`
|
||||
|
||||
Tests should not invoke an integration's `async_setup` from
|
||||
`__init__.py` directly. Instead, tests should let Home Assistant drive
|
||||
the setup through the normal pipeline:
|
||||
|
||||
* For integrations with config entries, add a `MockConfigEntry` and
|
||||
call `await hass.config_entries.async_setup(entry.entry_id)`.
|
||||
* For integrations without config entries (system integrations), use
|
||||
`await async_setup_component(hass, DOMAIN, {...})` from
|
||||
`homeassistant.setup`.
|
||||
|
||||
See [epic #79](https://github.com/home-assistant/epics/issues/79).
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
"""Checker for direct calls to ``async_setup`` from tests.
|
||||
|
||||
Tests should not invoke an integration's ``async_setup`` directly.
|
||||
Instead, tests should let Home Assistant perform the setup via the
|
||||
normal pipeline:
|
||||
|
||||
* For integrations with config entries, add a ``MockConfigEntry`` and
|
||||
call ``await hass.config_entries.async_setup(entry.entry_id)``.
|
||||
* For integrations without config entries (system integrations), use
|
||||
``await async_setup_component(hass, DOMAIN, {...})`` from
|
||||
``homeassistant.setup``.
|
||||
|
||||
This checker flags any ``await <something>.async_setup(...)`` or
|
||||
``await async_setup(...)`` call in a test module whose target resolves
|
||||
to a function defined in an integration's ``__init__`` module under
|
||||
``homeassistant.components.*``.
|
||||
"""
|
||||
|
||||
import astroid
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
from pylint_home_assistant.helpers.module_info import is_test_module, parse_module
|
||||
|
||||
|
||||
def _is_integration_async_setup(call: nodes.Call) -> bool:
|
||||
"""Return True if *call* targets an integration's ``async_setup``."""
|
||||
func = call.func
|
||||
if isinstance(func, nodes.Attribute):
|
||||
if func.attrname != "async_setup":
|
||||
return False
|
||||
elif isinstance(func, nodes.Name):
|
||||
if func.name != "async_setup":
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
try:
|
||||
inferred_values = list(func.infer())
|
||||
except astroid.InferenceError, astroid.AstroidError:
|
||||
return False
|
||||
|
||||
seen_qnames: set[str] = set()
|
||||
for inferred in inferred_values:
|
||||
if inferred is astroid.Uninferable:
|
||||
continue
|
||||
if not isinstance(inferred, (nodes.FunctionDef, nodes.AsyncFunctionDef)):
|
||||
continue
|
||||
qname = inferred.qname()
|
||||
if not qname or qname in seen_qnames:
|
||||
continue
|
||||
seen_qnames.add(qname)
|
||||
# qname is the function's fully-qualified name, e.g.
|
||||
# ``homeassistant.components.sun.async_setup``. Strip the function
|
||||
# name to get the module and parse it.
|
||||
module_qname = qname.rsplit(".", 1)[0]
|
||||
parsed = parse_module(module_qname)
|
||||
if parsed is None:
|
||||
continue
|
||||
# ``async_setup`` lives in the integration's ``__init__``.
|
||||
if parsed.module is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DirectAsyncSetup(BaseChecker):
|
||||
"""Checker for direct calls to async_setup in tests."""
|
||||
|
||||
name = "home_assistant_tests_direct_async_setup"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"W7422": (
|
||||
"Do not call `async_setup` directly from tests; set up a "
|
||||
"`MockConfigEntry` via `hass.config_entries.async_setup` or use "
|
||||
"`async_setup_component` instead",
|
||||
"home-assistant-tests-direct-async-setup",
|
||||
"Used when a test module calls an integration's `async_setup` "
|
||||
"directly. Tests should let Home Assistant drive the setup so "
|
||||
"the full setup pipeline is exercised.",
|
||||
),
|
||||
}
|
||||
options = ()
|
||||
|
||||
_in_test_module: bool = False
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Record whether the current module is a test module."""
|
||||
self._in_test_module = is_test_module(node.name)
|
||||
|
||||
def visit_call(self, node: nodes.Call) -> None:
|
||||
"""Flag direct calls to an integration's async_setup."""
|
||||
if not self._in_test_module:
|
||||
return
|
||||
if _is_integration_async_setup(node):
|
||||
self.add_message(
|
||||
"home-assistant-tests-direct-async-setup",
|
||||
node=node,
|
||||
)
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(DirectAsyncSetup(linter))
|
||||
+2
-2
@@ -31,7 +31,7 @@ dependencies = [
|
||||
"aiohttp==3.13.5",
|
||||
"aiohttp_cors==0.8.1",
|
||||
"aiohttp-fast-zlib==0.3.0",
|
||||
"aiohttp-asyncmdnsresolver==0.2.0",
|
||||
"aiohttp-asyncmdnsresolver==0.1.1",
|
||||
"aiozoneinfo==0.2.3",
|
||||
"annotatedyaml==1.0.2",
|
||||
"astral==2.2",
|
||||
@@ -72,7 +72,7 @@ dependencies = [
|
||||
"standard-aifc==3.13.0",
|
||||
"standard-telnetlib==3.13.0",
|
||||
"typing-extensions>=4.15.0,<5.0",
|
||||
"ulid-transform==2.2.9",
|
||||
"ulid-transform==2.2.1",
|
||||
"urllib3>=2.0",
|
||||
"uv==0.11.14",
|
||||
"voluptuous==0.15.2",
|
||||
|
||||
Generated
+2
-2
@@ -5,7 +5,7 @@
|
||||
# Home Assistant Core
|
||||
aiodns==4.0.4
|
||||
aiogithubapi==26.0.0
|
||||
aiohttp-asyncmdnsresolver==0.2.0
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
aiohttp==3.13.5
|
||||
aiohttp_cors==0.8.1
|
||||
@@ -53,7 +53,7 @@ SQLAlchemy==2.0.49
|
||||
standard-aifc==3.13.0
|
||||
standard-telnetlib==3.13.0
|
||||
typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==2.2.9
|
||||
ulid-transform==2.2.1
|
||||
urllib3>=2.0
|
||||
uv==0.11.14
|
||||
voluptuous-openapi==0.3.0
|
||||
|
||||
Generated
+8
-8
@@ -233,7 +233,7 @@ aiocentriconnect==0.2.3
|
||||
aiocomelit==2.0.3
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.7
|
||||
aiodhcpwatcher==1.2.6
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==3.2.3
|
||||
@@ -438,7 +438,7 @@ aiotedee==0.3.0
|
||||
aiotractive==1.0.3
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==91
|
||||
aiounifi==90
|
||||
|
||||
# homeassistant.components.usb
|
||||
aiousbwatcher==1.1.2
|
||||
@@ -675,7 +675,7 @@ bluecurrent-api==1.3.2
|
||||
bluemaestro-ble==0.4.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==2.3.0
|
||||
bluetooth-adapters==2.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.6.4
|
||||
@@ -797,7 +797,7 @@ datadog==0.52.0
|
||||
datapoint==0.12.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==5.0.11
|
||||
dbus-fast==5.0.9
|
||||
|
||||
# homeassistant.components.debugpy
|
||||
debugpy==1.8.17
|
||||
@@ -1213,7 +1213,7 @@ ha-xthings-cloud==1.0.5
|
||||
habiticalib==0.4.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==6.7.4
|
||||
habluetooth==6.7.3
|
||||
|
||||
# homeassistant.components.hanna
|
||||
hanna-cloud==0.0.7
|
||||
@@ -1320,7 +1320,7 @@ ical==13.2.4
|
||||
icalendar==6.3.1
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0.4
|
||||
icmplib==3.0
|
||||
|
||||
# homeassistant.components.idasen_desk
|
||||
idasen-ha==2.6.5
|
||||
@@ -1362,7 +1362,7 @@ influxdb==5.3.1
|
||||
infrared-protocols==5.6.0
|
||||
|
||||
# homeassistant.components.inkbird
|
||||
inkbird-ble==1.4.0
|
||||
inkbird-ble==1.2.3
|
||||
|
||||
# homeassistant.components.insteon
|
||||
insteon-frontend-home-assistant==0.6.2
|
||||
@@ -3018,7 +3018,7 @@ smart-meter-texas==0.5.5
|
||||
snapcast==2.3.7
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.31.1
|
||||
soco==0.30.15
|
||||
|
||||
# homeassistant.components.solaredge_local
|
||||
solaredge-local==0.2.3
|
||||
|
||||
@@ -140,7 +140,6 @@ def fixture_request(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
port_management_payload: dict[str, Any],
|
||||
param_properties_payload: str,
|
||||
param_properties_status_code: int,
|
||||
param_ports_payload: str,
|
||||
mqtt_status_code: int,
|
||||
) -> Callable[[str], None]:
|
||||
@@ -150,15 +149,12 @@ def fixture_request(
|
||||
def _url_pattern(path: str) -> re.Pattern[str]:
|
||||
return re.compile(rf"^https?://{re.escape(host)}(?::\d+)?{path}$")
|
||||
|
||||
def _text_response(
|
||||
url: URL, text: str, status: int = 200
|
||||
) -> AiohttpClientMockResponse:
|
||||
def _text_response(url: URL, text: str) -> AiohttpClientMockResponse:
|
||||
return AiohttpClientMockResponse(
|
||||
"post",
|
||||
url,
|
||||
text=text,
|
||||
headers={"Content-Type": "text/plain"},
|
||||
status=status,
|
||||
)
|
||||
|
||||
async def _param_cgi_response(
|
||||
@@ -176,9 +172,7 @@ def fixture_request(
|
||||
if group == "root.Output":
|
||||
return _text_response(url, PORTS_RESPONSE)
|
||||
if group == "root.Properties":
|
||||
return _text_response(
|
||||
url, param_properties_payload, param_properties_status_code
|
||||
)
|
||||
return _text_response(url, param_properties_payload)
|
||||
if group == "root.PTZ":
|
||||
return _text_response(url, PTZ_RESPONSE)
|
||||
if group == "root.StreamProfile":
|
||||
@@ -282,15 +276,9 @@ def fixture_param_ports_data() -> str:
|
||||
return PORTS_RESPONSE
|
||||
|
||||
|
||||
@pytest.fixture(name="param_properties_status_code")
|
||||
def fixture_param_properties_status_code() -> int:
|
||||
"""Property parameter status code."""
|
||||
return 200
|
||||
|
||||
|
||||
@pytest.fixture(name="mqtt_status_code")
|
||||
def fixture_mqtt_status_code() -> int:
|
||||
"""MQTT status code."""
|
||||
"""Property parameter data."""
|
||||
return 200
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import API_DISCOVERY_BASIC_DEVICE_INFO, DEFAULT_HOST, MAC, MODEL, NAME
|
||||
from .const import DEFAULT_HOST, MAC, MODEL, NAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -146,63 +146,6 @@ async def test_flow_fails_on_api(
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_default_requests")
|
||||
@pytest.mark.parametrize("param_properties_status_code", [404])
|
||||
async def test_flow_aborts_if_no_serial_number(hass: HomeAssistant) -> None:
|
||||
"""Test that config flow aborts if property_handler is not initialized and no serial is found."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_PROTOCOL: "http",
|
||||
CONF_HOST: "1.2.3.4",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_PORT: 80,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_serial_number"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_default_requests")
|
||||
@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO])
|
||||
async def test_flow_succeeds_with_basic_device_info(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that config flow succeeds when basic device info is present (positive path)."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_PROTOCOL: "http",
|
||||
CONF_HOST: "1.2.3.4",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_PORT: 80,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"M1065-LW - {MAC}"
|
||||
assert result["data"][CONF_HOST] == "1.2.3.4"
|
||||
assert result["data"][CONF_MODEL] == "M1065-LW"
|
||||
assert result["data"][CONF_NAME] == f"M1065-LW - {MAC}"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_default_requests")
|
||||
async def test_flow_create_entry_multiple_existing_entries_of_same_model(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Test bluetooth diagnostics."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
from bleak.backends.scanner import AdvertisementData, BLEDevice
|
||||
@@ -44,7 +43,6 @@ class FakeHaScanner(FakeScannerMixin, HaScanner):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="Requires Linux Bluetooth stack")
|
||||
@patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner)
|
||||
@pytest.mark.usefixtures("enable_bluetooth", "two_adapters")
|
||||
async def test_diagnostics(
|
||||
@@ -257,7 +255,7 @@ async def test_diagnostics(
|
||||
"type": "FakeHaScanner",
|
||||
"current_mode": {
|
||||
"__type": "<enum 'BluetoothScanningMode'>",
|
||||
"repr": "<BluetoothScanningMode.PASSIVE: 'passive'>",
|
||||
"repr": "<BluetoothScanningMode.AUTO: 'auto'>",
|
||||
},
|
||||
"requested_mode": {
|
||||
"__type": "<enum 'BluetoothScanningMode'>",
|
||||
@@ -327,10 +325,8 @@ async def test_diagnostics_macos(
|
||||
"Core Bluetooth": {
|
||||
"adapter_type": None,
|
||||
"address": "00:00:00:00:00:00",
|
||||
"advertise": True,
|
||||
"manufacturer": "Apple",
|
||||
"passive_scan": False,
|
||||
"powered": True,
|
||||
"product": "Unknown MacOS Model",
|
||||
"product_id": "Unknown",
|
||||
"sw_version": ANY,
|
||||
@@ -350,10 +346,8 @@ async def test_diagnostics_macos(
|
||||
"Core Bluetooth": {
|
||||
"adapter_type": None,
|
||||
"address": "00:00:00:00:00:00",
|
||||
"advertise": True,
|
||||
"manufacturer": "Apple",
|
||||
"passive_scan": False,
|
||||
"powered": True,
|
||||
"product": "Unknown MacOS Model",
|
||||
"product_id": "Unknown",
|
||||
"sw_version": ANY,
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.device_tracker import (
|
||||
ATTR_IP,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
CONF_ASSOCIATED_ZONE,
|
||||
DOMAIN,
|
||||
SourceType,
|
||||
)
|
||||
@@ -35,7 +36,11 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -893,6 +898,322 @@ async def test_base_scanner_entity_in_zones_when_connected(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_option(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test the associated_zone entity option overrides which zone in_zones reports.
|
||||
|
||||
The scanner reports being connected to a non-default zone; state and in_zones
|
||||
must follow the configured zone, and a zone enclosing the configured one is
|
||||
included in in_zones too.
|
||||
"""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"zone.kitchen",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Default: no option set -> associated with zone.home.
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_HOME
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.home"]
|
||||
|
||||
# Set the option -> associated_zone replaces zone.home; zone.home now shows
|
||||
# up via the enclosing-zones lookup.
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.kitchen"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert base_scanner_entity._scanner_option_associated_zone == "zone.kitchen"
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
# zone.kitchen is the configured zone -> state is the zone's name.
|
||||
assert entity_state.state == "kitchen"
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
|
||||
|
||||
# Clearing the option falls back to zone.home.
|
||||
entity_registry.async_update_entity_options(entity_id, DOMAIN, None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert base_scanner_entity._scanner_option_associated_zone == "zone.home"
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_HOME
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.home"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_removed_after_set(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test scanner state and repair issue when associated zone is removed.
|
||||
|
||||
When the user picks a zone via the associated_zone option and then deletes
|
||||
that zone, the scanner falls back to ``state == "unknown"`` and a repair
|
||||
issue is opened prompting the user to reconfigure.
|
||||
"""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"zone.kitchen",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.kitchen"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Sanity check before removal.
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == "kitchen"
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
|
||||
issue_id = f"associated_zone_missing_{entity_id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
# Remove the associated zone.
|
||||
hass.states.async_remove("zone.kitchen")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_UNKNOWN
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == []
|
||||
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert issue is not None
|
||||
assert issue.severity is ir.IssueSeverity.WARNING
|
||||
assert issue.translation_key == "associated_zone_missing"
|
||||
assert issue.translation_placeholders == {
|
||||
"entity_id": entity_id,
|
||||
"zone": "zone.kitchen",
|
||||
}
|
||||
|
||||
# Restore the zone -> issue is cleared, state recovers.
|
||||
hass.states.async_set(
|
||||
"zone.kitchen",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == "kitchen"
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_missing_at_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test repair issue is created when the configured zone is missing at setup."""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Pre-register the entity option pointing at a zone that does not exist.
|
||||
entity_registry.async_get_or_create(
|
||||
DOMAIN,
|
||||
TEST_DOMAIN,
|
||||
base_scanner_entity.unique_id,
|
||||
suggested_object_id="entity1",
|
||||
)
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.never_existed"},
|
||||
)
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_UNKNOWN
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == []
|
||||
issue_id = f"associated_zone_missing_{entity_id}"
|
||||
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert issue is not None
|
||||
assert issue.translation_placeholders == {
|
||||
"entity_id": entity_id,
|
||||
"zone": "zone.never_existed",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_issue_cleared_on_option_change(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test the repair issue is cleared when the user clears the option."""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.never_existed"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue_id = f"associated_zone_missing_{entity_id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
# Clearing the option restores the default and clears the repair issue.
|
||||
entity_registry.async_update_entity_options(entity_id, DOMAIN, None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_HOME
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_issue_cleared_on_unload(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test the repair issue is cleared when the entity is removed from hass."""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.never_existed"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue_id = f"associated_zone_missing_{entity_id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_option_set_before_add(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test associated_zone option set before the entity is added is honored."""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"zone.kitchen",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Pre-register the entity with the option set before the platform is set up.
|
||||
entity_registry.async_get_or_create(
|
||||
DOMAIN,
|
||||
TEST_DOMAIN,
|
||||
base_scanner_entity.unique_id,
|
||||
suggested_object_id="entity1",
|
||||
)
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.kitchen"},
|
||||
)
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert base_scanner_entity._scanner_option_associated_zone == "zone.kitchen"
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == "kitchen"
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ip_address", "mac_address", "hostname"),
|
||||
[("0.0.0.0", "ad:de:ef:be:ed:fe", "test.hostname.org")],
|
||||
|
||||
@@ -425,7 +425,6 @@ async def test_multiple_devices(hass: HomeAssistant) -> None:
|
||||
"sense_energy.SenseLink",
|
||||
return_value=Mock(start=AsyncMock(), close=AsyncMock()),
|
||||
):
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup
|
||||
assert await emulated_kasa.async_setup(hass, CONFIG) is True
|
||||
await hass.async_block_till_done()
|
||||
await emulated_kasa.validate_configs(hass, config)
|
||||
|
||||
@@ -6,17 +6,10 @@ from aioesphomeapi import (
|
||||
InfraredCapability,
|
||||
InfraredInfo,
|
||||
)
|
||||
from aioesphomeapi.client import InfraredRFReceiveEventModel
|
||||
from infrared_protocols.commands.nec import NECCommand
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import infrared
|
||||
from homeassistant.components.infrared import (
|
||||
DATA_COMPONENT,
|
||||
InfraredDeviceClass,
|
||||
InfraredReceivedSignal,
|
||||
InfraredReceiverEntity,
|
||||
)
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -40,47 +33,32 @@ async def _mock_ir_device(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("capabilities", "expected_device_class", "emitter_count", "receiver_count"),
|
||||
("capabilities", "entity_created"),
|
||||
[
|
||||
pytest.param(
|
||||
InfraredCapability.TRANSMITTER,
|
||||
InfraredDeviceClass.EMITTER,
|
||||
1,
|
||||
0,
|
||||
id="transmitter",
|
||||
),
|
||||
pytest.param(
|
||||
InfraredCapability.RECEIVER,
|
||||
InfraredDeviceClass.RECEIVER,
|
||||
0,
|
||||
1,
|
||||
id="receiver",
|
||||
),
|
||||
(InfraredCapability.TRANSMITTER, True),
|
||||
(InfraredCapability.RECEIVER, False),
|
||||
(InfraredCapability.TRANSMITTER | InfraredCapability.RECEIVER, True),
|
||||
(InfraredCapability(0), False),
|
||||
],
|
||||
)
|
||||
async def test_infrared_entity_single_capability(
|
||||
async def test_infrared_entity_transmitter(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
capabilities: InfraredCapability,
|
||||
expected_device_class: InfraredDeviceClass,
|
||||
emitter_count: int,
|
||||
receiver_count: int,
|
||||
entity_created: bool,
|
||||
) -> None:
|
||||
"""Test infrared entity is created with the right device class per capability."""
|
||||
"""Test infrared entity with transmitter capability is created."""
|
||||
await _mock_ir_device(mock_esphome_device, mock_client, capabilities)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert (state is not None) == (expected_device_class is not None)
|
||||
assert state.attributes["device_class"] == expected_device_class
|
||||
assert (state is not None) == entity_created
|
||||
|
||||
emitters = infrared.async_get_emitters(hass)
|
||||
assert len(emitters) == emitter_count
|
||||
receivers = infrared.async_get_receivers(hass)
|
||||
assert len(receivers) == receiver_count
|
||||
assert (len(emitters) == 1) == entity_created
|
||||
|
||||
|
||||
async def test_infrared_entity_dual_capability(
|
||||
async def test_infrared_multiple_entities_mixed_capabilities(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
@@ -99,6 +77,12 @@ async def test_infrared_entity_dual_capability(
|
||||
name="IR Receiver",
|
||||
capabilities=InfraredCapability.RECEIVER,
|
||||
),
|
||||
InfraredInfo(
|
||||
object_id="ir_transceiver",
|
||||
key=3,
|
||||
name="IR Transceiver",
|
||||
capabilities=InfraredCapability.TRANSMITTER | InfraredCapability.RECEIVER,
|
||||
),
|
||||
]
|
||||
await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
@@ -106,18 +90,13 @@ async def test_infrared_entity_dual_capability(
|
||||
states=[],
|
||||
)
|
||||
|
||||
transmitter_state = hass.states.get("infrared.test_ir_transmitter")
|
||||
assert transmitter_state is not None
|
||||
assert transmitter_state.attributes["device_class"] == InfraredDeviceClass.EMITTER
|
||||
|
||||
receiver_state = hass.states.get("infrared.test_ir_receiver")
|
||||
assert receiver_state is not None
|
||||
assert receiver_state.attributes["device_class"] == InfraredDeviceClass.RECEIVER
|
||||
# Only transmitter and transceiver should be created
|
||||
assert hass.states.get("infrared.test_ir_transmitter") is not None
|
||||
assert hass.states.get("infrared.test_ir_receiver") is None
|
||||
assert hass.states.get("infrared.test_ir_transceiver") is not None
|
||||
|
||||
emitters = infrared.async_get_emitters(hass)
|
||||
assert len(emitters) == 1
|
||||
receivers = infrared.async_get_receivers(hass)
|
||||
assert len(receivers) == 1
|
||||
assert len(emitters) == 2
|
||||
|
||||
|
||||
async def test_infrared_send_command_success(
|
||||
@@ -167,77 +146,6 @@ async def test_infrared_send_command_failure(
|
||||
assert exc_info.value.translation_key == "error_communicating_with_device"
|
||||
|
||||
|
||||
async def test_infrared_receiver_signal_dispatched(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
) -> None:
|
||||
"""Test receiver subscribes to events and dispatches received signals."""
|
||||
await _mock_ir_device(
|
||||
mock_esphome_device, mock_client, capabilities=InfraredCapability.RECEIVER
|
||||
)
|
||||
|
||||
mock_client.subscribe_infrared_rf_receive.assert_called_once()
|
||||
on_event = mock_client.subscribe_infrared_rf_receive.call_args[0][0]
|
||||
|
||||
receiver = hass.data[DATA_COMPONENT].get_entity(ENTITY_ID)
|
||||
assert isinstance(receiver, InfraredReceiverEntity)
|
||||
received_signals: list[InfraredReceivedSignal] = []
|
||||
receiver.async_subscribe_received_signal(received_signals.append)
|
||||
|
||||
timings = [100, -200, 300]
|
||||
on_event(InfraredRFReceiveEventModel(key=1, device_id=0, timings=timings))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert received_signals == [InfraredReceivedSignal(timings=timings)]
|
||||
assert hass.states.get(ENTITY_ID).state != STATE_UNAVAILABLE
|
||||
|
||||
# Test events with wrong key/device_id are ignored
|
||||
on_event(InfraredRFReceiveEventModel(key=99, device_id=0, timings=timings))
|
||||
on_event(InfraredRFReceiveEventModel(key=1, device_id=42, timings=timings))
|
||||
await hass.async_block_till_done()
|
||||
assert len(received_signals) == 1
|
||||
|
||||
|
||||
async def test_infrared_receiver_unsubscribes_on_unload(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
) -> None:
|
||||
"""Test receiver unsubscribes from device events when its entry is unloaded."""
|
||||
mock_device = await _mock_ir_device(
|
||||
mock_esphome_device, mock_client, capabilities=InfraredCapability.RECEIVER
|
||||
)
|
||||
|
||||
unsub = mock_client.subscribe_infrared_rf_receive.return_value
|
||||
unsub.assert_not_called()
|
||||
|
||||
await hass.config_entries.async_unload(mock_device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
unsub.assert_called_once()
|
||||
|
||||
|
||||
async def test_infrared_receiver_resubscribes_on_reconnect(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
) -> None:
|
||||
"""Test receiver re-subscribes to events after a reconnect."""
|
||||
mock_device = await _mock_ir_device(
|
||||
mock_esphome_device, mock_client, capabilities=InfraredCapability.RECEIVER
|
||||
)
|
||||
|
||||
assert mock_client.subscribe_infrared_rf_receive.call_count == 1
|
||||
|
||||
await mock_device.mock_disconnect(False)
|
||||
await hass.async_block_till_done()
|
||||
await mock_device.mock_connect()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_client.subscribe_infrared_rf_receive.call_count == 2
|
||||
|
||||
|
||||
async def test_infrared_entity_availability(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
|
||||
@@ -132,40 +132,18 @@ async def test_reload_core_conf(hass: HomeAssistant) -> None:
|
||||
@patch("homeassistant.config.os.path.isfile", Mock(return_value=True))
|
||||
@patch("homeassistant.components.homeassistant._LOGGER.error")
|
||||
@patch("homeassistant.core_config.async_process_ha_core_config")
|
||||
@pytest.mark.parametrize(
|
||||
("files_patch", "expected_error"),
|
||||
[
|
||||
(
|
||||
{config.YAML_CONFIG_FILE: yaml.dump(["invalid", "config"])},
|
||||
"YAML file .*configuration.yaml does not contain a dict",
|
||||
),
|
||||
({"not_existing": "blabla"}, "File not found: .*configuration.yaml"),
|
||||
],
|
||||
)
|
||||
async def test_reload_core_with_wrong_conf(
|
||||
mock_process,
|
||||
mock_error,
|
||||
hass: HomeAssistant,
|
||||
files_patch: dict[str, str],
|
||||
expected_error: str,
|
||||
mock_process, mock_error, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test reload core conf service."""
|
||||
files = {config.YAML_CONFIG_FILE: yaml.dump(["invalid", "config"])}
|
||||
await async_setup_component(hass, ha.DOMAIN, {})
|
||||
with (
|
||||
patch_yaml_files(files_patch, True),
|
||||
pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=(
|
||||
"Failed to reload the Home Assistant Core configuration - "
|
||||
f"{expected_error}"
|
||||
),
|
||||
),
|
||||
):
|
||||
with patch_yaml_files(files, True):
|
||||
await hass.services.async_call(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, blocking=True
|
||||
)
|
||||
|
||||
assert mock_error.called is False
|
||||
assert mock_error.called
|
||||
assert mock_process.called is False
|
||||
|
||||
|
||||
|
||||
@@ -4,17 +4,15 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
from homeassistant import config
|
||||
from homeassistant.components.homeassistant import scene as ha_scene
|
||||
from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_capture_events, async_mock_service, patch_yaml_files
|
||||
from tests.common import async_capture_events, async_mock_service
|
||||
|
||||
|
||||
async def test_reload_config_service(hass: HomeAssistant) -> None:
|
||||
@@ -48,36 +46,6 @@ async def test_reload_config_service(hass: HomeAssistant) -> None:
|
||||
assert hass.states.get("scene.bye") is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("files_patch", "expected_error"),
|
||||
[
|
||||
(
|
||||
{config.YAML_CONFIG_FILE: yaml.dump(["invalid", "config"])},
|
||||
"YAML file .*configuration.yaml does not contain a dict",
|
||||
),
|
||||
({"not_existing": "blabla"}, "File not found: .*configuration.yaml"),
|
||||
],
|
||||
)
|
||||
async def test_reload_config_service_failed(
|
||||
hass: HomeAssistant, files_patch: dict[str, str], expected_error: str
|
||||
) -> None:
|
||||
"""Test error handling when the reload config service fails."""
|
||||
assert await async_setup_component(hass, "scene", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch_yaml_files(files_patch, True),
|
||||
pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=(
|
||||
"Failed to reload the Home Assistant scene platform configuration - "
|
||||
f"{expected_error}"
|
||||
),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call("scene", "reload", blocking=True)
|
||||
|
||||
|
||||
async def test_apply_service(hass: HomeAssistant) -> None:
|
||||
"""Test the apply service."""
|
||||
assert await async_setup_component(hass, "scene", {})
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
'11011': 85,
|
||||
'11016': 0,
|
||||
'11034': 100,
|
||||
'11042': 32.1,
|
||||
'142': 1.79,
|
||||
'1501': 0,
|
||||
'1502': 0,
|
||||
|
||||
@@ -5641,64 +5641,6 @@
|
||||
'state': '15.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[2][sensor.cms_sf2000_main_mos_temperature-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': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.cms_sf2000_main_mos_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Main MOS temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Main MOS temperature',
|
||||
'platform': 'indevolt',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'main_mos_temperature',
|
||||
'unique_id': 'SolidFlex2000-87654321_11042',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[2][sensor.cms_sf2000_main_mos_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'CMS-SF2000 Main MOS temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cms_sf2000_main_mos_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '32.1',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[2][sensor.cms_sf2000_main_serial_number-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
{
|
||||
"056ee145a816487eaa69243c3280f8bf": {
|
||||
"available": true,
|
||||
"binary_sensors": {
|
||||
"dhw_state": false,
|
||||
"flame_state": false,
|
||||
"heating_state": false
|
||||
},
|
||||
"dev_class": "heater_central",
|
||||
"location": "bc93488efab249e5bc54fd7e175a6f91",
|
||||
"max_dhw_temperature": {
|
||||
"lower_bound": 40.0,
|
||||
"resolution": 0.01,
|
||||
"setpoint": 60.0,
|
||||
"upper_bound": 60.0
|
||||
},
|
||||
"maximum_boiler_temperature": {
|
||||
"lower_bound": 25.0,
|
||||
"resolution": 0.01,
|
||||
"setpoint": 50.0,
|
||||
"upper_bound": 95.0
|
||||
},
|
||||
"model": "Generic heater",
|
||||
"name": "OpenTherm",
|
||||
"sensors": {
|
||||
"intended_boiler_temperature": 0.0,
|
||||
"water_temperature": 37.0
|
||||
},
|
||||
"switches": {
|
||||
"dhw_cm_switch": false
|
||||
}
|
||||
},
|
||||
"14df5c4dc8cb4ba69f9d1ac0eaf7c5c6": {
|
||||
"available": true,
|
||||
"binary_sensors": {
|
||||
"low_battery": false
|
||||
},
|
||||
"dev_class": "zone_thermostat",
|
||||
"firmware": "2025-11-10T01:00:00+01:00",
|
||||
"hardware": "1",
|
||||
"location": "f2bf9048bef64cc5b6d5110154e33c81",
|
||||
"model": "Emma Pro",
|
||||
"model_id": "170-01",
|
||||
"name": "Emma",
|
||||
"sensors": {
|
||||
"battery": 100,
|
||||
"humidity": 65.0,
|
||||
"setpoint": 20.0,
|
||||
"temperature": 19.5
|
||||
},
|
||||
"temperature_offset": {
|
||||
"lower_bound": -2.0,
|
||||
"resolution": 0.1,
|
||||
"setpoint": 0.0,
|
||||
"upper_bound": 2.0
|
||||
},
|
||||
"vendor": "Plugwise",
|
||||
"zigbee_mac_address": "60EFABFFFE89CBA0"
|
||||
},
|
||||
"1772a4ea304041adb83f357b751341ff": {
|
||||
"available": true,
|
||||
"binary_sensors": {
|
||||
"low_battery": false
|
||||
},
|
||||
"dev_class": "thermostatic_radiator_valve",
|
||||
"firmware": "2020-11-04T01:00:00+01:00",
|
||||
"hardware": "1",
|
||||
"location": "f871b8c4d63549319221e294e4f88074",
|
||||
"model": "Tom",
|
||||
"model_id": "106-03",
|
||||
"name": "Tom Badkamer",
|
||||
"sensors": {
|
||||
"battery": 60,
|
||||
"setpoint": 25.0,
|
||||
"temperature": 18.6,
|
||||
"temperature_difference": -0.4,
|
||||
"valve_position": 100.0
|
||||
},
|
||||
"temperature_offset": {
|
||||
"lower_bound": -2.0,
|
||||
"resolution": 0.1,
|
||||
"setpoint": 0.1,
|
||||
"upper_bound": 2.0
|
||||
},
|
||||
"vendor": "Plugwise",
|
||||
"zigbee_mac_address": "000D6F000C8FCBA0"
|
||||
},
|
||||
"ad4838d7d35c4d6ea796ee12ae5aedf8": {
|
||||
"dev_class": "thermostat",
|
||||
"location": "f2bf9048bef64cc5b6d5110154e33c81",
|
||||
"model": "ThermoTouch",
|
||||
"model_id": "143.1",
|
||||
"name": "Anna",
|
||||
"sensors": {
|
||||
"setpoint": 20.0,
|
||||
"temperature": 19.1
|
||||
},
|
||||
"vendor": "Plugwise"
|
||||
},
|
||||
"c9293d1d68ee48fc8843c6f0dee2b6be": {
|
||||
"dev_class": "pumping",
|
||||
"members": [
|
||||
"854f8a9b0e7e425db97f1f110e1ce4b3",
|
||||
"ad4838d7d35c4d6ea796ee12ae5aedf8"
|
||||
],
|
||||
"model": "Group",
|
||||
"name": "Vloerverwarming",
|
||||
"sensors": {
|
||||
"electricity_consumed": 45.0,
|
||||
"electricity_produced": 0.0,
|
||||
"temperature": 20.1
|
||||
},
|
||||
"vendor": "Plugwise"
|
||||
},
|
||||
"da224107914542988a88561b4452b0f6": {
|
||||
"binary_sensors": {
|
||||
"plugwise_notification": false
|
||||
},
|
||||
"dev_class": "gateway",
|
||||
"firmware": "3.9.0",
|
||||
"gateway_modes": ["away", "full", "vacation"],
|
||||
"hardware": "AME Smile 2.0 board",
|
||||
"location": "bc93488efab249e5bc54fd7e175a6f91",
|
||||
"mac_address": "D40FB201CBA0",
|
||||
"model": "Gateway",
|
||||
"model_id": "smile_open_therm",
|
||||
"name": "Adam",
|
||||
"notifications": {},
|
||||
"regulation_modes": ["bleeding_cold", "heating", "off", "bleeding_hot"],
|
||||
"select_gateway_mode": "full",
|
||||
"select_regulation_mode": "off",
|
||||
"sensors": {
|
||||
"outdoor_temperature": -1.25
|
||||
},
|
||||
"vendor": "Plugwise",
|
||||
"zigbee_mac_address": "000D6F000D5ACBA0"
|
||||
},
|
||||
"da575e9e09b947e281fb6e3ebce3b174": {
|
||||
"available": true,
|
||||
"binary_sensors": {
|
||||
"low_battery": false
|
||||
},
|
||||
"dev_class": "zone_thermometer",
|
||||
"firmware": "2020-09-01T02:00:00+02:00",
|
||||
"hardware": "1",
|
||||
"location": "f2bf9048bef64cc5b6d5110154e33c81",
|
||||
"model": "Jip",
|
||||
"model_id": "168-01",
|
||||
"name": "Jip",
|
||||
"sensors": {
|
||||
"battery": 100,
|
||||
"humidity": 65.8,
|
||||
"setpoint": 20.0,
|
||||
"temperature": 19.3
|
||||
},
|
||||
"vendor": "Plugwise",
|
||||
"zigbee_mac_address": "70AC08FFFEE1CBA0"
|
||||
},
|
||||
"e2f4322d57924fa090fbbc48b3a140dc": {
|
||||
"available": true,
|
||||
"binary_sensors": {
|
||||
"low_battery": false
|
||||
},
|
||||
"dev_class": "zone_thermostat",
|
||||
"firmware": "2016-10-10T02:00:00+02:00",
|
||||
"hardware": "255",
|
||||
"location": "f871b8c4d63549319221e294e4f88074",
|
||||
"model": "Lisa",
|
||||
"model_id": "158-01",
|
||||
"name": "Lisa Badkamer",
|
||||
"sensors": {
|
||||
"battery": 71,
|
||||
"setpoint": 15.0,
|
||||
"temperature": 17.9
|
||||
},
|
||||
"temperature_offset": {
|
||||
"lower_bound": -2.0,
|
||||
"resolution": 0.1,
|
||||
"setpoint": 0.0,
|
||||
"upper_bound": 2.0
|
||||
},
|
||||
"vendor": "Plugwise",
|
||||
"zigbee_mac_address": "000D6F000C86CBA0"
|
||||
},
|
||||
"e8ef2a01ed3b4139a53bf749204fe6b4": {
|
||||
"dev_class": "switching",
|
||||
"members": [
|
||||
"2568cc4b9c1e401495d4741a5f89bee1",
|
||||
"29542b2b6a6a4169acecc15c72a599b8"
|
||||
],
|
||||
"model": "Group",
|
||||
"name": "Test",
|
||||
"sensors": {
|
||||
"electricity_consumed": 16.5,
|
||||
"electricity_produced": 0.0
|
||||
},
|
||||
"switches": {
|
||||
"relay": true
|
||||
},
|
||||
"vendor": "Plugwise"
|
||||
},
|
||||
"f2bf9048bef64cc5b6d5110154e33c81": {
|
||||
"active_preset": "home",
|
||||
"available_schedules": [
|
||||
"Badkamer",
|
||||
"Vakantie",
|
||||
"Weekschema",
|
||||
"Test",
|
||||
"off"
|
||||
],
|
||||
"climate_mode": "off",
|
||||
"control_state": "idle",
|
||||
"dev_class": "climate",
|
||||
"model": "ThermoZone",
|
||||
"name": "Living room",
|
||||
"preset_modes": ["vacation", "no_frost", "asleep", "home", "away"],
|
||||
"select_schedule": "off",
|
||||
"select_zone_profile": "active",
|
||||
"sensors": {
|
||||
"electricity_consumed": 60.8,
|
||||
"electricity_produced": 0.0,
|
||||
"temperature": 19.1
|
||||
},
|
||||
"thermostat": {
|
||||
"lower_bound": 1.0,
|
||||
"resolution": 0.01,
|
||||
"setpoint": 20.0,
|
||||
"upper_bound": 35.0
|
||||
},
|
||||
"thermostats": {
|
||||
"primary": [
|
||||
"ad4838d7d35c4d6ea796ee12ae5aedf8",
|
||||
"14df5c4dc8cb4ba69f9d1ac0eaf7c5c6",
|
||||
"da575e9e09b947e281fb6e3ebce3b174"
|
||||
],
|
||||
"secondary": []
|
||||
},
|
||||
"vendor": "Plugwise",
|
||||
"zone_profiles": ["active", "off", "passive"]
|
||||
},
|
||||
"f871b8c4d63549319221e294e4f88074": {
|
||||
"active_preset": "vacation",
|
||||
"available_schedules": [
|
||||
"Badkamer",
|
||||
"Vakantie",
|
||||
"Weekschema",
|
||||
"Test",
|
||||
"off"
|
||||
],
|
||||
"climate_mode": "off",
|
||||
"control_state": "idle",
|
||||
"dev_class": "climate",
|
||||
"model": "ThermoZone",
|
||||
"name": "Bathroom",
|
||||
"preset_modes": ["vacation", "no_frost", "asleep", "home", "away"],
|
||||
"select_schedule": "Badkamer",
|
||||
"select_zone_profile": "passive",
|
||||
"sensors": {
|
||||
"electricity_consumed": 0.0,
|
||||
"electricity_produced": 0.0,
|
||||
"temperature": 17.9
|
||||
},
|
||||
"thermostat": {
|
||||
"lower_bound": 0.0,
|
||||
"resolution": 0.01,
|
||||
"setpoint": 15.0,
|
||||
"upper_bound": 99.9
|
||||
},
|
||||
"thermostats": {
|
||||
"primary": ["e2f4322d57924fa090fbbc48b3a140dc"],
|
||||
"secondary": ["1772a4ea304041adb83f357b751341ff"]
|
||||
},
|
||||
"vendor": "Plugwise",
|
||||
"zone_profiles": ["active", "off", "passive"]
|
||||
}
|
||||
}
|
||||
@@ -275,64 +275,6 @@ async def test_adam_2_climate_snapshot(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_adam_heating_off_schedule"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [False], indirect=True)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_adam_off_regulation_mode_change(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_adam_heat_cool: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test changing from regulation off mode."""
|
||||
mock_restore_cache_with_extra_data(
|
||||
hass,
|
||||
[
|
||||
(
|
||||
State("climate.living_room", "heat"),
|
||||
PlugwiseClimateExtraStoredData(
|
||||
last_active_schedule=None,
|
||||
previous_action_mode="heating",
|
||||
).as_dict(),
|
||||
),
|
||||
(
|
||||
State("climate.bathroom", "heat"),
|
||||
PlugwiseClimateExtraStoredData(
|
||||
last_active_schedule="Badkamer",
|
||||
previous_action_mode="heating",
|
||||
).as_dict(),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get("climate.living_room"))
|
||||
assert state.state == "off"
|
||||
|
||||
# Verify a HomeAssistantError is raised setting a schedule from regulation-off-mode with last_active_schedule = None
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVACMode.AUTO},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Verify that the active schedule is turned off when transitioning from regulation-off-mode to a manual mode
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: "climate.bathroom", ATTR_HVAC_MODE: HVACMode.HEAT},
|
||||
blocking=True,
|
||||
)
|
||||
mock_smile_adam_heat_cool.set_schedule_state.assert_called_with(
|
||||
"f871b8c4d63549319221e294e4f88074", STATE_OFF, "Badkamer"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [True], indirect=True)
|
||||
async def test_adam_3_climate_entity_attributes(
|
||||
@@ -619,26 +561,6 @@ async def test_anna_climate_entity_climate_changes(
|
||||
"standaard",
|
||||
)
|
||||
|
||||
data = mock_smile_anna.async_update.return_value
|
||||
data["3cb70739631c4d17a86b8b12e8a5161b"]["climate_mode"] = "heat"
|
||||
with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data):
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: "climate.anna", ATTR_HVAC_MODE: HVACMode.AUTO},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_smile_anna.set_schedule_state.call_count == 2
|
||||
mock_smile_anna.set_schedule_state.assert_called_with(
|
||||
"c784ee9fdab44e1395b8dee7d7a497d5",
|
||||
STATE_ON,
|
||||
"standaard",
|
||||
)
|
||||
|
||||
# Mock user deleting last schedule from app or browser
|
||||
data = mock_smile_anna.async_update.return_value
|
||||
data["3cb70739631c4d17a86b8b12e8a5161b"]["available_schedules"] = []
|
||||
|
||||
@@ -114,7 +114,6 @@ MOCK_GAMES_LOCKED = {MOCK_ID: MOCK_GAMES_DATA_LOCKED}
|
||||
|
||||
async def test_ps4_integration_setup(hass: HomeAssistant) -> None:
|
||||
"""Test PS4 integration is setup."""
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup
|
||||
await ps4.async_setup(hass, {})
|
||||
await hass.async_block_till_done()
|
||||
assert hass.data[PS4_DATA].protocol is not None
|
||||
|
||||
@@ -19,7 +19,6 @@ async def test_async_new_device_discovery_no_entry(
|
||||
"""Service should raise when no config entry exists."""
|
||||
|
||||
# Ensure the integration is set up so the service is registered
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup
|
||||
assert await async_setup(hass, {})
|
||||
|
||||
# No entries for the domain, service should raise
|
||||
@@ -35,7 +34,6 @@ async def test_async_new_device_discovery_entry_not_loaded(
|
||||
# Add a config entry but do not set it up (state is not LOADED)
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
# Ensure the integration is set up so the service is registered
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup
|
||||
assert await async_setup(hass, {})
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="Entry not loaded"):
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
"""Tests for the direct async_setup checker."""
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.tests.direct_async_setup import DirectAsyncSetup
|
||||
import pytest
|
||||
|
||||
from tests.pylint import assert_no_messages
|
||||
|
||||
# Pre-load so astroid can resolve ``async_setup`` in parsed snippets.
|
||||
astroid.MANAGER.ast_from_module_name("homeassistant.components.ps4")
|
||||
|
||||
|
||||
@pytest.fixture(name="checker")
|
||||
def checker_fixture(linter: UnittestLinter) -> DirectAsyncSetup:
|
||||
"""Fixture to provide a direct async_setup checker."""
|
||||
return DirectAsyncSetup(linter)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "module_name"),
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
async def test_setup(hass):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
""",
|
||||
"tests.components.ps4.test_init",
|
||||
id="proper_config_entry_setup_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components.ps4 import async_setup
|
||||
|
||||
async def test_setup(hass):
|
||||
await async_setup(hass, {})
|
||||
""",
|
||||
"homeassistant.components.ps4",
|
||||
id="not_a_test_module",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async def test_setup(hass):
|
||||
await some_local.async_setup(hass, {})
|
||||
""",
|
||||
"tests.components.ps4.test_init",
|
||||
id="unresolved_attribute_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
async def test_setup(hass):
|
||||
await async_setup(hass, {})
|
||||
""",
|
||||
"tests.components.ps4.test_init",
|
||||
id="local_async_setup_not_an_integration",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components.ps4 import sensor
|
||||
|
||||
async def test_setup(hass):
|
||||
await sensor.async_setup(hass, {})
|
||||
""",
|
||||
"tests.components.ps4.test_sensor",
|
||||
id="platform_async_setup_not_flagged",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_no_warning(
|
||||
linter: UnittestLinter,
|
||||
checker: DirectAsyncSetup,
|
||||
code: str,
|
||||
module_name: str,
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "module_name"),
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components.ps4 import async_setup
|
||||
|
||||
async def test_setup(hass):
|
||||
await async_setup(hass, {})
|
||||
""",
|
||||
"tests.components.ps4.test_init",
|
||||
id="direct_name_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components import ps4
|
||||
|
||||
async def test_setup(hass):
|
||||
await ps4.async_setup(hass, {})
|
||||
""",
|
||||
"tests.components.ps4.test_init",
|
||||
id="attribute_call",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_warning(
|
||||
linter: UnittestLinter,
|
||||
checker: DirectAsyncSetup,
|
||||
code: str,
|
||||
module_name: str,
|
||||
) -> None:
|
||||
"""Test cases that should trigger a warning."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-tests-direct-async-setup"
|
||||
|
||||
|
||||
def test_multiple_calls_each_flagged(
|
||||
linter: UnittestLinter,
|
||||
checker: DirectAsyncSetup,
|
||||
) -> None:
|
||||
"""Test that multiple direct calls are each flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
from homeassistant.components.ps4 import async_setup
|
||||
|
||||
async def test_a(hass):
|
||||
await async_setup(hass, {})
|
||||
|
||||
async def test_b(hass):
|
||||
await async_setup(hass, {})
|
||||
""",
|
||||
"tests.components.ps4.test_init",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 2
|
||||
Reference in New Issue
Block a user