Upgrade tplink with new platforms, features and device support (#120060)
Co-authored-by: Teemu Rytilahti <tpr@iki.fi> Co-authored-by: sdb9696 <steven.beth@gmail.com> Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Teemu R. <tpr@iki.fi>
This commit is contained in:
parent
9bc4361855
commit
4290a1fcb5
45 changed files with 6528 additions and 849 deletions
34
homeassistant/components/tplink/README.md
Normal file
34
homeassistant/components/tplink/README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# TPLink Integration
|
||||
|
||||
This document covers details that new contributors may find helpful when getting started.
|
||||
|
||||
## Modules vs Features
|
||||
|
||||
The python-kasa library which this integration depends on exposes functionality via modules and features.
|
||||
The `Module` APIs encapsulate groups of functionality provided by a device,
|
||||
e.g. Light which has multiple attributes and methods such as `set_hsv`, `brightness` etc.
|
||||
The `features` encapsulate unitary functions and allow for introspection.
|
||||
e.g. `on_since`, `voltage` etc.
|
||||
|
||||
If the integration implements a platform that presents single functions or data points, such as `sensor`,
|
||||
`button`, `switch` it uses features.
|
||||
If it's implementing a platform with more complex functionality like `light`, `fan` or `climate` it will
|
||||
use modules.
|
||||
|
||||
## Adding new entities
|
||||
|
||||
All feature-based entities are created based on the information from the upstream library.
|
||||
If you want to add new feature, it needs to be implemented at first in there.
|
||||
After the feature is exposed by the upstream library,
|
||||
it needs to be added to the `<PLATFORM>_DESCRIPTIONS` list of the corresponding platform.
|
||||
The integration logs missing descriptions on features supported by the device to help spotting them.
|
||||
|
||||
In many cases it is enough to define the `key` (corresponding to upstream `feature.id`),
|
||||
but you can pass more information for nicer user experience:
|
||||
* `device_class` and `state_class` should be set accordingly for binary_sensor and sensor
|
||||
* If no matching classes are available, you need to update `strings.json` and `icons.json`
|
||||
When doing so, do not forget to run `script/setup` to generate the translations.
|
||||
|
||||
Other information like the category and whether to enable per default are read from the feature,
|
||||
as are information about units and display precision hints.
|
||||
|
|
@ -9,14 +9,15 @@ from typing import Any
|
|||
|
||||
from aiohttp import ClientSession
|
||||
from kasa import (
|
||||
AuthenticationException,
|
||||
AuthenticationError,
|
||||
Credentials,
|
||||
Device,
|
||||
DeviceConfig,
|
||||
Discover,
|
||||
SmartDevice,
|
||||
SmartDeviceException,
|
||||
KasaException,
|
||||
)
|
||||
from kasa.httpclient import get_cookie_jar
|
||||
from kasa.iot import IotStrip
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import network
|
||||
|
@ -51,6 +52,8 @@ from .const import (
|
|||
from .coordinator import TPLinkDataUpdateCoordinator
|
||||
from .models import TPLinkData
|
||||
|
||||
type TPLinkConfigEntry = ConfigEntry[TPLinkData]
|
||||
|
||||
DISCOVERY_INTERVAL = timedelta(minutes=15)
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
@ -67,7 +70,7 @@ def create_async_tplink_clientsession(hass: HomeAssistant) -> ClientSession:
|
|||
@callback
|
||||
def async_trigger_discovery(
|
||||
hass: HomeAssistant,
|
||||
discovered_devices: dict[str, SmartDevice],
|
||||
discovered_devices: dict[str, Device],
|
||||
) -> None:
|
||||
"""Trigger config flows for discovered devices."""
|
||||
for formatted_mac, device in discovered_devices.items():
|
||||
|
@ -87,7 +90,7 @@ def async_trigger_discovery(
|
|||
)
|
||||
|
||||
|
||||
async def async_discover_devices(hass: HomeAssistant) -> dict[str, SmartDevice]:
|
||||
async def async_discover_devices(hass: HomeAssistant) -> dict[str, Device]:
|
||||
"""Discover TPLink devices on configured network interfaces."""
|
||||
|
||||
credentials = await get_credentials(hass)
|
||||
|
@ -101,7 +104,7 @@ async def async_discover_devices(hass: HomeAssistant) -> dict[str, SmartDevice]:
|
|||
)
|
||||
for address in broadcast_addresses
|
||||
]
|
||||
discovered_devices: dict[str, SmartDevice] = {}
|
||||
discovered_devices: dict[str, Device] = {}
|
||||
for device_list in await asyncio.gather(*tasks):
|
||||
for device in device_list.values():
|
||||
discovered_devices[dr.format_mac(device.mac)] = device
|
||||
|
@ -126,7 +129,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bool:
|
||||
"""Set up TPLink from a config entry."""
|
||||
host: str = entry.data[CONF_HOST]
|
||||
credentials = await get_credentials(hass)
|
||||
|
@ -135,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
if config_dict := entry.data.get(CONF_DEVICE_CONFIG):
|
||||
try:
|
||||
config = DeviceConfig.from_dict(config_dict)
|
||||
except SmartDeviceException:
|
||||
except KasaException:
|
||||
_LOGGER.warning(
|
||||
"Invalid connection type dict for %s: %s", host, config_dict
|
||||
)
|
||||
|
@ -151,10 +154,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
if credentials:
|
||||
config.credentials = credentials
|
||||
try:
|
||||
device: SmartDevice = await SmartDevice.connect(config=config)
|
||||
except AuthenticationException as ex:
|
||||
device: Device = await Device.connect(config=config)
|
||||
except AuthenticationError as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except SmartDeviceException as ex:
|
||||
except KasaException as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
device_config_dict = device.config.to_dict(
|
||||
|
@ -189,7 +192,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
parent_coordinator = TPLinkDataUpdateCoordinator(hass, device, timedelta(seconds=5))
|
||||
child_coordinators: list[TPLinkDataUpdateCoordinator] = []
|
||||
|
||||
if device.is_strip:
|
||||
# The iot HS300 allows a limited number of concurrent requests and fetching the
|
||||
# emeter information requires separate ones so create child coordinators here.
|
||||
if isinstance(device, IotStrip):
|
||||
child_coordinators = [
|
||||
# The child coordinators only update energy data so we can
|
||||
# set a longer update interval to avoid flooding the device
|
||||
|
@ -197,27 +202,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
for child in device.children
|
||||
]
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = TPLinkData(
|
||||
parent_coordinator, child_coordinators
|
||||
)
|
||||
entry.runtime_data = TPLinkData(parent_coordinator, child_coordinators)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hass_data: dict[str, Any] = hass.data[DOMAIN]
|
||||
data: TPLinkData = hass_data[entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
device = data.parent_coordinator.device
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass_data.pop(entry.entry_id)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
await device.protocol.close()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
def legacy_device_id(device: SmartDevice) -> str:
|
||||
def legacy_device_id(device: Device) -> str:
|
||||
"""Convert the device id so it matches what was used in the original version."""
|
||||
device_id: str = device.device_id
|
||||
# Plugs are prefixed with the mac in python-kasa but not
|
||||
|
@ -227,6 +228,24 @@ def legacy_device_id(device: SmartDevice) -> str:
|
|||
return device_id.split("_")[1]
|
||||
|
||||
|
||||
def get_device_name(device: Device, parent: Device | None = None) -> str:
|
||||
"""Get a name for the device. alias can be none on some devices."""
|
||||
if device.alias:
|
||||
return device.alias
|
||||
# Return the child device type with an index if there's more than one child device
|
||||
# of the same type. i.e. Devices like the ks240 with one child of each type
|
||||
# skip the suffix
|
||||
if parent:
|
||||
devices = [
|
||||
child.device_id
|
||||
for child in parent.children
|
||||
if child.device_type is device.device_type
|
||||
]
|
||||
suffix = f" {devices.index(device.device_id) + 1}" if len(devices) > 1 else ""
|
||||
return f"{device.device_type.value.capitalize()}{suffix}"
|
||||
return f"Unnamed {device.model}"
|
||||
|
||||
|
||||
async def get_credentials(hass: HomeAssistant) -> Credentials | None:
|
||||
"""Retrieve the credentials from hass data."""
|
||||
if DOMAIN in hass.data and CONF_AUTHENTICATION in hass.data[DOMAIN]:
|
||||
|
@ -247,3 +266,67 @@ async def set_credentials(hass: HomeAssistant, username: str, password: str) ->
|
|||
def mac_alias(mac: str) -> str:
|
||||
"""Convert a MAC address to a short address for the UI."""
|
||||
return mac.replace(":", "")[-4:].upper()
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
version = config_entry.version
|
||||
minor_version = config_entry.minor_version
|
||||
|
||||
_LOGGER.debug("Migrating from version %s.%s", version, minor_version)
|
||||
|
||||
if version == 1 and minor_version < 3:
|
||||
# Previously entities on child devices added themselves to the parent
|
||||
# device and set their device id as identifiers along with mac
|
||||
# as a connection which creates a single device entry linked by all
|
||||
# identifiers. Now we create separate devices connected with via_device
|
||||
# so the identifier linkage must be removed otherwise the devices will
|
||||
# always be linked into one device.
|
||||
dev_reg = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id):
|
||||
new_identifiers: set[tuple[str, str]] | None = None
|
||||
if len(device.identifiers) > 1 and (
|
||||
mac := next(
|
||||
iter(
|
||||
[
|
||||
conn[1]
|
||||
for conn in device.connections
|
||||
if conn[0] == dr.CONNECTION_NETWORK_MAC
|
||||
]
|
||||
),
|
||||
None,
|
||||
)
|
||||
):
|
||||
for identifier in device.identifiers:
|
||||
# Previously only iot devices that use the MAC address as
|
||||
# device_id had child devices so check for mac as the
|
||||
# parent device.
|
||||
if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper():
|
||||
new_identifiers = {identifier}
|
||||
break
|
||||
if new_identifiers:
|
||||
dev_reg.async_update_device(
|
||||
device.id, new_identifiers=new_identifiers
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Replaced identifiers for device %s (%s): %s with: %s",
|
||||
device.name,
|
||||
device.model,
|
||||
device.identifiers,
|
||||
new_identifiers,
|
||||
)
|
||||
else:
|
||||
# No match on mac so raise an error.
|
||||
_LOGGER.error(
|
||||
"Unable to replace identifiers for device %s (%s): %s",
|
||||
device.name,
|
||||
device.model,
|
||||
device.identifiers,
|
||||
)
|
||||
|
||||
minor_version = 3
|
||||
hass.config_entries.async_update_entry(config_entry, minor_version=3)
|
||||
|
||||
_LOGGER.debug("Migration to version %s.%s successful", version, minor_version)
|
||||
|
||||
return True
|
||||
|
|
96
homeassistant/components/tplink/binary_sensor.py
Normal file
96
homeassistant/components/tplink/binary_sensor.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
"""Support for TPLink binary sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from kasa import Feature
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import TPLinkConfigEntry
|
||||
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TPLinkBinarySensorEntityDescription(
|
||||
BinarySensorEntityDescription, TPLinkFeatureEntityDescription
|
||||
):
|
||||
"""Base class for a TPLink feature based sensor entity description."""
|
||||
|
||||
|
||||
BINARY_SENSOR_DESCRIPTIONS: Final = (
|
||||
TPLinkBinarySensorEntityDescription(
|
||||
key="overheated",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
),
|
||||
TPLinkBinarySensorEntityDescription(
|
||||
key="battery_low",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
),
|
||||
TPLinkBinarySensorEntityDescription(
|
||||
key="cloud_connection",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
),
|
||||
# To be replaced & disabled per default by the upcoming update platform.
|
||||
TPLinkBinarySensorEntityDescription(
|
||||
key="update_available",
|
||||
device_class=BinarySensorDeviceClass.UPDATE,
|
||||
),
|
||||
TPLinkBinarySensorEntityDescription(
|
||||
key="temperature_warning",
|
||||
),
|
||||
TPLinkBinarySensorEntityDescription(
|
||||
key="humidity_warning",
|
||||
),
|
||||
TPLinkBinarySensorEntityDescription(
|
||||
key="is_open",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
),
|
||||
TPLinkBinarySensorEntityDescription(
|
||||
key="water_alert",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
),
|
||||
)
|
||||
|
||||
BINARYSENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in BINARY_SENSOR_DESCRIPTIONS}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors."""
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
children_coordinators = data.children_coordinators
|
||||
device = parent_coordinator.device
|
||||
|
||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||
device=device,
|
||||
coordinator=parent_coordinator,
|
||||
feature_type=Feature.Type.BinarySensor,
|
||||
entity_class=TPLinkBinarySensorEntity,
|
||||
descriptions=BINARYSENSOR_DESCRIPTIONS_MAP,
|
||||
child_coordinators=children_coordinators,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class TPLinkBinarySensorEntity(CoordinatedTPLinkFeatureEntity, BinarySensorEntity):
|
||||
"""Representation of a TPLink binary sensor."""
|
||||
|
||||
entity_description: TPLinkBinarySensorEntityDescription
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
self._attr_is_on = self._feature.value
|
69
homeassistant/components/tplink/button.py
Normal file
69
homeassistant/components/tplink/button.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
"""Support for TPLink button entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from kasa import Feature
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import TPLinkConfigEntry
|
||||
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TPLinkButtonEntityDescription(
|
||||
ButtonEntityDescription, TPLinkFeatureEntityDescription
|
||||
):
|
||||
"""Base class for a TPLink feature based button entity description."""
|
||||
|
||||
|
||||
BUTTON_DESCRIPTIONS: Final = [
|
||||
TPLinkButtonEntityDescription(
|
||||
key="test_alarm",
|
||||
),
|
||||
TPLinkButtonEntityDescription(
|
||||
key="stop_alarm",
|
||||
),
|
||||
]
|
||||
|
||||
BUTTON_DESCRIPTIONS_MAP = {desc.key: desc for desc in BUTTON_DESCRIPTIONS}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up buttons."""
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
children_coordinators = data.children_coordinators
|
||||
device = parent_coordinator.device
|
||||
|
||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||
device=device,
|
||||
coordinator=parent_coordinator,
|
||||
feature_type=Feature.Type.Action,
|
||||
entity_class=TPLinkButtonEntity,
|
||||
descriptions=BUTTON_DESCRIPTIONS_MAP,
|
||||
child_coordinators=children_coordinators,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class TPLinkButtonEntity(CoordinatedTPLinkFeatureEntity, ButtonEntity):
|
||||
"""Representation of a TPLink button entity."""
|
||||
|
||||
entity_description: TPLinkButtonEntityDescription
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Execute action."""
|
||||
await self._feature.set_value(True)
|
||||
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""No need to update anything."""
|
140
homeassistant/components/tplink/climate.py
Normal file
140
homeassistant/components/tplink/climate.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
"""Support for TP-Link thermostats."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from kasa import Device, DeviceType
|
||||
from kasa.smart.modules.temperaturecontrol import ThermostatState
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TEMPERATURE,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import PRECISION_WHOLE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import TPLinkConfigEntry
|
||||
from .const import UNIT_MAPPING
|
||||
from .coordinator import TPLinkDataUpdateCoordinator
|
||||
from .entity import CoordinatedTPLinkEntity, async_refresh_after
|
||||
|
||||
# Upstream state to HVACAction
|
||||
STATE_TO_ACTION = {
|
||||
ThermostatState.Idle: HVACAction.IDLE,
|
||||
ThermostatState.Heating: HVACAction.HEATING,
|
||||
ThermostatState.Off: HVACAction.OFF,
|
||||
}
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up climate entities."""
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
device = parent_coordinator.device
|
||||
|
||||
# As there are no standalone thermostats, we just iterate over the children.
|
||||
async_add_entities(
|
||||
TPLinkClimateEntity(child, parent_coordinator, parent=device)
|
||||
for child in device.children
|
||||
if child.device_type is DeviceType.Thermostat
|
||||
)
|
||||
|
||||
|
||||
class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity):
|
||||
"""Representation of a TPLink thermostat."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_precision = PRECISION_WHOLE
|
||||
|
||||
# This disables the warning for async_turn_{on,off}, can be removed later.
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
parent: Device,
|
||||
) -> None:
|
||||
"""Initialize the climate entity."""
|
||||
super().__init__(device, coordinator, parent=parent)
|
||||
self._state_feature = self._device.features["state"]
|
||||
self._mode_feature = self._device.features["thermostat_mode"]
|
||||
self._temp_feature = self._device.features["temperature"]
|
||||
self._target_feature = self._device.features["target_temperature"]
|
||||
|
||||
self._attr_min_temp = self._target_feature.minimum_value
|
||||
self._attr_max_temp = self._target_feature.maximum_value
|
||||
self._attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature.unit)]
|
||||
|
||||
@async_refresh_after
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set target temperature."""
|
||||
await self._target_feature.set_value(int(kwargs[ATTR_TEMPERATURE]))
|
||||
|
||||
@async_refresh_after
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode (heat/off)."""
|
||||
if hvac_mode is HVACMode.HEAT:
|
||||
await self._state_feature.set_value(True)
|
||||
elif hvac_mode is HVACMode.OFF:
|
||||
await self._state_feature.set_value(False)
|
||||
else:
|
||||
raise ServiceValidationError(f"Tried to set unsupported mode: {hvac_mode}")
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn heating on."""
|
||||
await self._state_feature.set_value(True)
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn heating off."""
|
||||
await self._state_feature.set_value(False)
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
self._attr_current_temperature = self._temp_feature.value
|
||||
self._attr_target_temperature = self._target_feature.value
|
||||
|
||||
self._attr_hvac_mode = (
|
||||
HVACMode.HEAT if self._state_feature.value else HVACMode.OFF
|
||||
)
|
||||
|
||||
if (
|
||||
self._mode_feature.value not in STATE_TO_ACTION
|
||||
and self._attr_hvac_action is not HVACAction.OFF
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Unknown thermostat state, defaulting to OFF: %s",
|
||||
self._mode_feature.value,
|
||||
)
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
return
|
||||
|
||||
self._attr_hvac_action = STATE_TO_ACTION[self._mode_feature.value]
|
||||
|
||||
def _get_unique_id(self) -> str:
|
||||
"""Return unique id."""
|
||||
return f"{self._device.device_id}_climate"
|
|
@ -6,13 +6,13 @@ from collections.abc import Mapping
|
|||
from typing import Any
|
||||
|
||||
from kasa import (
|
||||
AuthenticationException,
|
||||
AuthenticationError,
|
||||
Credentials,
|
||||
Device,
|
||||
DeviceConfig,
|
||||
Discover,
|
||||
SmartDevice,
|
||||
SmartDeviceException,
|
||||
TimeoutException,
|
||||
KasaException,
|
||||
TimeoutError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -55,13 +55,13 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
"""Handle a config flow for tplink."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovered_devices: dict[str, SmartDevice] = {}
|
||||
self._discovered_device: SmartDevice | None = None
|
||||
self._discovered_devices: dict[str, Device] = {}
|
||||
self._discovered_device: Device | None = None
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: dhcp.DhcpServiceInfo
|
||||
|
@ -129,9 +129,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
await self._async_try_discover_and_update(
|
||||
host, credentials, raise_on_progress=True
|
||||
)
|
||||
except AuthenticationException:
|
||||
except AuthenticationError:
|
||||
return await self.async_step_discovery_auth_confirm()
|
||||
except SmartDeviceException:
|
||||
except KasaException:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
@ -149,7 +149,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
device = await self._async_try_connect(
|
||||
self._discovered_device, credentials
|
||||
)
|
||||
except AuthenticationException:
|
||||
except AuthenticationError:
|
||||
pass # Authentication exceptions should continue to the rest of the step
|
||||
else:
|
||||
self._discovered_device = device
|
||||
|
@ -165,10 +165,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
device = await self._async_try_connect(
|
||||
self._discovered_device, credentials
|
||||
)
|
||||
except AuthenticationException as ex:
|
||||
except AuthenticationError as ex:
|
||||
errors[CONF_PASSWORD] = "invalid_auth"
|
||||
placeholders["error"] = str(ex)
|
||||
except SmartDeviceException as ex:
|
||||
except KasaException as ex:
|
||||
errors["base"] = "cannot_connect"
|
||||
placeholders["error"] = str(ex)
|
||||
else:
|
||||
|
@ -229,9 +229,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
device = await self._async_try_discover_and_update(
|
||||
host, credentials, raise_on_progress=False
|
||||
)
|
||||
except AuthenticationException:
|
||||
except AuthenticationError:
|
||||
return await self.async_step_user_auth_confirm()
|
||||
except SmartDeviceException as ex:
|
||||
except KasaException as ex:
|
||||
errors["base"] = "cannot_connect"
|
||||
placeholders["error"] = str(ex)
|
||||
else:
|
||||
|
@ -261,10 +261,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
device = await self._async_try_connect(
|
||||
self._discovered_device, credentials
|
||||
)
|
||||
except AuthenticationException as ex:
|
||||
except AuthenticationError as ex:
|
||||
errors[CONF_PASSWORD] = "invalid_auth"
|
||||
placeholders["error"] = str(ex)
|
||||
except SmartDeviceException as ex:
|
||||
except KasaException as ex:
|
||||
errors["base"] = "cannot_connect"
|
||||
placeholders["error"] = str(ex)
|
||||
else:
|
||||
|
@ -298,9 +298,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
device = await self._async_try_connect(
|
||||
self._discovered_device, credentials
|
||||
)
|
||||
except AuthenticationException:
|
||||
except AuthenticationError:
|
||||
return await self.async_step_user_auth_confirm()
|
||||
except SmartDeviceException:
|
||||
except KasaException:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
return self._async_create_entry_from_device(device)
|
||||
|
||||
|
@ -343,7 +343,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
_config_entries.flow.async_abort(flow["flow_id"])
|
||||
|
||||
@callback
|
||||
def _async_create_entry_from_device(self, device: SmartDevice) -> ConfigFlowResult:
|
||||
def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult:
|
||||
"""Create a config entry from a smart device."""
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: device.host})
|
||||
return self.async_create_entry(
|
||||
|
@ -364,7 +364,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
host: str,
|
||||
credentials: Credentials | None,
|
||||
raise_on_progress: bool,
|
||||
) -> SmartDevice:
|
||||
) -> Device:
|
||||
"""Try to discover the device and call update.
|
||||
|
||||
Will try to connect to legacy devices if discovery fails.
|
||||
|
@ -373,11 +373,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
self._discovered_device = await Discover.discover_single(
|
||||
host, credentials=credentials
|
||||
)
|
||||
except TimeoutException:
|
||||
except TimeoutError:
|
||||
# Try connect() to legacy devices if discovery fails
|
||||
self._discovered_device = await SmartDevice.connect(
|
||||
config=DeviceConfig(host)
|
||||
)
|
||||
self._discovered_device = await Device.connect(config=DeviceConfig(host))
|
||||
else:
|
||||
if self._discovered_device.config.uses_http:
|
||||
self._discovered_device.config.http_client = (
|
||||
|
@ -392,9 +390,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
async def _async_try_connect(
|
||||
self,
|
||||
discovered_device: SmartDevice,
|
||||
discovered_device: Device,
|
||||
credentials: Credentials | None,
|
||||
) -> SmartDevice:
|
||||
) -> Device:
|
||||
"""Try to connect."""
|
||||
self._async_abort_entries_match({CONF_HOST: discovered_device.host})
|
||||
|
||||
|
@ -405,7 +403,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
if config.uses_http:
|
||||
config.http_client = create_async_tplink_clientsession(self.hass)
|
||||
|
||||
self._discovered_device = await SmartDevice.connect(config=config)
|
||||
self._discovered_device = await Device.connect(config=config)
|
||||
await self.async_set_unique_id(
|
||||
dr.format_mac(self._discovered_device.mac),
|
||||
raise_on_progress=False,
|
||||
|
@ -442,10 +440,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
credentials=credentials,
|
||||
raise_on_progress=True,
|
||||
)
|
||||
except AuthenticationException as ex:
|
||||
except AuthenticationError as ex:
|
||||
errors[CONF_PASSWORD] = "invalid_auth"
|
||||
placeholders["error"] = str(ex)
|
||||
except SmartDeviceException as ex:
|
||||
except KasaException as ex:
|
||||
errors["base"] = "cannot_connect"
|
||||
placeholders["error"] = str(ex)
|
||||
else:
|
||||
|
|
|
@ -4,13 +4,16 @@ from __future__ import annotations
|
|||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import Platform, UnitOfTemperature
|
||||
|
||||
DOMAIN = "tplink"
|
||||
|
||||
DISCOVERY_TIMEOUT = 5 # Home Assistant will complain if startup takes > 10s
|
||||
CONNECT_TIMEOUT = 5
|
||||
|
||||
# Identifier used for primary control state.
|
||||
PRIMARY_STATE_ID = "state"
|
||||
|
||||
ATTR_CURRENT_A: Final = "current_a"
|
||||
ATTR_CURRENT_POWER_W: Final = "current_power_w"
|
||||
ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh"
|
||||
|
@ -18,4 +21,19 @@ ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh"
|
|||
|
||||
CONF_DEVICE_CONFIG: Final = "device_config"
|
||||
|
||||
PLATFORMS: Final = [Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS: Final = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.FAN,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
UNIT_MAPPING = {
|
||||
"celsius": UnitOfTemperature.CELSIUS,
|
||||
"fahrenheit": UnitOfTemperature.FAHRENHEIT,
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from kasa import AuthenticationException, SmartDevice, SmartDeviceException
|
||||
from kasa import AuthenticationError, Device, KasaException
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -26,7 +26,7 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
device: SmartDevice,
|
||||
device: Device,
|
||||
update_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize DataUpdateCoordinator to gather data for specific SmartPlug."""
|
||||
|
@ -47,7 +47,7 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||
"""Fetch all device and sensor data from api."""
|
||||
try:
|
||||
await self.device.update(update_children=False)
|
||||
except AuthenticationException as ex:
|
||||
except AuthenticationError as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except SmartDeviceException as ex:
|
||||
except KasaException as ex:
|
||||
raise UpdateFailed(ex) from ex
|
||||
|
|
|
@ -5,12 +5,10 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import TPLinkData
|
||||
from . import TPLinkConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
# Entry fields
|
||||
|
@ -23,6 +21,7 @@ TO_REDACT = {
|
|||
"hwId",
|
||||
"oemId",
|
||||
"deviceId",
|
||||
"id", # child id for HS300
|
||||
# Device location
|
||||
"latitude",
|
||||
"latitude_i",
|
||||
|
@ -38,14 +37,17 @@ TO_REDACT = {
|
|||
"ssid",
|
||||
"nickname",
|
||||
"ip",
|
||||
# Child device information
|
||||
"original_device_id",
|
||||
"parent_device_id",
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: TPLinkConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data: TPLinkData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
coordinator = data.parent_coordinator
|
||||
oui = format_mac(coordinator.device.mac)[:8].upper()
|
||||
return async_redact_data(
|
||||
|
|
|
@ -2,24 +2,81 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable, Callable, Coroutine, Mapping
|
||||
from dataclasses import dataclass, replace
|
||||
import logging
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from kasa import (
|
||||
AuthenticationException,
|
||||
SmartDevice,
|
||||
SmartDeviceException,
|
||||
TimeoutException,
|
||||
AuthenticationError,
|
||||
Device,
|
||||
DeviceType,
|
||||
Feature,
|
||||
KasaException,
|
||||
TimeoutError,
|
||||
)
|
||||
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import get_device_name, legacy_device_id
|
||||
from .const import (
|
||||
ATTR_CURRENT_A,
|
||||
ATTR_CURRENT_POWER_W,
|
||||
ATTR_TODAY_ENERGY_KWH,
|
||||
ATTR_TOTAL_ENERGY_KWH,
|
||||
DOMAIN,
|
||||
PRIMARY_STATE_ID,
|
||||
)
|
||||
from .coordinator import TPLinkDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Mapping from upstream category to homeassistant category
|
||||
FEATURE_CATEGORY_TO_ENTITY_CATEGORY = {
|
||||
Feature.Category.Config: EntityCategory.CONFIG,
|
||||
Feature.Category.Info: EntityCategory.DIAGNOSTIC,
|
||||
Feature.Category.Debug: EntityCategory.DIAGNOSTIC,
|
||||
}
|
||||
|
||||
# Skips creating entities for primary features supported by a specialized platform.
|
||||
# For example, we do not need a separate "state" switch for light bulbs.
|
||||
DEVICETYPES_WITH_SPECIALIZED_PLATFORMS = {
|
||||
DeviceType.Bulb,
|
||||
DeviceType.LightStrip,
|
||||
DeviceType.Dimmer,
|
||||
DeviceType.Fan,
|
||||
DeviceType.Thermostat,
|
||||
}
|
||||
|
||||
# Features excluded due to future platform additions
|
||||
EXCLUDED_FEATURES = {
|
||||
# update
|
||||
"current_firmware_version",
|
||||
"available_firmware_version",
|
||||
# fan
|
||||
"fan_speed_level",
|
||||
}
|
||||
|
||||
LEGACY_KEY_MAPPING = {
|
||||
"current": ATTR_CURRENT_A,
|
||||
"current_consumption": ATTR_CURRENT_POWER_W,
|
||||
"consumption_today": ATTR_TODAY_ENERGY_KWH,
|
||||
"consumption_total": ATTR_TOTAL_ENERGY_KWH,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TPLinkFeatureEntityDescription(EntityDescription):
|
||||
"""Base class for a TPLink feature based entity description."""
|
||||
|
||||
|
||||
def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
|
||||
func: Callable[Concatenate[_T, _P], Awaitable[None]],
|
||||
|
@ -29,7 +86,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
|
|||
async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except AuthenticationException as ex:
|
||||
except AuthenticationError as ex:
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
|
@ -39,7 +96,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
|
|||
"exc": str(ex),
|
||||
},
|
||||
) from ex
|
||||
except TimeoutException as ex:
|
||||
except TimeoutError as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_timeout",
|
||||
|
@ -48,7 +105,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
|
|||
"exc": str(ex),
|
||||
},
|
||||
) from ex
|
||||
except SmartDeviceException as ex:
|
||||
except KasaException as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_error",
|
||||
|
@ -62,24 +119,302 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
|
|||
return _async_wrap
|
||||
|
||||
|
||||
class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator]):
|
||||
class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], ABC):
|
||||
"""Common base class for all coordinated tplink entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_device: Device
|
||||
|
||||
def __init__(
|
||||
self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator
|
||||
self,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
feature: Feature | None = None,
|
||||
parent: Device | None = None,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self.device: SmartDevice = device
|
||||
self._attr_unique_id = device.device_id
|
||||
self._device: Device = device
|
||||
self._feature = feature
|
||||
|
||||
registry_device = device
|
||||
device_name = get_device_name(device, parent=parent)
|
||||
if parent and parent.device_type is not Device.Type.Hub:
|
||||
if not feature or feature.id == PRIMARY_STATE_ID:
|
||||
# Entity will be added to parent if not a hub and no feature
|
||||
# parameter (i.e. core platform like Light, Fan) or the feature
|
||||
# is the primary state
|
||||
registry_device = parent
|
||||
device_name = get_device_name(registry_device)
|
||||
else:
|
||||
# Prefix the device name with the parent name unless it is a
|
||||
# hub attached device. Sensible default for child devices like
|
||||
# strip plugs or the ks240 where the child alias makes more
|
||||
# sense in the context of the parent. i.e. Hall Ceiling Fan &
|
||||
# Bedroom Ceiling Fan; Child device aliases will be Ceiling Fan
|
||||
# and Dimmer Switch for both so should be distinguished by the
|
||||
# parent name.
|
||||
device_name = f"{get_device_name(parent)} {get_device_name(device, parent=parent)}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device.mac)},
|
||||
identifiers={(DOMAIN, str(device.device_id))},
|
||||
identifiers={(DOMAIN, str(registry_device.device_id))},
|
||||
manufacturer="TP-Link",
|
||||
model=device.model,
|
||||
name=device.alias,
|
||||
sw_version=device.hw_info["sw_ver"],
|
||||
hw_version=device.hw_info["hw_ver"],
|
||||
model=registry_device.model,
|
||||
name=device_name,
|
||||
sw_version=registry_device.hw_info["sw_ver"],
|
||||
hw_version=registry_device.hw_info["hw_ver"],
|
||||
)
|
||||
|
||||
if (
|
||||
parent is not None
|
||||
and parent != registry_device
|
||||
and parent.device_type is not Device.Type.WallSwitch
|
||||
):
|
||||
self._attr_device_info["via_device"] = (DOMAIN, parent.device_id)
|
||||
else:
|
||||
self._attr_device_info["connections"] = {
|
||||
(dr.CONNECTION_NETWORK_MAC, device.mac)
|
||||
}
|
||||
|
||||
self._attr_unique_id = self._get_unique_id()
|
||||
|
||||
def _get_unique_id(self) -> str:
|
||||
"""Return unique ID for the entity."""
|
||||
return legacy_device_id(self._device)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle being added to hass."""
|
||||
self._async_call_update_attrs()
|
||||
return await super().async_added_to_hass()
|
||||
|
||||
@abstractmethod
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Platforms implement this to update the entity internals."""
|
||||
raise NotImplementedError
|
||||
|
||||
@callback
|
||||
def _async_call_update_attrs(self) -> None:
|
||||
"""Call update_attrs and make entity unavailable on error.
|
||||
|
||||
update_attrs can sometimes fail if a device firmware update breaks the
|
||||
downstream library.
|
||||
"""
|
||||
try:
|
||||
self._async_update_attrs()
|
||||
except Exception as ex: # noqa: BLE001
|
||||
if self._attr_available:
|
||||
_LOGGER.warning(
|
||||
"Unable to read data for %s %s: %s",
|
||||
self._device,
|
||||
self.entity_id,
|
||||
ex,
|
||||
)
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._async_call_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self.coordinator.last_update_success and self._attr_available
|
||||
|
||||
|
||||
class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC):
|
||||
"""Common base class for all coordinated tplink feature entities."""
|
||||
|
||||
entity_description: TPLinkFeatureEntityDescription
|
||||
_feature: Feature
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
feature: Feature,
|
||||
description: TPLinkFeatureEntityDescription,
|
||||
parent: Device | None = None,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.entity_description = description
|
||||
super().__init__(device, coordinator, parent=parent, feature=feature)
|
||||
|
||||
def _get_unique_id(self) -> str:
|
||||
"""Return unique ID for the entity."""
|
||||
key = self.entity_description.key
|
||||
# The unique id for the state feature in the switch platform is the
|
||||
# device_id
|
||||
if key == PRIMARY_STATE_ID:
|
||||
return legacy_device_id(self._device)
|
||||
|
||||
# Historically the legacy device emeter attributes which are now
|
||||
# replaced with features used slightly different keys. This ensures
|
||||
# that those entities are not orphaned. Returns the mapped key or the
|
||||
# provided key if not mapped.
|
||||
key = LEGACY_KEY_MAPPING.get(key, key)
|
||||
return f"{legacy_device_id(self._device)}_{key}"
|
||||
|
||||
@classmethod
|
||||
def _category_for_feature(cls, feature: Feature | None) -> EntityCategory | None:
|
||||
"""Return entity category for a feature."""
|
||||
# Main controls have no category
|
||||
if feature is None or feature.category is Feature.Category.Primary:
|
||||
return None
|
||||
|
||||
if (
|
||||
entity_category := FEATURE_CATEGORY_TO_ENTITY_CATEGORY.get(feature.category)
|
||||
) is None:
|
||||
_LOGGER.error(
|
||||
"Unhandled category %s, fallback to DIAGNOSTIC", feature.category
|
||||
)
|
||||
entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
return entity_category
|
||||
|
||||
@classmethod
|
||||
def _description_for_feature[_D: EntityDescription](
|
||||
cls,
|
||||
feature: Feature,
|
||||
descriptions: Mapping[str, _D],
|
||||
*,
|
||||
device: Device,
|
||||
parent: Device | None = None,
|
||||
) -> _D | None:
|
||||
"""Return description object for the given feature.
|
||||
|
||||
This is responsible for setting the common parameters & deciding
|
||||
based on feature id which additional parameters are passed.
|
||||
"""
|
||||
|
||||
if descriptions and (desc := descriptions.get(feature.id)):
|
||||
translation_key: str | None = feature.id
|
||||
# HA logic is to name entities based on the following logic:
|
||||
# _attr_name > translation.name > description.name
|
||||
# > device_class (if base platform supports).
|
||||
name: str | None | UndefinedType = UNDEFINED
|
||||
|
||||
# The state feature gets the device name or the child device
|
||||
# name if it's a child device
|
||||
if feature.id == PRIMARY_STATE_ID:
|
||||
translation_key = None
|
||||
# if None will use device name
|
||||
name = get_device_name(device, parent=parent) if parent else None
|
||||
|
||||
return replace(
|
||||
desc,
|
||||
translation_key=translation_key,
|
||||
name=name, # if undefined will use translation key
|
||||
entity_category=cls._category_for_feature(feature),
|
||||
# enabled_default can be overridden to False in the description
|
||||
entity_registry_enabled_default=feature.category
|
||||
is not Feature.Category.Debug
|
||||
and desc.entity_registry_enabled_default,
|
||||
)
|
||||
|
||||
_LOGGER.info(
|
||||
"Device feature: %s (%s) needs an entity description defined in HA",
|
||||
feature.name,
|
||||
feature.id,
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _entities_for_device[
|
||||
_E: CoordinatedTPLinkFeatureEntity,
|
||||
_D: TPLinkFeatureEntityDescription,
|
||||
](
|
||||
cls,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
feature_type: Feature.Type,
|
||||
entity_class: type[_E],
|
||||
descriptions: Mapping[str, _D],
|
||||
parent: Device | None = None,
|
||||
) -> list[_E]:
|
||||
"""Return a list of entities to add.
|
||||
|
||||
This filters out unwanted features to avoid creating unnecessary entities
|
||||
for device features that are implemented by specialized platforms like light.
|
||||
"""
|
||||
entities: list[_E] = [
|
||||
entity_class(
|
||||
device,
|
||||
coordinator,
|
||||
feature=feat,
|
||||
description=desc,
|
||||
parent=parent,
|
||||
)
|
||||
for feat in device.features.values()
|
||||
if feat.type == feature_type
|
||||
and feat.id not in EXCLUDED_FEATURES
|
||||
and (
|
||||
feat.category is not Feature.Category.Primary
|
||||
or device.device_type not in DEVICETYPES_WITH_SPECIALIZED_PLATFORMS
|
||||
)
|
||||
and (
|
||||
desc := cls._description_for_feature(
|
||||
feat, descriptions, device=device, parent=parent
|
||||
)
|
||||
)
|
||||
]
|
||||
return entities
|
||||
|
||||
@classmethod
|
||||
def entities_for_device_and_its_children[
|
||||
_E: CoordinatedTPLinkFeatureEntity,
|
||||
_D: TPLinkFeatureEntityDescription,
|
||||
](
|
||||
cls,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
feature_type: Feature.Type,
|
||||
entity_class: type[_E],
|
||||
descriptions: Mapping[str, _D],
|
||||
child_coordinators: list[TPLinkDataUpdateCoordinator] | None = None,
|
||||
) -> list[_E]:
|
||||
"""Create entities for device and its children.
|
||||
|
||||
This is a helper that calls *_entities_for_device* for the device and its children.
|
||||
"""
|
||||
entities: list[_E] = []
|
||||
# Add parent entities before children so via_device id works.
|
||||
entities.extend(
|
||||
cls._entities_for_device(
|
||||
device,
|
||||
coordinator=coordinator,
|
||||
feature_type=feature_type,
|
||||
entity_class=entity_class,
|
||||
descriptions=descriptions,
|
||||
)
|
||||
)
|
||||
if device.children:
|
||||
_LOGGER.debug("Initializing device with %s children", len(device.children))
|
||||
for idx, child in enumerate(device.children):
|
||||
# HS300 does not like too many concurrent requests and its
|
||||
# emeter data requires a request for each socket, so we receive
|
||||
# separate coordinators.
|
||||
if child_coordinators:
|
||||
child_coordinator = child_coordinators[idx]
|
||||
else:
|
||||
child_coordinator = coordinator
|
||||
entities.extend(
|
||||
cls._entities_for_device(
|
||||
child,
|
||||
coordinator=child_coordinator,
|
||||
feature_type=feature_type,
|
||||
entity_class=entity_class,
|
||||
descriptions=descriptions,
|
||||
parent=device,
|
||||
)
|
||||
)
|
||||
|
||||
return entities
|
||||
|
|
111
homeassistant/components/tplink/fan.py
Normal file
111
homeassistant/components/tplink/fan.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
"""Support for TPLink Fan devices."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from kasa import Device, Module
|
||||
from kasa.interfaces import Fan as FanInterface
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
percentage_to_ranged_value,
|
||||
ranged_value_to_percentage,
|
||||
)
|
||||
from homeassistant.util.scaling import int_states_in_range
|
||||
|
||||
from . import TPLinkConfigEntry
|
||||
from .coordinator import TPLinkDataUpdateCoordinator
|
||||
from .entity import CoordinatedTPLinkEntity, async_refresh_after
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up fans."""
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
device = parent_coordinator.device
|
||||
entities: list[CoordinatedTPLinkEntity] = []
|
||||
if Module.Fan in device.modules:
|
||||
entities.append(
|
||||
TPLinkFanEntity(
|
||||
device, parent_coordinator, fan_module=device.modules[Module.Fan]
|
||||
)
|
||||
)
|
||||
entities.extend(
|
||||
TPLinkFanEntity(
|
||||
child,
|
||||
parent_coordinator,
|
||||
fan_module=child.modules[Module.Fan],
|
||||
parent=device,
|
||||
)
|
||||
for child in device.children
|
||||
if Module.Fan in child.modules
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
SPEED_RANGE = (1, 4) # off is not included
|
||||
|
||||
|
||||
class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity):
|
||||
"""Representation of a fan for a TPLink Fan device."""
|
||||
|
||||
_attr_speed_count = int_states_in_range(SPEED_RANGE)
|
||||
_attr_supported_features = FanEntityFeature.SET_SPEED
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
fan_module: FanInterface,
|
||||
parent: Device | None = None,
|
||||
) -> None:
|
||||
"""Initialize the fan."""
|
||||
super().__init__(device, coordinator, parent=parent)
|
||||
self.fan_module = fan_module
|
||||
# If _attr_name is None the entity name will be the device name
|
||||
self._attr_name = None if parent is None else device.alias
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn on the fan."""
|
||||
if percentage is not None:
|
||||
value_in_range = math.ceil(
|
||||
percentage_to_ranged_value(SPEED_RANGE, percentage)
|
||||
)
|
||||
else:
|
||||
value_in_range = SPEED_RANGE[1]
|
||||
await self.fan_module.set_fan_speed_level(value_in_range)
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fan off."""
|
||||
await self.fan_module.set_fan_speed_level(0)
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed percentage of the fan."""
|
||||
value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
|
||||
await self.fan_module.set_fan_speed_level(value_in_range)
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
fan_speed = self.fan_module.fan_speed_level
|
||||
self._attr_is_on = fan_speed != 0
|
||||
if self._attr_is_on:
|
||||
self._attr_percentage = ranged_value_to_percentage(SPEED_RANGE, fan_speed)
|
||||
else:
|
||||
self._attr_percentage = None
|
|
@ -1,11 +1,110 @@
|
|||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"humidity_warning": {
|
||||
"default": "mdi:water-percent",
|
||||
"state": {
|
||||
"on": "mdi:water-percent-alert"
|
||||
}
|
||||
},
|
||||
"temperature_warning": {
|
||||
"default": "mdi:thermometer-check",
|
||||
"state": {
|
||||
"on": "mdi:thermometer-alert"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"test_alarm": {
|
||||
"default": "mdi:bell-alert"
|
||||
},
|
||||
"stop_alarm": {
|
||||
"default": "mdi:bell-cancel"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"light_preset": {
|
||||
"default": "mdi:sign-direction"
|
||||
},
|
||||
"alarm_sound": {
|
||||
"default": "mdi:music-note"
|
||||
},
|
||||
"alarm_volume": {
|
||||
"default": "mdi:volume-medium",
|
||||
"state": {
|
||||
"low": "mdi:volume-low",
|
||||
"medium": "mdi:volume-medium",
|
||||
"high": "mdi:volume-high"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"led": {
|
||||
"default": "mdi:led-off",
|
||||
"state": {
|
||||
"on": "mdi:led-on"
|
||||
}
|
||||
},
|
||||
"auto_update_enabled": {
|
||||
"default": "mdi:autorenew-off",
|
||||
"state": {
|
||||
"on": "mdi:autorenew"
|
||||
}
|
||||
},
|
||||
"auto_off_enabled": {
|
||||
"default": "mdi:sleep-off",
|
||||
"state": {
|
||||
"on": "mdi:sleep"
|
||||
}
|
||||
},
|
||||
"smooth_transitions": {
|
||||
"default": "mdi:transition-masked",
|
||||
"state": {
|
||||
"on": "mdi:transition"
|
||||
}
|
||||
},
|
||||
"fan_sleep_mode": {
|
||||
"default": "mdi:sleep-off",
|
||||
"state": {
|
||||
"on": "mdi:sleep"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"on_since": {
|
||||
"default": "mdi:clock"
|
||||
},
|
||||
"ssid": {
|
||||
"default": "mdi:wifi"
|
||||
},
|
||||
"signal_level": {
|
||||
"default": "mdi:signal"
|
||||
},
|
||||
"current_firmware_version": {
|
||||
"default": "mdi:information"
|
||||
},
|
||||
"available_firmware_version": {
|
||||
"default": "mdi:information-outline"
|
||||
},
|
||||
"alarm_source": {
|
||||
"default": "mdi:bell"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"smooth_transition_off": {
|
||||
"default": "mdi:weather-sunset-down"
|
||||
},
|
||||
"smooth_transition_on": {
|
||||
"default": "mdi:weather-sunset-up"
|
||||
},
|
||||
"auto_off_minutes": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"temperature_offset": {
|
||||
"default": "mdi:contrast"
|
||||
},
|
||||
"target_temperature": {
|
||||
"default": "mdi:thermometer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,9 +4,11 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import Sequence
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from kasa import SmartBulb, SmartLightStrip
|
||||
from kasa import Device, DeviceType, LightState, Module
|
||||
from kasa.interfaces import Light, LightEffect
|
||||
from kasa.iot import IotDevice
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
|
@ -15,23 +17,21 @@ from homeassistant.components.light import (
|
|||
ATTR_EFFECT,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
EFFECT_OFF,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
filter_supported_color_modes,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_platform
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from . import legacy_device_id
|
||||
from .const import DOMAIN
|
||||
from . import TPLinkConfigEntry, legacy_device_id
|
||||
from .coordinator import TPLinkDataUpdateCoordinator
|
||||
from .entity import CoordinatedTPLinkEntity, async_refresh_after
|
||||
from .models import TPLinkData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -132,16 +132,24 @@ def _async_build_base_effect(
|
|||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switches."""
|
||||
data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
device = parent_coordinator.device
|
||||
if device.is_light_strip:
|
||||
async_add_entities(
|
||||
[TPLinkSmartLightStrip(cast(SmartLightStrip, device), parent_coordinator)]
|
||||
entities: list[TPLinkLightEntity | TPLinkLightEffectEntity] = []
|
||||
if (
|
||||
effect_module := device.modules.get(Module.LightEffect)
|
||||
) and effect_module.has_custom_effects:
|
||||
entities.append(
|
||||
TPLinkLightEffectEntity(
|
||||
device,
|
||||
parent_coordinator,
|
||||
light_module=device.modules[Module.Light],
|
||||
effect_module=effect_module,
|
||||
)
|
||||
)
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
|
@ -154,52 +162,83 @@ async def async_setup_entry(
|
|||
SEQUENCE_EFFECT_DICT,
|
||||
"async_set_sequence_effect",
|
||||
)
|
||||
elif device.is_bulb or device.is_dimmer:
|
||||
async_add_entities(
|
||||
[TPLinkSmartBulb(cast(SmartBulb, device), parent_coordinator)]
|
||||
elif Module.Light in device.modules:
|
||||
entities.append(
|
||||
TPLinkLightEntity(
|
||||
device, parent_coordinator, light_module=device.modules[Module.Light]
|
||||
)
|
||||
)
|
||||
entities.extend(
|
||||
TPLinkLightEntity(
|
||||
child,
|
||||
parent_coordinator,
|
||||
light_module=child.modules[Module.Light],
|
||||
parent=device,
|
||||
)
|
||||
for child in device.children
|
||||
if Module.Light in child.modules
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity):
|
||||
"""Representation of a TPLink Smart Bulb."""
|
||||
|
||||
_attr_supported_features = LightEntityFeature.TRANSITION
|
||||
_attr_name = None
|
||||
_fixed_color_mode: ColorMode | None = None
|
||||
|
||||
device: SmartBulb
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: SmartBulb,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
light_module: Light,
|
||||
parent: Device | None = None,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device, coordinator)
|
||||
# For backwards compat with pyHS100
|
||||
if device.is_dimmer:
|
||||
# Dimmers used to use the switch format since
|
||||
# pyHS100 treated them as SmartPlug but the old code
|
||||
# created them as lights
|
||||
# https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86
|
||||
self._attr_unique_id = legacy_device_id(device)
|
||||
else:
|
||||
self._attr_unique_id = device.mac.replace(":", "").upper()
|
||||
"""Initialize the light."""
|
||||
self._parent = parent
|
||||
super().__init__(device, coordinator, parent=parent)
|
||||
self._light_module = light_module
|
||||
# If _attr_name is None the entity name will be the device name
|
||||
self._attr_name = None if parent is None else device.alias
|
||||
modes: set[ColorMode] = {ColorMode.ONOFF}
|
||||
if device.is_variable_color_temp:
|
||||
if light_module.is_variable_color_temp:
|
||||
modes.add(ColorMode.COLOR_TEMP)
|
||||
temp_range = device.valid_temperature_range
|
||||
temp_range = light_module.valid_temperature_range
|
||||
self._attr_min_color_temp_kelvin = temp_range.min
|
||||
self._attr_max_color_temp_kelvin = temp_range.max
|
||||
if device.is_color:
|
||||
if light_module.is_color:
|
||||
modes.add(ColorMode.HS)
|
||||
if device.is_dimmable:
|
||||
if light_module.is_dimmable:
|
||||
modes.add(ColorMode.BRIGHTNESS)
|
||||
self._attr_supported_color_modes = filter_supported_color_modes(modes)
|
||||
if len(self._attr_supported_color_modes) == 1:
|
||||
# If the light supports only a single color mode, set it now
|
||||
self._fixed_color_mode = next(iter(self._attr_supported_color_modes))
|
||||
self._async_update_attrs()
|
||||
self._async_call_update_attrs()
|
||||
|
||||
def _get_unique_id(self) -> str:
|
||||
"""Return unique ID for the entity."""
|
||||
# For historical reasons the light platform uses the mac address as
|
||||
# the unique id whereas all other platforms use device_id.
|
||||
device = self._device
|
||||
|
||||
# For backwards compat with pyHS100
|
||||
if device.device_type is DeviceType.Dimmer and isinstance(device, IotDevice):
|
||||
# Dimmers used to use the switch format since
|
||||
# pyHS100 treated them as SmartPlug but the old code
|
||||
# created them as lights
|
||||
# https://github.com/home-assistant/core/blob/2021.9.7/ \
|
||||
# homeassistant/components/tplink/common.py#L86
|
||||
return legacy_device_id(device)
|
||||
|
||||
# Newer devices can have child lights. While there isn't currently
|
||||
# an example of a device with more than one light we use the device_id
|
||||
# for consistency and future proofing
|
||||
if self._parent or device.children:
|
||||
return legacy_device_id(device)
|
||||
|
||||
return device.mac.replace(":", "").upper()
|
||||
|
||||
@callback
|
||||
def _async_extract_brightness_transition(
|
||||
|
@ -211,12 +250,12 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||
if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None:
|
||||
brightness = round((brightness * 100.0) / 255.0)
|
||||
|
||||
if self.device.is_dimmer and transition is None:
|
||||
# This is a stopgap solution for inconsistent set_brightness handling
|
||||
# in the upstream library, see #57265.
|
||||
if self._device.device_type is DeviceType.Dimmer and transition is None:
|
||||
# This is a stopgap solution for inconsistent set_brightness
|
||||
# handling in the upstream library, see #57265.
|
||||
# This should be removed when the upstream has fixed the issue.
|
||||
# The device logic is to change the settings without turning it on
|
||||
# except when transition is defined, so we leverage that here for now.
|
||||
# except when transition is defined so we leverage that for now.
|
||||
transition = 1
|
||||
|
||||
return brightness, transition
|
||||
|
@ -226,13 +265,13 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||
) -> None:
|
||||
# TP-Link requires integers.
|
||||
hue, sat = tuple(int(val) for val in hs_color)
|
||||
await self.device.set_hsv(hue, sat, brightness, transition=transition)
|
||||
await self._light_module.set_hsv(hue, sat, brightness, transition=transition)
|
||||
|
||||
async def _async_set_color_temp(
|
||||
self, color_temp: float, brightness: int | None, transition: int | None
|
||||
) -> None:
|
||||
device = self.device
|
||||
valid_temperature_range = device.valid_temperature_range
|
||||
light_module = self._light_module
|
||||
valid_temperature_range = light_module.valid_temperature_range
|
||||
requested_color_temp = round(color_temp)
|
||||
# Clamp color temp to valid range
|
||||
# since if the light in a group we will
|
||||
|
@ -242,7 +281,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||
valid_temperature_range.max,
|
||||
max(valid_temperature_range.min, requested_color_temp),
|
||||
)
|
||||
await device.set_color_temp(
|
||||
await light_module.set_color_temp(
|
||||
clamped_color_temp,
|
||||
brightness=brightness,
|
||||
transition=transition,
|
||||
|
@ -253,9 +292,11 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||
) -> None:
|
||||
# Fallback to adjusting brightness or turning the bulb on
|
||||
if brightness is not None:
|
||||
await self.device.set_brightness(brightness, transition=transition)
|
||||
await self._light_module.set_brightness(brightness, transition=transition)
|
||||
return
|
||||
await self.device.turn_on(transition=transition)
|
||||
await self._light_module.set_state(
|
||||
LightState(light_on=True, transition=transition)
|
||||
)
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
|
@ -275,7 +316,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||
"""Turn the light off."""
|
||||
if (transition := kwargs.get(ATTR_TRANSITION)) is not None:
|
||||
transition = int(transition * 1_000)
|
||||
await self.device.turn_off(transition=transition)
|
||||
await self._light_module.set_state(
|
||||
LightState(light_on=False, transition=transition)
|
||||
)
|
||||
|
||||
def _determine_color_mode(self) -> ColorMode:
|
||||
"""Return the active color mode."""
|
||||
|
@ -284,48 +327,53 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||
return self._fixed_color_mode
|
||||
|
||||
# The light supports both color temp and color, determine which on is active
|
||||
if self.device.is_variable_color_temp and self.device.color_temp:
|
||||
if self._light_module.is_variable_color_temp and self._light_module.color_temp:
|
||||
return ColorMode.COLOR_TEMP
|
||||
return ColorMode.HS
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
device = self.device
|
||||
self._attr_is_on = device.is_on
|
||||
if device.is_dimmable:
|
||||
self._attr_brightness = round((device.brightness * 255.0) / 100.0)
|
||||
light_module = self._light_module
|
||||
self._attr_is_on = light_module.state.light_on is True
|
||||
if light_module.is_dimmable:
|
||||
self._attr_brightness = round((light_module.brightness * 255.0) / 100.0)
|
||||
color_mode = self._determine_color_mode()
|
||||
self._attr_color_mode = color_mode
|
||||
if color_mode is ColorMode.COLOR_TEMP:
|
||||
self._attr_color_temp_kelvin = device.color_temp
|
||||
self._attr_color_temp_kelvin = light_module.color_temp
|
||||
elif color_mode is ColorMode.HS:
|
||||
hue, saturation, _ = device.hsv
|
||||
hue, saturation, _ = light_module.hsv
|
||||
self._attr_hs_color = hue, saturation
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
|
||||
class TPLinkSmartLightStrip(TPLinkSmartBulb):
|
||||
class TPLinkLightEffectEntity(TPLinkLightEntity):
|
||||
"""Representation of a TPLink Smart Light Strip."""
|
||||
|
||||
device: SmartLightStrip
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
light_module: Light,
|
||||
effect_module: LightEffect,
|
||||
) -> None:
|
||||
"""Initialize the light strip."""
|
||||
self._effect_module = effect_module
|
||||
super().__init__(device, coordinator, light_module=light_module)
|
||||
|
||||
_attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
super()._async_update_attrs()
|
||||
device = self.device
|
||||
if (effect := device.effect) and effect["enable"]:
|
||||
self._attr_effect = effect["name"]
|
||||
effect_module = self._effect_module
|
||||
if effect_module.effect != LightEffect.LIGHT_EFFECTS_OFF:
|
||||
self._attr_effect = effect_module.effect
|
||||
else:
|
||||
self._attr_effect = None
|
||||
if effect_list := device.effect_list:
|
||||
self._attr_effect = EFFECT_OFF
|
||||
if effect_list := effect_module.effect_list:
|
||||
self._attr_effect_list = effect_list
|
||||
else:
|
||||
self._attr_effect_list = None
|
||||
|
@ -335,15 +383,15 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb):
|
|||
"""Turn the light on."""
|
||||
brightness, transition = self._async_extract_brightness_transition(**kwargs)
|
||||
if ATTR_EFFECT in kwargs:
|
||||
await self.device.set_effect(
|
||||
await self._effect_module.set_effect(
|
||||
kwargs[ATTR_EFFECT], brightness=brightness, transition=transition
|
||||
)
|
||||
elif ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
if self.effect:
|
||||
# If there is an effect in progress
|
||||
# we have to set an HSV value to clear the effect
|
||||
# we have to clear the effect
|
||||
# before we can set a color temp
|
||||
await self.device.set_hsv(0, 0, brightness)
|
||||
await self._light_module.set_hsv(0, 0, brightness)
|
||||
await self._async_set_color_temp(
|
||||
kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition
|
||||
)
|
||||
|
@ -390,7 +438,7 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb):
|
|||
if transition_range:
|
||||
effect["transition_range"] = transition_range
|
||||
effect["transition"] = 0
|
||||
await self.device.set_custom_effect(effect)
|
||||
await self._effect_module.set_custom_effect(effect)
|
||||
|
||||
async def async_set_sequence_effect(
|
||||
self,
|
||||
|
@ -412,4 +460,4 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb):
|
|||
"spread": spread,
|
||||
"direction": direction,
|
||||
}
|
||||
await self.device.set_custom_effect(effect)
|
||||
await self._effect_module.set_custom_effect(effect)
|
||||
|
|
|
@ -40,6 +40,10 @@
|
|||
"hostname": "k[lps]*",
|
||||
"macaddress": "5091E3*"
|
||||
},
|
||||
{
|
||||
"hostname": "p1*",
|
||||
"macaddress": "5091E3*"
|
||||
},
|
||||
{
|
||||
"hostname": "k[lps]*",
|
||||
"macaddress": "9C5322*"
|
||||
|
@ -216,14 +220,26 @@
|
|||
"hostname": "s5*",
|
||||
"macaddress": "3C52A1*"
|
||||
},
|
||||
{
|
||||
"hostname": "h1*",
|
||||
"macaddress": "3C52A1*"
|
||||
},
|
||||
{
|
||||
"hostname": "l9*",
|
||||
"macaddress": "A842A1*"
|
||||
},
|
||||
{
|
||||
"hostname": "p1*",
|
||||
"macaddress": "A842A1*"
|
||||
},
|
||||
{
|
||||
"hostname": "l9*",
|
||||
"macaddress": "3460F9*"
|
||||
},
|
||||
{
|
||||
"hostname": "p1*",
|
||||
"macaddress": "3460F9*"
|
||||
},
|
||||
{
|
||||
"hostname": "hs*",
|
||||
"macaddress": "704F57*"
|
||||
|
@ -232,6 +248,10 @@
|
|||
"hostname": "k[lps]*",
|
||||
"macaddress": "74DA88*"
|
||||
},
|
||||
{
|
||||
"hostname": "p1*",
|
||||
"macaddress": "74DA88*"
|
||||
},
|
||||
{
|
||||
"hostname": "p3*",
|
||||
"macaddress": "788CB5*"
|
||||
|
@ -263,11 +283,19 @@
|
|||
{
|
||||
"hostname": "l9*",
|
||||
"macaddress": "F0A731*"
|
||||
},
|
||||
{
|
||||
"hostname": "ks2*",
|
||||
"macaddress": "F0A731*"
|
||||
},
|
||||
{
|
||||
"hostname": "kh1*",
|
||||
"macaddress": "F0A731*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/tplink",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["kasa"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-kasa[speedups]==0.6.2.1"]
|
||||
"requirements": ["python-kasa[speedups]==0.7.0.1"]
|
||||
}
|
||||
|
|
108
homeassistant/components/tplink/number.py
Normal file
108
homeassistant/components/tplink/number.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
"""Support for TPLink number entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from kasa import Device, Feature
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import TPLinkConfigEntry
|
||||
from .entity import (
|
||||
CoordinatedTPLinkFeatureEntity,
|
||||
TPLinkDataUpdateCoordinator,
|
||||
TPLinkFeatureEntityDescription,
|
||||
async_refresh_after,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TPLinkNumberEntityDescription(
|
||||
NumberEntityDescription, TPLinkFeatureEntityDescription
|
||||
):
|
||||
"""Base class for a TPLink feature based sensor entity description."""
|
||||
|
||||
|
||||
NUMBER_DESCRIPTIONS: Final = (
|
||||
TPLinkNumberEntityDescription(
|
||||
key="smooth_transition_on",
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
TPLinkNumberEntityDescription(
|
||||
key="smooth_transition_off",
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
TPLinkNumberEntityDescription(
|
||||
key="auto_off_minutes",
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
TPLinkNumberEntityDescription(
|
||||
key="temperature_offset",
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
)
|
||||
|
||||
NUMBER_DESCRIPTIONS_MAP = {desc.key: desc for desc in NUMBER_DESCRIPTIONS}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors."""
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
children_coordinators = data.children_coordinators
|
||||
device = parent_coordinator.device
|
||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||
device=device,
|
||||
coordinator=parent_coordinator,
|
||||
feature_type=Feature.Type.Number,
|
||||
entity_class=TPLinkNumberEntity,
|
||||
descriptions=NUMBER_DESCRIPTIONS_MAP,
|
||||
child_coordinators=children_coordinators,
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class TPLinkNumberEntity(CoordinatedTPLinkFeatureEntity, NumberEntity):
|
||||
"""Representation of a feature-based TPLink sensor."""
|
||||
|
||||
entity_description: TPLinkNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
feature: Feature,
|
||||
description: TPLinkFeatureEntityDescription,
|
||||
parent: Device | None = None,
|
||||
) -> None:
|
||||
"""Initialize the a switch."""
|
||||
super().__init__(
|
||||
device, coordinator, feature=feature, description=description, parent=parent
|
||||
)
|
||||
self._attr_native_min_value = self._feature.minimum_value
|
||||
self._attr_native_max_value = self._feature.maximum_value
|
||||
|
||||
@async_refresh_after
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set feature value."""
|
||||
await self._feature.set_value(int(value))
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
self._attr_native_value = self._feature.value
|
95
homeassistant/components/tplink/select.py
Normal file
95
homeassistant/components/tplink/select.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
"""Support for TPLink select entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, cast
|
||||
|
||||
from kasa import Device, Feature
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import TPLinkConfigEntry
|
||||
from .entity import (
|
||||
CoordinatedTPLinkFeatureEntity,
|
||||
TPLinkDataUpdateCoordinator,
|
||||
TPLinkFeatureEntityDescription,
|
||||
async_refresh_after,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TPLinkSelectEntityDescription(
|
||||
SelectEntityDescription, TPLinkFeatureEntityDescription
|
||||
):
|
||||
"""Base class for a TPLink feature based sensor entity description."""
|
||||
|
||||
|
||||
SELECT_DESCRIPTIONS: Final = [
|
||||
TPLinkSelectEntityDescription(
|
||||
key="light_preset",
|
||||
),
|
||||
TPLinkSelectEntityDescription(
|
||||
key="alarm_sound",
|
||||
),
|
||||
TPLinkSelectEntityDescription(
|
||||
key="alarm_volume",
|
||||
),
|
||||
]
|
||||
|
||||
SELECT_DESCRIPTIONS_MAP = {desc.key: desc for desc in SELECT_DESCRIPTIONS}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors."""
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
children_coordinators = data.children_coordinators
|
||||
device = parent_coordinator.device
|
||||
|
||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||
device=device,
|
||||
coordinator=parent_coordinator,
|
||||
feature_type=Feature.Type.Choice,
|
||||
entity_class=TPLinkSelectEntity,
|
||||
descriptions=SELECT_DESCRIPTIONS_MAP,
|
||||
child_coordinators=children_coordinators,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class TPLinkSelectEntity(CoordinatedTPLinkFeatureEntity, SelectEntity):
|
||||
"""Representation of a tplink select entity."""
|
||||
|
||||
entity_description: TPLinkSelectEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
feature: Feature,
|
||||
description: TPLinkFeatureEntityDescription,
|
||||
parent: Device | None = None,
|
||||
) -> None:
|
||||
"""Initialize a select."""
|
||||
super().__init__(
|
||||
device, coordinator, feature=feature, description=description, parent=parent
|
||||
)
|
||||
self._attr_options = cast(list, self._feature.choices)
|
||||
|
||||
@async_refresh_after
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Update the current selected option."""
|
||||
await self._feature.set_value(option)
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
self._attr_current_option = self._feature.value
|
|
@ -1,11 +1,11 @@
|
|||
"""Support for TPLink HS100/HS110/HS200 smart switch energy sensors."""
|
||||
"""Support for TPLink sensor entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from kasa import SmartDevice
|
||||
from kasa import Device, Feature
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
|
@ -13,175 +13,164 @@ from homeassistant.components.sensor import (
|
|||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_VOLTAGE,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import legacy_device_id
|
||||
from .const import (
|
||||
ATTR_CURRENT_A,
|
||||
ATTR_CURRENT_POWER_W,
|
||||
ATTR_TODAY_ENERGY_KWH,
|
||||
ATTR_TOTAL_ENERGY_KWH,
|
||||
DOMAIN,
|
||||
)
|
||||
from . import TPLinkConfigEntry
|
||||
from .const import UNIT_MAPPING
|
||||
from .coordinator import TPLinkDataUpdateCoordinator
|
||||
from .entity import CoordinatedTPLinkEntity
|
||||
from .models import TPLinkData
|
||||
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TPLinkSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes TPLink sensor entity."""
|
||||
|
||||
emeter_attr: str | None = None
|
||||
precision: int | None = None
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TPLinkSensorEntityDescription(
|
||||
SensorEntityDescription, TPLinkFeatureEntityDescription
|
||||
):
|
||||
"""Base class for a TPLink feature based sensor entity description."""
|
||||
|
||||
|
||||
ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = (
|
||||
SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = (
|
||||
TPLinkSensorEntityDescription(
|
||||
key=ATTR_CURRENT_POWER_W,
|
||||
translation_key="current_consumption",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
key="current_consumption",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
emeter_attr="power",
|
||||
precision=1,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key=ATTR_TOTAL_ENERGY_KWH,
|
||||
translation_key="total_consumption",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
key="consumption_total",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
emeter_attr="total",
|
||||
precision=3,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key=ATTR_TODAY_ENERGY_KWH,
|
||||
translation_key="today_consumption",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
key="consumption_today",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=3,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key=ATTR_VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
key="consumption_this_month",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="voltage",
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
emeter_attr="voltage",
|
||||
precision=1,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key=ATTR_CURRENT_A,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
key="current",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
emeter_attr="current",
|
||||
precision=2,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
# Disable as the value reported by the device changes seconds frequently
|
||||
entity_registry_enabled_default=False,
|
||||
key="on_since",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="rssi",
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="signal_level",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="ssid",
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="battery_level",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="auto_off_at",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="device_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="report_interval",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="alarm_source",
|
||||
),
|
||||
TPLinkSensorEntityDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def async_emeter_from_device(
|
||||
device: SmartDevice, description: TPLinkSensorEntityDescription
|
||||
) -> float | None:
|
||||
"""Map a sensor key to the device attribute."""
|
||||
if attr := description.emeter_attr:
|
||||
if (val := getattr(device.emeter_realtime, attr)) is None:
|
||||
return None
|
||||
return round(cast(float, val), description.precision)
|
||||
|
||||
# ATTR_TODAY_ENERGY_KWH
|
||||
if (emeter_today := device.emeter_today) is not None:
|
||||
return round(cast(float, emeter_today), description.precision)
|
||||
# today's consumption not available, when device was off all the day
|
||||
# bulb's do not report this information, so filter it out
|
||||
return None if device.is_bulb else 0.0
|
||||
|
||||
|
||||
def _async_sensors_for_device(
|
||||
device: SmartDevice,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
has_parent: bool = False,
|
||||
) -> list[SmartPlugSensor]:
|
||||
"""Generate the sensors for the device."""
|
||||
return [
|
||||
SmartPlugSensor(device, coordinator, description, has_parent)
|
||||
for description in ENERGY_SENSORS
|
||||
if async_emeter_from_device(device, description) is not None
|
||||
]
|
||||
SENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in SENSOR_DESCRIPTIONS}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors."""
|
||||
data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
children_coordinators = data.children_coordinators
|
||||
entities: list[SmartPlugSensor] = []
|
||||
parent = parent_coordinator.device
|
||||
if not parent.has_emeter:
|
||||
return
|
||||
|
||||
if parent.is_strip:
|
||||
# Historically we only add the children if the device is a strip
|
||||
for idx, child in enumerate(parent.children):
|
||||
entities.extend(
|
||||
_async_sensors_for_device(child, children_coordinators[idx], True)
|
||||
)
|
||||
else:
|
||||
entities.extend(_async_sensors_for_device(parent, parent_coordinator))
|
||||
device = parent_coordinator.device
|
||||
|
||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||
device=device,
|
||||
coordinator=parent_coordinator,
|
||||
feature_type=Feature.Type.Sensor,
|
||||
entity_class=TPLinkSensorEntity,
|
||||
descriptions=SENSOR_DESCRIPTIONS_MAP,
|
||||
child_coordinators=children_coordinators,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity):
|
||||
"""Representation of a TPLink Smart Plug energy sensor."""
|
||||
class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity):
|
||||
"""Representation of a feature-based TPLink sensor."""
|
||||
|
||||
entity_description: TPLinkSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: SmartDevice,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
feature: Feature,
|
||||
description: TPLinkSensorEntityDescription,
|
||||
has_parent: bool = False,
|
||||
parent: Device | None = None,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device, coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{legacy_device_id(device)}_{description.key}"
|
||||
if has_parent:
|
||||
assert device.alias
|
||||
self._attr_translation_placeholders = {"device_name": device.alias}
|
||||
if description.translation_key:
|
||||
self._attr_translation_key = f"{description.translation_key}_child"
|
||||
else:
|
||||
assert description.device_class
|
||||
self._attr_translation_key = f"{description.device_class.value}_child"
|
||||
self._async_update_attrs()
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
device, coordinator, description=description, feature=feature, parent=parent
|
||||
)
|
||||
self._async_call_update_attrs()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
self._attr_native_value = async_emeter_from_device(
|
||||
self.device, self.entity_description
|
||||
)
|
||||
value = self._feature.value
|
||||
if value is not None and self._feature.precision_hint is not None:
|
||||
value = round(cast(float, value), self._feature.precision_hint)
|
||||
# We probably do not need this, when we are rounding already?
|
||||
self._attr_suggested_display_precision = self._feature.precision_hint
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
self._attr_native_value = value
|
||||
# Map to homeassistant units and fallback to upstream one if none found
|
||||
if self._feature.unit is not None:
|
||||
self._attr_native_unit_of_measurement = UNIT_MAPPING.get(
|
||||
self._feature.unit, self._feature.unit
|
||||
)
|
||||
|
|
|
@ -59,35 +59,151 @@
|
|||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"humidity_warning": {
|
||||
"name": "Humidity warning"
|
||||
},
|
||||
"temperature_warning": {
|
||||
"name": "Temperature warning"
|
||||
},
|
||||
"overheated": {
|
||||
"name": "Overheated"
|
||||
},
|
||||
"battery_low": {
|
||||
"name": "Battery low"
|
||||
},
|
||||
"cloud_connection": {
|
||||
"name": "Cloud connection"
|
||||
},
|
||||
"update_available": {
|
||||
"name": "[%key:component::binary_sensor::entity_component::update::name%]",
|
||||
"state": {
|
||||
"off": "[%key:component::binary_sensor::entity_component::update::state::off%]",
|
||||
"on": "[%key:component::binary_sensor::entity_component::update::state::on%]"
|
||||
}
|
||||
},
|
||||
"is_open": {
|
||||
"name": "[%key:component::binary_sensor::entity_component::door::name%]",
|
||||
"state": {
|
||||
"off": "[%key:common::state::closed%]",
|
||||
"on": "[%key:common::state::open%]"
|
||||
}
|
||||
},
|
||||
"water_alert": {
|
||||
"name": "[%key:component::binary_sensor::entity_component::moisture::name%]",
|
||||
"state": {
|
||||
"off": "[%key:component::binary_sensor::entity_component::moisture::state::off%]",
|
||||
"on": "[%key:component::binary_sensor::entity_component::moisture::state::on%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"test_alarm": {
|
||||
"name": "Test alarm"
|
||||
},
|
||||
"stop_alarm": {
|
||||
"name": "Stop alarm"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"light_preset": {
|
||||
"name": "Light preset"
|
||||
},
|
||||
"alarm_sound": {
|
||||
"name": "Alarm sound"
|
||||
},
|
||||
"alarm_volume": {
|
||||
"name": "Alarm volume"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"current_consumption": {
|
||||
"name": "Current consumption"
|
||||
},
|
||||
"total_consumption": {
|
||||
"consumption_total": {
|
||||
"name": "Total consumption"
|
||||
},
|
||||
"today_consumption": {
|
||||
"consumption_today": {
|
||||
"name": "Today's consumption"
|
||||
},
|
||||
"current_consumption_child": {
|
||||
"name": "{device_name} current consumption"
|
||||
"consumption_this_month": {
|
||||
"name": "This month's consumption"
|
||||
},
|
||||
"total_consumption_child": {
|
||||
"name": "{device_name} total consumption"
|
||||
"on_since": {
|
||||
"name": "On since"
|
||||
},
|
||||
"today_consumption_child": {
|
||||
"name": "{device_name} today's consumption"
|
||||
"ssid": {
|
||||
"name": "SSID"
|
||||
},
|
||||
"current_child": {
|
||||
"name": "{device_name} current"
|
||||
"signal_level": {
|
||||
"name": "Signal level"
|
||||
},
|
||||
"voltage_child": {
|
||||
"name": "{device_name} voltage"
|
||||
"current_firmware_version": {
|
||||
"name": "Current firmware version"
|
||||
},
|
||||
"available_firmware_version": {
|
||||
"name": "Available firmware version"
|
||||
},
|
||||
"battery_level": {
|
||||
"name": "Battery level"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "[%key:component::sensor::entity_component::temperature::name%]"
|
||||
},
|
||||
"voltage": {
|
||||
"name": "[%key:component::sensor::entity_component::voltage::name%]"
|
||||
},
|
||||
"current": {
|
||||
"name": "[%key:component::sensor::entity_component::current::name%]"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "[%key:component::sensor::entity_component::humidity::name%]"
|
||||
},
|
||||
"device_time": {
|
||||
"name": "Device time"
|
||||
},
|
||||
"auto_off_at": {
|
||||
"name": "Auto off at"
|
||||
},
|
||||
"report_interval": {
|
||||
"name": "Report interval"
|
||||
},
|
||||
"alarm_source": {
|
||||
"name": "Alarm source"
|
||||
},
|
||||
"rssi": {
|
||||
"name": "[%key:component::sensor::entity_component::signal_strength::name%]"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"led": {
|
||||
"name": "LED"
|
||||
},
|
||||
"auto_update_enabled": {
|
||||
"name": "Auto update enabled"
|
||||
},
|
||||
"auto_off_enabled": {
|
||||
"name": "Auto off enabled"
|
||||
},
|
||||
"smooth_transitions": {
|
||||
"name": "Smooth transitions"
|
||||
},
|
||||
"fan_sleep_mode": {
|
||||
"name": "Fan sleep mode"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"smooth_transition_on": {
|
||||
"name": "Smooth on"
|
||||
},
|
||||
"smooth_transition_off": {
|
||||
"name": "Smooth off"
|
||||
},
|
||||
"auto_off_minutes": {
|
||||
"name": "Turn off in"
|
||||
},
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,158 +1,112 @@
|
|||
"""Support for TPLink HS100/HS110/HS200 smart switch."""
|
||||
"""Support for TPLink switch entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from kasa import SmartDevice, SmartPlug
|
||||
from kasa import Device, Feature
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import legacy_device_id
|
||||
from .const import DOMAIN
|
||||
from . import TPLinkConfigEntry
|
||||
from .coordinator import TPLinkDataUpdateCoordinator
|
||||
from .entity import CoordinatedTPLinkEntity, async_refresh_after
|
||||
from .models import TPLinkData
|
||||
from .entity import (
|
||||
CoordinatedTPLinkFeatureEntity,
|
||||
TPLinkFeatureEntityDescription,
|
||||
async_refresh_after,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TPLinkSwitchEntityDescription(
|
||||
SwitchEntityDescription, TPLinkFeatureEntityDescription
|
||||
):
|
||||
"""Base class for a TPLink feature based sensor entity description."""
|
||||
|
||||
|
||||
SWITCH_DESCRIPTIONS: tuple[TPLinkSwitchEntityDescription, ...] = (
|
||||
TPLinkSwitchEntityDescription(
|
||||
key="state",
|
||||
),
|
||||
TPLinkSwitchEntityDescription(
|
||||
key="led",
|
||||
),
|
||||
TPLinkSwitchEntityDescription(
|
||||
key="auto_update_enabled",
|
||||
),
|
||||
TPLinkSwitchEntityDescription(
|
||||
key="auto_off_enabled",
|
||||
),
|
||||
TPLinkSwitchEntityDescription(
|
||||
key="smooth_transitions",
|
||||
),
|
||||
TPLinkSwitchEntityDescription(
|
||||
key="fan_sleep_mode",
|
||||
),
|
||||
)
|
||||
|
||||
SWITCH_DESCRIPTIONS_MAP = {desc.key: desc for desc in SWITCH_DESCRIPTIONS}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: TPLinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switches."""
|
||||
data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
data = config_entry.runtime_data
|
||||
parent_coordinator = data.parent_coordinator
|
||||
device = cast(SmartPlug, parent_coordinator.device)
|
||||
if not device.is_plug and not device.is_strip and not device.is_dimmer:
|
||||
return
|
||||
entities: list = []
|
||||
if device.is_strip:
|
||||
# Historically we only add the children if the device is a strip
|
||||
_LOGGER.debug("Initializing strip with %s sockets", len(device.children))
|
||||
entities.extend(
|
||||
SmartPlugSwitchChild(device, parent_coordinator, child)
|
||||
for child in device.children
|
||||
)
|
||||
elif device.is_plug:
|
||||
entities.append(SmartPlugSwitch(device, parent_coordinator))
|
||||
device = parent_coordinator.device
|
||||
|
||||
# this will be removed on the led is implemented
|
||||
if hasattr(device, "led"):
|
||||
entities.append(SmartPlugLedSwitch(device, parent_coordinator))
|
||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||
device,
|
||||
coordinator=parent_coordinator,
|
||||
feature_type=Feature.Switch,
|
||||
entity_class=TPLinkSwitch,
|
||||
descriptions=SWITCH_DESCRIPTIONS_MAP,
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity):
|
||||
"""Representation of switch for the LED of a TPLink Smart Plug."""
|
||||
class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity):
|
||||
"""Representation of a feature-based TPLink switch."""
|
||||
|
||||
device: SmartPlug
|
||||
|
||||
_attr_translation_key = "led"
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
def __init__(
|
||||
self, device: SmartPlug, coordinator: TPLinkDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize the LED switch."""
|
||||
super().__init__(device, coordinator)
|
||||
self._attr_unique_id = f"{device.mac}_led"
|
||||
self._async_update_attrs()
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the LED switch on."""
|
||||
await self.device.set_led(True)
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the LED switch off."""
|
||||
await self.device.set_led(False)
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
self._attr_is_on = self.device.led
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
|
||||
class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity):
|
||||
"""Representation of a TPLink Smart Plug switch."""
|
||||
|
||||
_attr_name: str | None = None
|
||||
entity_description: TPLinkSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: SmartDevice,
|
||||
device: Device,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
*,
|
||||
feature: Feature,
|
||||
description: TPLinkSwitchEntityDescription,
|
||||
parent: Device | None = None,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device, coordinator)
|
||||
# For backwards compat with pyHS100
|
||||
self._attr_unique_id = legacy_device_id(device)
|
||||
self._async_update_attrs()
|
||||
super().__init__(
|
||||
device, coordinator, description=description, feature=feature, parent=parent
|
||||
)
|
||||
|
||||
self._async_call_update_attrs()
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self.device.turn_on()
|
||||
await self._feature.set_value(True)
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self.device.turn_off()
|
||||
await self._feature.set_value(False)
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
self._attr_is_on = self.device.is_on
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
|
||||
class SmartPlugSwitchChild(SmartPlugSwitch):
|
||||
"""Representation of an individual plug of a TPLink Smart Plug strip."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: SmartDevice,
|
||||
coordinator: TPLinkDataUpdateCoordinator,
|
||||
plug: SmartDevice,
|
||||
) -> None:
|
||||
"""Initialize the child switch."""
|
||||
self._plug = plug
|
||||
super().__init__(device, coordinator)
|
||||
self._attr_unique_id = legacy_device_id(plug)
|
||||
self._attr_name = plug.alias
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the child switch on."""
|
||||
await self._plug.turn_on()
|
||||
|
||||
@async_refresh_after
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the child switch off."""
|
||||
await self._plug.turn_off()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity's attributes."""
|
||||
self._attr_is_on = self._plug.is_on
|
||||
self._attr_is_on = self._feature.value
|
||||
|
|
|
@ -650,6 +650,11 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
|||
"hostname": "k[lps]*",
|
||||
"macaddress": "5091E3*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "p1*",
|
||||
"macaddress": "5091E3*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "k[lps]*",
|
||||
|
@ -870,16 +875,31 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
|||
"hostname": "s5*",
|
||||
"macaddress": "3C52A1*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "h1*",
|
||||
"macaddress": "3C52A1*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "l9*",
|
||||
"macaddress": "A842A1*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "p1*",
|
||||
"macaddress": "A842A1*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "l9*",
|
||||
"macaddress": "3460F9*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "p1*",
|
||||
"macaddress": "3460F9*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "hs*",
|
||||
|
@ -890,6 +910,11 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
|||
"hostname": "k[lps]*",
|
||||
"macaddress": "74DA88*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "p1*",
|
||||
"macaddress": "74DA88*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "p3*",
|
||||
|
@ -930,6 +955,16 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
|||
"hostname": "l9*",
|
||||
"macaddress": "F0A731*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "ks2*",
|
||||
"macaddress": "F0A731*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "kh1*",
|
||||
"macaddress": "F0A731*",
|
||||
},
|
||||
{
|
||||
"domain": "tuya",
|
||||
"macaddress": "105A17*",
|
||||
|
|
|
@ -2278,7 +2278,7 @@ python-join-api==0.0.9
|
|||
python-juicenet==1.1.0
|
||||
|
||||
# homeassistant.components.tplink
|
||||
python-kasa[speedups]==0.6.2.1
|
||||
python-kasa[speedups]==0.7.0.1
|
||||
|
||||
# homeassistant.components.lirc
|
||||
# python-lirc==1.2.3
|
||||
|
|
|
@ -1778,7 +1778,7 @@ python-izone==1.2.9
|
|||
python-juicenet==1.1.0
|
||||
|
||||
# homeassistant.components.tplink
|
||||
python-kasa[speedups]==0.6.2.1
|
||||
python-kasa[speedups]==0.7.0.1
|
||||
|
||||
# homeassistant.components.matter
|
||||
python-matter-server==6.1.0
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
"""Tests for the TP-Link component."""
|
||||
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from kasa import (
|
||||
ConnectionType,
|
||||
Device,
|
||||
DeviceConfig,
|
||||
DeviceFamilyType,
|
||||
EncryptType,
|
||||
SmartBulb,
|
||||
SmartDevice,
|
||||
SmartDimmer,
|
||||
SmartLightStrip,
|
||||
SmartPlug,
|
||||
SmartStrip,
|
||||
DeviceConnectionParameters,
|
||||
DeviceEncryptionType,
|
||||
DeviceFamily,
|
||||
DeviceType,
|
||||
Feature,
|
||||
KasaException,
|
||||
Module,
|
||||
)
|
||||
from kasa.exceptions import SmartDeviceException
|
||||
from kasa.interfaces import Fan, Light, LightEffect, LightState
|
||||
from kasa.protocol import BaseProtocol
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.tplink import (
|
||||
CONF_ALIAS,
|
||||
|
@ -25,9 +28,17 @@ from homeassistant.components.tplink import (
|
|||
Credentials,
|
||||
)
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.helpers.typing import UNDEFINED
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, load_json_value_fixture
|
||||
|
||||
ColorTempRange = namedtuple("ColorTempRange", ["min", "max"])
|
||||
|
||||
MODULE = "homeassistant.components.tplink"
|
||||
MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow"
|
||||
|
@ -36,6 +47,7 @@ IP_ADDRESS2 = "127.0.0.2"
|
|||
ALIAS = "My Bulb"
|
||||
MODEL = "HS100"
|
||||
MAC_ADDRESS = "aa:bb:cc:dd:ee:ff"
|
||||
DEVICE_ID = "123456789ABCDEFGH"
|
||||
DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "")
|
||||
MAC_ADDRESS2 = "11:22:33:44:55:66"
|
||||
DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}"
|
||||
|
@ -49,16 +61,16 @@ CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv=="
|
|||
DEVICE_CONFIG_AUTH = DeviceConfig(
|
||||
IP_ADDRESS,
|
||||
credentials=CREDENTIALS,
|
||||
connection_type=ConnectionType(
|
||||
DeviceFamilyType.IotSmartPlugSwitch, EncryptType.Klap
|
||||
connection_type=DeviceConnectionParameters(
|
||||
DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Klap
|
||||
),
|
||||
uses_http=True,
|
||||
)
|
||||
DEVICE_CONFIG_AUTH2 = DeviceConfig(
|
||||
IP_ADDRESS2,
|
||||
credentials=CREDENTIALS,
|
||||
connection_type=ConnectionType(
|
||||
DeviceFamilyType.IotSmartPlugSwitch, EncryptType.Klap
|
||||
connection_type=DeviceConnectionParameters(
|
||||
DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Klap
|
||||
),
|
||||
uses_http=True,
|
||||
)
|
||||
|
@ -90,190 +102,316 @@ CREATE_ENTRY_DATA_AUTH2 = {
|
|||
}
|
||||
|
||||
|
||||
def _load_feature_fixtures():
|
||||
fixtures = load_json_value_fixture("features.json", DOMAIN)
|
||||
for fixture in fixtures.values():
|
||||
if isinstance(fixture["value"], str):
|
||||
try:
|
||||
time = datetime.strptime(fixture["value"], "%Y-%m-%d %H:%M:%S.%f%z")
|
||||
fixture["value"] = time
|
||||
except ValueError:
|
||||
pass
|
||||
return fixtures
|
||||
|
||||
|
||||
FEATURES_FIXTURE = _load_feature_fixtures()
|
||||
|
||||
|
||||
async def setup_platform_for_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, platform: Platform, device: Device
|
||||
):
|
||||
"""Set up a single tplink platform with a device."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.tplink.PLATFORMS", [platform]),
|
||||
_patch_discovery(device=device),
|
||||
_patch_connect(device=device),
|
||||
):
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
# Good practice to wait background tasks in tests see PR #112726
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
|
||||
async def snapshot_platform(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
config_entry_id: str,
|
||||
) -> None:
|
||||
"""Snapshot a platform."""
|
||||
device_entries = dr.async_entries_for_config_entry(device_registry, config_entry_id)
|
||||
assert device_entries
|
||||
for device_entry in device_entries:
|
||||
assert device_entry == snapshot(
|
||||
name=f"{device_entry.name}-entry"
|
||||
), f"device entry snapshot failed for {device_entry.name}"
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id)
|
||||
assert entity_entries
|
||||
assert (
|
||||
len({entity_entry.domain for entity_entry in entity_entries}) == 1
|
||||
), "Please limit the loaded platforms to 1 platform."
|
||||
|
||||
translations = await async_get_translations(hass, "en", "entity", [DOMAIN])
|
||||
for entity_entry in entity_entries:
|
||||
if entity_entry.translation_key:
|
||||
key = f"component.{DOMAIN}.entity.{entity_entry.domain}.{entity_entry.translation_key}.name"
|
||||
assert (
|
||||
key in translations
|
||||
), f"No translation for entity {entity_entry.unique_id}, expected {key}"
|
||||
assert entity_entry == snapshot(
|
||||
name=f"{entity_entry.entity_id}-entry"
|
||||
), f"entity entry snapshot failed for {entity_entry.entity_id}"
|
||||
if entity_entry.disabled_by is None:
|
||||
state = hass.states.get(entity_entry.entity_id)
|
||||
assert state, f"State not found for {entity_entry.entity_id}"
|
||||
assert state == snapshot(
|
||||
name=f"{entity_entry.entity_id}-state"
|
||||
), f"state snapshot failed for {entity_entry.entity_id}"
|
||||
|
||||
|
||||
def _mock_protocol() -> BaseProtocol:
|
||||
protocol = MagicMock(auto_spec=BaseProtocol)
|
||||
protocol = MagicMock(spec=BaseProtocol)
|
||||
protocol.close = AsyncMock()
|
||||
return protocol
|
||||
|
||||
|
||||
def _mocked_bulb(
|
||||
def _mocked_device(
|
||||
device_config=DEVICE_CONFIG_LEGACY,
|
||||
credentials_hash=CREDENTIALS_HASH_LEGACY,
|
||||
mac=MAC_ADDRESS,
|
||||
device_id=DEVICE_ID,
|
||||
alias=ALIAS,
|
||||
) -> SmartBulb:
|
||||
bulb = MagicMock(auto_spec=SmartBulb, name="Mocked bulb")
|
||||
bulb.update = AsyncMock()
|
||||
bulb.mac = mac
|
||||
bulb.alias = alias
|
||||
bulb.model = MODEL
|
||||
bulb.host = IP_ADDRESS
|
||||
bulb.brightness = 50
|
||||
bulb.color_temp = 4000
|
||||
bulb.is_color = True
|
||||
bulb.is_strip = False
|
||||
bulb.is_plug = False
|
||||
bulb.is_dimmer = False
|
||||
bulb.is_light_strip = False
|
||||
bulb.has_effects = False
|
||||
bulb.effect = None
|
||||
bulb.effect_list = None
|
||||
bulb.hsv = (10, 30, 5)
|
||||
bulb.device_id = mac
|
||||
bulb.valid_temperature_range.min = 4000
|
||||
bulb.valid_temperature_range.max = 9000
|
||||
bulb.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
||||
bulb.turn_off = AsyncMock()
|
||||
bulb.turn_on = AsyncMock()
|
||||
bulb.set_brightness = AsyncMock()
|
||||
bulb.set_hsv = AsyncMock()
|
||||
bulb.set_color_temp = AsyncMock()
|
||||
bulb.protocol = _mock_protocol()
|
||||
bulb.config = device_config
|
||||
bulb.credentials_hash = credentials_hash
|
||||
return bulb
|
||||
model=MODEL,
|
||||
ip_address=IP_ADDRESS,
|
||||
modules: list[str] | None = None,
|
||||
children: list[Device] | None = None,
|
||||
features: list[str | Feature] | None = None,
|
||||
device_type=None,
|
||||
spec: type = Device,
|
||||
) -> Device:
|
||||
device = MagicMock(spec=spec, name="Mocked device")
|
||||
device.update = AsyncMock()
|
||||
device.turn_off = AsyncMock()
|
||||
device.turn_on = AsyncMock()
|
||||
|
||||
device.mac = mac
|
||||
device.alias = alias
|
||||
device.model = model
|
||||
device.host = ip_address
|
||||
device.device_id = device_id
|
||||
device.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
||||
device.modules = {}
|
||||
device.features = {}
|
||||
|
||||
if modules:
|
||||
device.modules = {
|
||||
module_name: MODULE_TO_MOCK_GEN[module_name]() for module_name in modules
|
||||
}
|
||||
|
||||
if features:
|
||||
device.features = {
|
||||
feature_id: _mocked_feature(feature_id, require_fixture=True)
|
||||
for feature_id in features
|
||||
if isinstance(feature_id, str)
|
||||
}
|
||||
|
||||
device.features.update(
|
||||
{
|
||||
feature.id: feature
|
||||
for feature in features
|
||||
if isinstance(feature, Feature)
|
||||
}
|
||||
)
|
||||
device.children = []
|
||||
if children:
|
||||
for child in children:
|
||||
child.mac = mac
|
||||
device.children = children
|
||||
device.device_type = device_type if device_type else DeviceType.Unknown
|
||||
if (
|
||||
not device_type
|
||||
and device.children
|
||||
and all(
|
||||
child.device_type is DeviceType.StripSocket for child in device.children
|
||||
)
|
||||
):
|
||||
device.device_type = DeviceType.Strip
|
||||
|
||||
device.protocol = _mock_protocol()
|
||||
device.config = device_config
|
||||
device.credentials_hash = credentials_hash
|
||||
return device
|
||||
|
||||
|
||||
class MockedSmartLightStrip(SmartLightStrip):
|
||||
"""Mock a SmartLightStrip."""
|
||||
def _mocked_feature(
|
||||
id: str,
|
||||
*,
|
||||
require_fixture=False,
|
||||
value: Any = UNDEFINED,
|
||||
name=None,
|
||||
type_=None,
|
||||
category=None,
|
||||
precision_hint=None,
|
||||
choices=None,
|
||||
unit=None,
|
||||
minimum_value=0,
|
||||
maximum_value=2**16, # Arbitrary max
|
||||
) -> Feature:
|
||||
"""Get a mocked feature.
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""Mock a SmartLightStrip that will pass an isinstance check."""
|
||||
return MagicMock(spec=cls)
|
||||
If kwargs are provided they will override the attributes for any features defined in fixtures.json
|
||||
"""
|
||||
feature = MagicMock(spec=Feature, name=f"Mocked {id} feature")
|
||||
feature.id = id
|
||||
feature.name = name or id.upper()
|
||||
feature.set_value = AsyncMock()
|
||||
if not (fixture := FEATURES_FIXTURE.get(id)):
|
||||
assert (
|
||||
require_fixture is False
|
||||
), f"No fixture defined for feature {id} and require_fixture is True"
|
||||
assert (
|
||||
value is not UNDEFINED
|
||||
), f"Value must be provided if feature {id} not defined in features.json"
|
||||
fixture = {"value": value, "category": "Primary", "type": "Sensor"}
|
||||
elif value is not UNDEFINED:
|
||||
fixture["value"] = value
|
||||
feature.value = fixture["value"]
|
||||
|
||||
feature.type = type_ or Feature.Type[fixture["type"]]
|
||||
feature.category = category or Feature.Category[fixture["category"]]
|
||||
|
||||
# sensor
|
||||
feature.precision_hint = precision_hint or fixture.get("precision_hint")
|
||||
feature.unit = unit or fixture.get("unit")
|
||||
|
||||
# number
|
||||
feature.minimum_value = minimum_value or fixture.get("minimum_value")
|
||||
feature.maximum_value = maximum_value or fixture.get("maximum_value")
|
||||
|
||||
# select
|
||||
feature.choices = choices or fixture.get("choices")
|
||||
return feature
|
||||
|
||||
|
||||
def _mocked_smart_light_strip() -> SmartLightStrip:
|
||||
strip = MockedSmartLightStrip()
|
||||
strip.update = AsyncMock()
|
||||
strip.mac = MAC_ADDRESS
|
||||
strip.alias = ALIAS
|
||||
strip.model = MODEL
|
||||
strip.host = IP_ADDRESS
|
||||
strip.brightness = 50
|
||||
strip.color_temp = 4000
|
||||
strip.is_color = True
|
||||
strip.is_strip = False
|
||||
strip.is_plug = False
|
||||
strip.is_dimmer = False
|
||||
strip.is_light_strip = True
|
||||
strip.has_effects = True
|
||||
strip.effect = {"name": "Effect1", "enable": 1}
|
||||
strip.effect_list = ["Effect1", "Effect2"]
|
||||
strip.hsv = (10, 30, 5)
|
||||
strip.device_id = MAC_ADDRESS
|
||||
strip.valid_temperature_range.min = 4000
|
||||
strip.valid_temperature_range.max = 9000
|
||||
strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
||||
strip.turn_off = AsyncMock()
|
||||
strip.turn_on = AsyncMock()
|
||||
strip.set_brightness = AsyncMock()
|
||||
strip.set_hsv = AsyncMock()
|
||||
strip.set_color_temp = AsyncMock()
|
||||
strip.set_effect = AsyncMock()
|
||||
strip.set_custom_effect = AsyncMock()
|
||||
strip.protocol = _mock_protocol()
|
||||
strip.config = DEVICE_CONFIG_LEGACY
|
||||
strip.credentials_hash = CREDENTIALS_HASH_LEGACY
|
||||
return strip
|
||||
def _mocked_light_module() -> Light:
|
||||
light = MagicMock(spec=Light, name="Mocked light module")
|
||||
light.update = AsyncMock()
|
||||
light.brightness = 50
|
||||
light.color_temp = 4000
|
||||
light.state = LightState(
|
||||
light_on=True, brightness=light.brightness, color_temp=light.color_temp
|
||||
)
|
||||
light.is_color = True
|
||||
light.is_variable_color_temp = True
|
||||
light.is_dimmable = True
|
||||
light.is_brightness = True
|
||||
light.has_effects = False
|
||||
light.hsv = (10, 30, 5)
|
||||
light.valid_temperature_range = ColorTempRange(min=4000, max=9000)
|
||||
light.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
||||
light.set_state = AsyncMock()
|
||||
light.set_brightness = AsyncMock()
|
||||
light.set_hsv = AsyncMock()
|
||||
light.set_color_temp = AsyncMock()
|
||||
light.protocol = _mock_protocol()
|
||||
return light
|
||||
|
||||
|
||||
def _mocked_dimmer() -> SmartDimmer:
|
||||
dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer")
|
||||
dimmer.update = AsyncMock()
|
||||
dimmer.mac = MAC_ADDRESS
|
||||
dimmer.alias = "My Dimmer"
|
||||
dimmer.model = MODEL
|
||||
dimmer.host = IP_ADDRESS
|
||||
dimmer.brightness = 50
|
||||
dimmer.color_temp = 4000
|
||||
dimmer.is_color = True
|
||||
dimmer.is_strip = False
|
||||
dimmer.is_plug = False
|
||||
dimmer.is_dimmer = True
|
||||
dimmer.is_light_strip = False
|
||||
dimmer.effect = None
|
||||
dimmer.effect_list = None
|
||||
dimmer.hsv = (10, 30, 5)
|
||||
dimmer.device_id = MAC_ADDRESS
|
||||
dimmer.valid_temperature_range.min = 4000
|
||||
dimmer.valid_temperature_range.max = 9000
|
||||
dimmer.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
||||
dimmer.turn_off = AsyncMock()
|
||||
dimmer.turn_on = AsyncMock()
|
||||
dimmer.set_brightness = AsyncMock()
|
||||
dimmer.set_hsv = AsyncMock()
|
||||
dimmer.set_color_temp = AsyncMock()
|
||||
dimmer.set_led = AsyncMock()
|
||||
dimmer.protocol = _mock_protocol()
|
||||
dimmer.config = DEVICE_CONFIG_LEGACY
|
||||
dimmer.credentials_hash = CREDENTIALS_HASH_LEGACY
|
||||
return dimmer
|
||||
def _mocked_light_effect_module() -> LightEffect:
|
||||
effect = MagicMock(spec=LightEffect, name="Mocked light effect")
|
||||
effect.has_effects = True
|
||||
effect.has_custom_effects = True
|
||||
effect.effect = "Effect1"
|
||||
effect.effect_list = ["Off", "Effect1", "Effect2"]
|
||||
effect.set_effect = AsyncMock()
|
||||
effect.set_custom_effect = AsyncMock()
|
||||
return effect
|
||||
|
||||
|
||||
def _mocked_plug() -> SmartPlug:
|
||||
plug = MagicMock(auto_spec=SmartPlug, name="Mocked plug")
|
||||
plug.update = AsyncMock()
|
||||
plug.mac = MAC_ADDRESS
|
||||
plug.alias = "My Plug"
|
||||
plug.model = MODEL
|
||||
plug.host = IP_ADDRESS
|
||||
plug.is_light_strip = False
|
||||
plug.is_bulb = False
|
||||
plug.is_dimmer = False
|
||||
plug.is_strip = False
|
||||
plug.is_plug = True
|
||||
plug.device_id = MAC_ADDRESS
|
||||
plug.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
||||
plug.turn_off = AsyncMock()
|
||||
plug.turn_on = AsyncMock()
|
||||
plug.set_led = AsyncMock()
|
||||
plug.protocol = _mock_protocol()
|
||||
plug.config = DEVICE_CONFIG_LEGACY
|
||||
plug.credentials_hash = CREDENTIALS_HASH_LEGACY
|
||||
return plug
|
||||
def _mocked_fan_module() -> Fan:
|
||||
fan = MagicMock(auto_spec=Fan, name="Mocked fan")
|
||||
fan.fan_speed_level = 0
|
||||
fan.set_fan_speed_level = AsyncMock()
|
||||
return fan
|
||||
|
||||
|
||||
def _mocked_strip() -> SmartStrip:
|
||||
strip = MagicMock(auto_spec=SmartStrip, name="Mocked strip")
|
||||
strip.update = AsyncMock()
|
||||
strip.mac = MAC_ADDRESS
|
||||
strip.alias = "My Strip"
|
||||
strip.model = MODEL
|
||||
strip.host = IP_ADDRESS
|
||||
strip.is_light_strip = False
|
||||
strip.is_bulb = False
|
||||
strip.is_dimmer = False
|
||||
strip.is_strip = True
|
||||
strip.is_plug = True
|
||||
strip.device_id = MAC_ADDRESS
|
||||
strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
||||
strip.turn_off = AsyncMock()
|
||||
strip.turn_on = AsyncMock()
|
||||
strip.set_led = AsyncMock()
|
||||
strip.protocol = _mock_protocol()
|
||||
strip.config = DEVICE_CONFIG_LEGACY
|
||||
strip.credentials_hash = CREDENTIALS_HASH_LEGACY
|
||||
plug0 = _mocked_plug()
|
||||
plug0.alias = "Plug0"
|
||||
plug0.device_id = "bb:bb:cc:dd:ee:ff_PLUG0DEVICEID"
|
||||
plug0.mac = "bb:bb:cc:dd:ee:ff"
|
||||
def _mocked_strip_children(features=None, alias=None) -> list[Device]:
|
||||
plug0 = _mocked_device(
|
||||
alias="Plug0" if alias is None else alias,
|
||||
device_id="bb:bb:cc:dd:ee:ff_PLUG0DEVICEID",
|
||||
mac="bb:bb:cc:dd:ee:ff",
|
||||
device_type=DeviceType.StripSocket,
|
||||
features=features,
|
||||
)
|
||||
plug1 = _mocked_device(
|
||||
alias="Plug1" if alias is None else alias,
|
||||
device_id="cc:bb:cc:dd:ee:ff_PLUG1DEVICEID",
|
||||
mac="cc:bb:cc:dd:ee:ff",
|
||||
device_type=DeviceType.StripSocket,
|
||||
features=features,
|
||||
)
|
||||
plug0.is_on = True
|
||||
plug0.protocol = _mock_protocol()
|
||||
plug1 = _mocked_plug()
|
||||
plug1.device_id = "cc:bb:cc:dd:ee:ff_PLUG1DEVICEID"
|
||||
plug1.mac = "cc:bb:cc:dd:ee:ff"
|
||||
plug1.alias = "Plug1"
|
||||
plug1.protocol = _mock_protocol()
|
||||
plug1.is_on = False
|
||||
strip.children = [plug0, plug1]
|
||||
return strip
|
||||
return [plug0, plug1]
|
||||
|
||||
|
||||
def _mocked_energy_features(
|
||||
power=None, total=None, voltage=None, current=None, today=None
|
||||
) -> list[Feature]:
|
||||
feats = []
|
||||
if power is not None:
|
||||
feats.append(
|
||||
_mocked_feature(
|
||||
"current_consumption",
|
||||
value=power,
|
||||
)
|
||||
)
|
||||
if total is not None:
|
||||
feats.append(
|
||||
_mocked_feature(
|
||||
"consumption_total",
|
||||
value=total,
|
||||
)
|
||||
)
|
||||
if voltage is not None:
|
||||
feats.append(
|
||||
_mocked_feature(
|
||||
"voltage",
|
||||
value=voltage,
|
||||
)
|
||||
)
|
||||
if current is not None:
|
||||
feats.append(
|
||||
_mocked_feature(
|
||||
"current",
|
||||
value=current,
|
||||
)
|
||||
)
|
||||
# Today is always reported as 0 by the library rather than none
|
||||
feats.append(
|
||||
_mocked_feature(
|
||||
"consumption_today",
|
||||
value=today if today is not None else 0.0,
|
||||
)
|
||||
)
|
||||
return feats
|
||||
|
||||
|
||||
MODULE_TO_MOCK_GEN = {
|
||||
Module.Light: _mocked_light_module,
|
||||
Module.LightEffect: _mocked_light_effect_module,
|
||||
Module.Fan: _mocked_fan_module,
|
||||
}
|
||||
|
||||
|
||||
def _patch_discovery(device=None, no_device=False):
|
||||
async def _discovery(*args, **kwargs):
|
||||
if no_device:
|
||||
return {}
|
||||
return {IP_ADDRESS: _mocked_bulb()}
|
||||
return {IP_ADDRESS: _mocked_device()}
|
||||
|
||||
return patch("homeassistant.components.tplink.Discover.discover", new=_discovery)
|
||||
|
||||
|
@ -281,8 +419,8 @@ def _patch_discovery(device=None, no_device=False):
|
|||
def _patch_single_discovery(device=None, no_device=False):
|
||||
async def _discover_single(*args, **kwargs):
|
||||
if no_device:
|
||||
raise SmartDeviceException
|
||||
return device if device else _mocked_bulb()
|
||||
raise KasaException
|
||||
return device if device else _mocked_device()
|
||||
|
||||
return patch(
|
||||
"homeassistant.components.tplink.Discover.discover_single", new=_discover_single
|
||||
|
@ -292,14 +430,14 @@ def _patch_single_discovery(device=None, no_device=False):
|
|||
def _patch_connect(device=None, no_device=False):
|
||||
async def _connect(*args, **kwargs):
|
||||
if no_device:
|
||||
raise SmartDeviceException
|
||||
return device if device else _mocked_bulb()
|
||||
raise KasaException
|
||||
return device if device else _mocked_device()
|
||||
|
||||
return patch("homeassistant.components.tplink.SmartDevice.connect", new=_connect)
|
||||
return patch("homeassistant.components.tplink.Device.connect", new=_connect)
|
||||
|
||||
|
||||
async def initialize_config_entry_for_device(
|
||||
hass: HomeAssistant, dev: SmartDevice
|
||||
hass: HomeAssistant, dev: Device
|
||||
) -> MockConfigEntry:
|
||||
"""Create a mocked configuration entry for the given device.
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ from . import (
|
|||
IP_ADDRESS2,
|
||||
MAC_ADDRESS,
|
||||
MAC_ADDRESS2,
|
||||
_mocked_bulb,
|
||||
_mocked_device,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
|
||||
|
@ -31,13 +31,13 @@ def mock_discovery():
|
|||
discover=DEFAULT,
|
||||
discover_single=DEFAULT,
|
||||
) as mock_discovery:
|
||||
device = _mocked_bulb(
|
||||
device = _mocked_device(
|
||||
device_config=copy.deepcopy(DEVICE_CONFIG_AUTH),
|
||||
credentials_hash=CREDENTIALS_HASH_AUTH,
|
||||
alias=None,
|
||||
)
|
||||
devices = {
|
||||
"127.0.0.1": _mocked_bulb(
|
||||
"127.0.0.1": _mocked_device(
|
||||
device_config=copy.deepcopy(DEVICE_CONFIG_AUTH),
|
||||
credentials_hash=CREDENTIALS_HASH_AUTH,
|
||||
alias=None,
|
||||
|
@ -52,12 +52,12 @@ def mock_discovery():
|
|||
@pytest.fixture
|
||||
def mock_connect():
|
||||
"""Mock python-kasa connect."""
|
||||
with patch("homeassistant.components.tplink.SmartDevice.connect") as mock_connect:
|
||||
with patch("homeassistant.components.tplink.Device.connect") as mock_connect:
|
||||
devices = {
|
||||
IP_ADDRESS: _mocked_bulb(
|
||||
IP_ADDRESS: _mocked_device(
|
||||
device_config=DEVICE_CONFIG_AUTH, credentials_hash=CREDENTIALS_HASH_AUTH
|
||||
),
|
||||
IP_ADDRESS2: _mocked_bulb(
|
||||
IP_ADDRESS2: _mocked_device(
|
||||
device_config=DEVICE_CONFIG_AUTH,
|
||||
credentials_hash=CREDENTIALS_HASH_AUTH,
|
||||
mac=MAC_ADDRESS2,
|
||||
|
|
287
tests/components/tplink/fixtures/features.json
Normal file
287
tests/components/tplink/fixtures/features.json
Normal file
|
@ -0,0 +1,287 @@
|
|||
{
|
||||
"state": {
|
||||
"value": true,
|
||||
"type": "Switch",
|
||||
"category": "Primary"
|
||||
},
|
||||
"led": {
|
||||
"value": true,
|
||||
"type": "Switch",
|
||||
"category": "Config"
|
||||
},
|
||||
"auto_update_enabled": {
|
||||
"value": true,
|
||||
"type": "Switch",
|
||||
"category": "Config"
|
||||
},
|
||||
"auto_off_enabled": {
|
||||
"value": true,
|
||||
"type": "Switch",
|
||||
"category": "Config"
|
||||
},
|
||||
"smooth_transitions": {
|
||||
"value": true,
|
||||
"type": "Switch",
|
||||
"category": "Config"
|
||||
},
|
||||
"frost_protection_enabled": {
|
||||
"value": true,
|
||||
"type": "Switch",
|
||||
"category": "Config"
|
||||
},
|
||||
"fan_sleep_mode": {
|
||||
"value": false,
|
||||
"type": "Switch",
|
||||
"category": "Config"
|
||||
},
|
||||
"current_consumption": {
|
||||
"value": 5.23,
|
||||
"type": "Sensor",
|
||||
"category": "Primary",
|
||||
"unit": "W",
|
||||
"precision_hint": 1
|
||||
},
|
||||
"consumption_today": {
|
||||
"value": 5.23,
|
||||
"type": "Sensor",
|
||||
"category": "Info",
|
||||
"unit": "kWh",
|
||||
"precision_hint": 3
|
||||
},
|
||||
"consumption_this_month": {
|
||||
"value": 15.345,
|
||||
"type": "Sensor",
|
||||
"category": "Info",
|
||||
"unit": "kWh",
|
||||
"precision_hint": 3
|
||||
},
|
||||
"consumption_total": {
|
||||
"value": 30.0049,
|
||||
"type": "Sensor",
|
||||
"category": "Info",
|
||||
"unit": "kWh",
|
||||
"precision_hint": 3
|
||||
},
|
||||
"current": {
|
||||
"value": 5.035,
|
||||
"type": "Sensor",
|
||||
"category": "Primary",
|
||||
"unit": "A",
|
||||
"precision_hint": 2
|
||||
},
|
||||
"voltage": {
|
||||
"value": 121.1,
|
||||
"type": "Sensor",
|
||||
"category": "Primary",
|
||||
"unit": "v",
|
||||
"precision_hint": 1
|
||||
},
|
||||
"device_id": {
|
||||
"value": "94hd2dn298812je12u0931828",
|
||||
"type": "Sensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"signal_level": {
|
||||
"value": 2,
|
||||
"type": "Sensor",
|
||||
"category": "Info"
|
||||
},
|
||||
"rssi": {
|
||||
"value": -62,
|
||||
"type": "Sensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"ssid": {
|
||||
"value": "HOMEWIFI",
|
||||
"type": "Sensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"on_since": {
|
||||
"value": "2024-06-24 10:03:11.046643+01:00",
|
||||
"type": "Sensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"battery_level": {
|
||||
"value": 85,
|
||||
"type": "Sensor",
|
||||
"category": "Info",
|
||||
"unit": "%"
|
||||
},
|
||||
"auto_off_at": {
|
||||
"value": "2024-06-24 10:03:11.046643+01:00",
|
||||
"type": "Sensor",
|
||||
"category": "Info"
|
||||
},
|
||||
"humidity": {
|
||||
"value": 12,
|
||||
"type": "Sensor",
|
||||
"category": "Primary",
|
||||
"unit": "%"
|
||||
},
|
||||
"report_interval": {
|
||||
"value": 16,
|
||||
"type": "Sensor",
|
||||
"category": "Debug",
|
||||
"unit": "%"
|
||||
},
|
||||
"alarm_source": {
|
||||
"value": "",
|
||||
"type": "Sensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"device_time": {
|
||||
"value": "2024-06-24 10:03:11.046643+01:00",
|
||||
"type": "Sensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"temperature": {
|
||||
"value": 19.2,
|
||||
"type": "Sensor",
|
||||
"category": "Debug",
|
||||
"unit": "celsius"
|
||||
},
|
||||
"current_firmware_version": {
|
||||
"value": "1.1.2",
|
||||
"type": "Sensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"available_firmware_version": {
|
||||
"value": "1.1.3",
|
||||
"type": "Sensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"thermostat_mode": {
|
||||
"value": "off",
|
||||
"type": "Sensor",
|
||||
"category": "Primary"
|
||||
},
|
||||
"overheated": {
|
||||
"value": false,
|
||||
"type": "BinarySensor",
|
||||
"category": "Info"
|
||||
},
|
||||
"battery_low": {
|
||||
"value": false,
|
||||
"type": "BinarySensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"update_available": {
|
||||
"value": false,
|
||||
"type": "BinarySensor",
|
||||
"category": "Info"
|
||||
},
|
||||
"cloud_connection": {
|
||||
"value": false,
|
||||
"type": "BinarySensor",
|
||||
"category": "Info"
|
||||
},
|
||||
"temperature_warning": {
|
||||
"value": false,
|
||||
"type": "BinarySensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"humidity_warning": {
|
||||
"value": false,
|
||||
"type": "BinarySensor",
|
||||
"category": "Debug"
|
||||
},
|
||||
"water_alert": {
|
||||
"value": false,
|
||||
"type": "BinarySensor",
|
||||
"category": "Primary"
|
||||
},
|
||||
"is_open": {
|
||||
"value": false,
|
||||
"type": "BinarySensor",
|
||||
"category": "Primary"
|
||||
},
|
||||
"test_alarm": {
|
||||
"value": "<Action>",
|
||||
"type": "Action",
|
||||
"category": "Config"
|
||||
},
|
||||
"stop_alarm": {
|
||||
"value": "<Action>",
|
||||
"type": "Action",
|
||||
"category": "Config"
|
||||
},
|
||||
"smooth_transition_on": {
|
||||
"value": false,
|
||||
"type": "Number",
|
||||
"category": "Config",
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 60
|
||||
},
|
||||
"smooth_transition_off": {
|
||||
"value": false,
|
||||
"type": "Number",
|
||||
"category": "Config",
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 60
|
||||
},
|
||||
"auto_off_minutes": {
|
||||
"value": false,
|
||||
"type": "Number",
|
||||
"category": "Config",
|
||||
"unit": "min",
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 60
|
||||
},
|
||||
"temperature_offset": {
|
||||
"value": false,
|
||||
"type": "Number",
|
||||
"category": "Config",
|
||||
"minimum_value": -10,
|
||||
"maximum_value": 10
|
||||
},
|
||||
"target_temperature": {
|
||||
"value": false,
|
||||
"type": "Number",
|
||||
"category": "Primary"
|
||||
},
|
||||
"fan_speed_level": {
|
||||
"value": 2,
|
||||
"type": "Number",
|
||||
"category": "Primary",
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 4
|
||||
},
|
||||
"light_preset": {
|
||||
"value": "Off",
|
||||
"type": "Choice",
|
||||
"category": "Config",
|
||||
"choices": ["Off", "Preset 1", "Preset 2"]
|
||||
},
|
||||
"alarm_sound": {
|
||||
"value": "Phone Ring",
|
||||
"type": "Choice",
|
||||
"category": "Config",
|
||||
"choices": [
|
||||
"Doorbell Ring 1",
|
||||
"Doorbell Ring 2",
|
||||
"Doorbell Ring 3",
|
||||
"Doorbell Ring 4",
|
||||
"Doorbell Ring 5",
|
||||
"Doorbell Ring 6",
|
||||
"Doorbell Ring 7",
|
||||
"Doorbell Ring 8",
|
||||
"Doorbell Ring 9",
|
||||
"Doorbell Ring 10",
|
||||
"Phone Ring",
|
||||
"Alarm 1",
|
||||
"Alarm 2",
|
||||
"Alarm 3",
|
||||
"Alarm 4",
|
||||
"Dripping Tap",
|
||||
"Alarm 5",
|
||||
"Connection 1",
|
||||
"Connection 2"
|
||||
]
|
||||
},
|
||||
"alarm_volume": {
|
||||
"value": "normal",
|
||||
"type": "Choice",
|
||||
"category": "Config",
|
||||
"choices": ["low", "normal", "high"]
|
||||
}
|
||||
}
|
369
tests/components/tplink/snapshots/test_binary_sensor.ambr
Normal file
369
tests/components/tplink/snapshots/test_binary_sensor.ambr
Normal file
|
@ -0,0 +1,369 @@
|
|||
# serializer version: 1
|
||||
# name: test_states[binary_sensor.my_device_battery_low-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.my_device_battery_low',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.BATTERY: 'battery'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery low',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'battery_low',
|
||||
'unique_id': '123456789ABCDEFGH_battery_low',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_cloud_connection-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.my_device_cloud_connection',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cloud connection',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cloud_connection',
|
||||
'unique_id': '123456789ABCDEFGH_cloud_connection',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_cloud_connection-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'connectivity',
|
||||
'friendly_name': 'my_device Cloud connection',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.my_device_cloud_connection',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.my_device_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Door',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'is_open',
|
||||
'unique_id': '123456789ABCDEFGH_is_open',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'my_device Door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.my_device_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_humidity_warning-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.my_device_humidity_warning',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity warning',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'humidity_warning',
|
||||
'unique_id': '123456789ABCDEFGH_humidity_warning',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_moisture-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.my_device_moisture',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.MOISTURE: 'moisture'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Moisture',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'water_alert',
|
||||
'unique_id': '123456789ABCDEFGH_water_alert',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_moisture-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'moisture',
|
||||
'friendly_name': 'my_device Moisture',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.my_device_moisture',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_overheated-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.my_device_overheated',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Overheated',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'overheated',
|
||||
'unique_id': '123456789ABCDEFGH_overheated',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_overheated-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'problem',
|
||||
'friendly_name': 'my_device Overheated',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.my_device_overheated',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_temperature_warning-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.my_device_temperature_warning',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature warning',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_warning',
|
||||
'unique_id': '123456789ABCDEFGH_temperature_warning',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_update-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.my_device_update',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.UPDATE: 'update'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Update',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'update_available',
|
||||
'unique_id': '123456789ABCDEFGH_update_available',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[binary_sensor.my_device_update-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'update',
|
||||
'friendly_name': 'my_device Update',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.my_device_update',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[my_device-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tplink',
|
||||
'123456789ABCDEFGH',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'TP-Link',
|
||||
'model': 'HS100',
|
||||
'name': 'my_device',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.0.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
127
tests/components/tplink/snapshots/test_button.ambr
Normal file
127
tests/components/tplink/snapshots/test_button.ambr
Normal file
|
@ -0,0 +1,127 @@
|
|||
# serializer version: 1
|
||||
# name: test_states[button.my_device_stop_alarm-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'button.my_device_stop_alarm',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Stop alarm',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'stop_alarm',
|
||||
'unique_id': '123456789ABCDEFGH_stop_alarm',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[button.my_device_stop_alarm-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Stop alarm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.my_device_stop_alarm',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[button.my_device_test_alarm-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'button.my_device_test_alarm',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Test alarm',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'test_alarm',
|
||||
'unique_id': '123456789ABCDEFGH_test_alarm',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[button.my_device_test_alarm-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Test alarm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.my_device_test_alarm',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[my_device-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tplink',
|
||||
'123456789ABCDEFGH',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'TP-Link',
|
||||
'model': 'HS100',
|
||||
'name': 'my_device',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.0.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
94
tests/components/tplink/snapshots/test_climate.ambr
Normal file
94
tests/components/tplink/snapshots/test_climate.ambr
Normal file
|
@ -0,0 +1,94 @@
|
|||
# serializer version: 1
|
||||
# name: test_states[climate.thermostat-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 65536,
|
||||
'min_temp': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.thermostat',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'translation_key': None,
|
||||
'unique_id': '123456789ABCDEFGH_climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[climate.thermostat-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 20,
|
||||
'friendly_name': 'thermostat',
|
||||
'hvac_action': <HVACAction.HEATING: 'heating'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 65536,
|
||||
'min_temp': None,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'temperature': 22,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.thermostat',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[thermostat-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tplink',
|
||||
'123456789ABCDEFGH',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'TP-Link',
|
||||
'model': 'HS100',
|
||||
'name': 'thermostat',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.0.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
194
tests/components/tplink/snapshots/test_fan.ambr
Normal file
194
tests/components/tplink/snapshots/test_fan.ambr
Normal file
|
@ -0,0 +1,194 @@
|
|||
# serializer version: 1
|
||||
# name: test_states[fan.my_device-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.my_device',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <FanEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'unique_id': '123456789ABCDEFGH',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[fan.my_device-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device',
|
||||
'percentage': None,
|
||||
'percentage_step': 25.0,
|
||||
'preset_mode': None,
|
||||
'preset_modes': None,
|
||||
'supported_features': <FanEntityFeature: 1>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.my_device',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[fan.my_device_my_fan_0-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.my_device_my_fan_0',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'my_fan_0',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <FanEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'unique_id': '123456789ABCDEFGH00',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[fan.my_device_my_fan_0-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device my_fan_0',
|
||||
'percentage': None,
|
||||
'percentage_step': 25.0,
|
||||
'preset_mode': None,
|
||||
'preset_modes': None,
|
||||
'supported_features': <FanEntityFeature: 1>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.my_device_my_fan_0',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[fan.my_device_my_fan_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.my_device_my_fan_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'my_fan_1',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <FanEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'unique_id': '123456789ABCDEFGH01',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[fan.my_device_my_fan_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device my_fan_1',
|
||||
'percentage': None,
|
||||
'percentage_step': 25.0,
|
||||
'preset_mode': None,
|
||||
'preset_modes': None,
|
||||
'supported_features': <FanEntityFeature: 1>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.my_device_my_fan_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[my_device-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tplink',
|
||||
'123456789ABCDEFGH',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'TP-Link',
|
||||
'model': 'HS100',
|
||||
'name': 'my_device',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.0.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
255
tests/components/tplink/snapshots/test_number.ambr
Normal file
255
tests/components/tplink/snapshots/test_number.ambr
Normal file
|
@ -0,0 +1,255 @@
|
|||
# serializer version: 1
|
||||
# name: test_states[my_device-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tplink',
|
||||
'123456789ABCDEFGH',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'TP-Link',
|
||||
'model': 'HS100',
|
||||
'name': 'my_device',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.0.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[number.my_device_smooth_off-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65536,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.my_device_smooth_off',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Smooth off',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'smooth_transition_off',
|
||||
'unique_id': '123456789ABCDEFGH_smooth_transition_off',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[number.my_device_smooth_off-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Smooth off',
|
||||
'max': 65536,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.my_device_smooth_off',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'False',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[number.my_device_smooth_on-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65536,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.my_device_smooth_on',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Smooth on',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'smooth_transition_on',
|
||||
'unique_id': '123456789ABCDEFGH_smooth_transition_on',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[number.my_device_smooth_on-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Smooth on',
|
||||
'max': 65536,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.my_device_smooth_on',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'False',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[number.my_device_temperature_offset-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65536,
|
||||
'min': -10,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.my_device_temperature_offset',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature offset',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_offset',
|
||||
'unique_id': '123456789ABCDEFGH_temperature_offset',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[number.my_device_temperature_offset-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Temperature offset',
|
||||
'max': 65536,
|
||||
'min': -10,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.my_device_temperature_offset',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'False',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[number.my_device_turn_off_in-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65536,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.my_device_turn_off_in',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Turn off in',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'auto_off_minutes',
|
||||
'unique_id': '123456789ABCDEFGH_auto_off_minutes',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[number.my_device_turn_off_in-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Turn off in',
|
||||
'max': 65536,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.my_device_turn_off_in',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'False',
|
||||
})
|
||||
# ---
|
238
tests/components/tplink/snapshots/test_select.ambr
Normal file
238
tests/components/tplink/snapshots/test_select.ambr
Normal file
|
@ -0,0 +1,238 @@
|
|||
# serializer version: 1
|
||||
# name: test_states[my_device-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tplink',
|
||||
'123456789ABCDEFGH',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'TP-Link',
|
||||
'model': 'HS100',
|
||||
'name': 'my_device',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.0.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[select.my_device_alarm_sound-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'Doorbell Ring 1',
|
||||
'Doorbell Ring 2',
|
||||
'Doorbell Ring 3',
|
||||
'Doorbell Ring 4',
|
||||
'Doorbell Ring 5',
|
||||
'Doorbell Ring 6',
|
||||
'Doorbell Ring 7',
|
||||
'Doorbell Ring 8',
|
||||
'Doorbell Ring 9',
|
||||
'Doorbell Ring 10',
|
||||
'Phone Ring',
|
||||
'Alarm 1',
|
||||
'Alarm 2',
|
||||
'Alarm 3',
|
||||
'Alarm 4',
|
||||
'Dripping Tap',
|
||||
'Alarm 5',
|
||||
'Connection 1',
|
||||
'Connection 2',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.my_device_alarm_sound',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Alarm sound',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'alarm_sound',
|
||||
'unique_id': '123456789ABCDEFGH_alarm_sound',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[select.my_device_alarm_sound-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Alarm sound',
|
||||
'options': list([
|
||||
'Doorbell Ring 1',
|
||||
'Doorbell Ring 2',
|
||||
'Doorbell Ring 3',
|
||||
'Doorbell Ring 4',
|
||||
'Doorbell Ring 5',
|
||||
'Doorbell Ring 6',
|
||||
'Doorbell Ring 7',
|
||||
'Doorbell Ring 8',
|
||||
'Doorbell Ring 9',
|
||||
'Doorbell Ring 10',
|
||||
'Phone Ring',
|
||||
'Alarm 1',
|
||||
'Alarm 2',
|
||||
'Alarm 3',
|
||||
'Alarm 4',
|
||||
'Dripping Tap',
|
||||
'Alarm 5',
|
||||
'Connection 1',
|
||||
'Connection 2',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.my_device_alarm_sound',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Phone Ring',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[select.my_device_alarm_volume-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'low',
|
||||
'normal',
|
||||
'high',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.my_device_alarm_volume',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Alarm volume',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'alarm_volume',
|
||||
'unique_id': '123456789ABCDEFGH_alarm_volume',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[select.my_device_alarm_volume-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Alarm volume',
|
||||
'options': list([
|
||||
'low',
|
||||
'normal',
|
||||
'high',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.my_device_alarm_volume',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'normal',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[select.my_device_light_preset-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'Off',
|
||||
'Preset 1',
|
||||
'Preset 2',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.my_device_light_preset',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Light preset',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'light_preset',
|
||||
'unique_id': '123456789ABCDEFGH_light_preset',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[select.my_device_light_preset-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Light preset',
|
||||
'options': list([
|
||||
'Off',
|
||||
'Preset 1',
|
||||
'Preset 2',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.my_device_light_preset',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Off',
|
||||
})
|
||||
# ---
|
790
tests/components/tplink/snapshots/test_sensor.ambr
Normal file
790
tests/components/tplink/snapshots/test_sensor.ambr
Normal file
|
@ -0,0 +1,790 @@
|
|||
# serializer version: 1
|
||||
# name: test_states[my_device-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tplink',
|
||||
'123456789ABCDEFGH',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'TP-Link',
|
||||
'model': 'HS100',
|
||||
'name': 'my_device',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.0.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_alarm_source-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_alarm_source',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Alarm source',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'alarm_source',
|
||||
'unique_id': '123456789ABCDEFGH_alarm_source',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_auto_off_at-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_auto_off_at',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Auto off at',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'auto_off_at',
|
||||
'unique_id': '123456789ABCDEFGH_auto_off_at',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_auto_off_at-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'my_device Auto off at',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_auto_off_at',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2024-06-24T09:03:11+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_battery_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_battery_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery level',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'battery_level',
|
||||
'unique_id': '123456789ABCDEFGH_battery_level',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_battery_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'my_device Battery level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_battery_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '85',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_device_current',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Current',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'current',
|
||||
'unique_id': '123456789ABCDEFGH_current_a',
|
||||
'unit_of_measurement': 'A',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_current-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'current',
|
||||
'friendly_name': 'my_device Current',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'A',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_current',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.04',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_current_consumption-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_device_current_consumption',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Current consumption',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'current_consumption',
|
||||
'unique_id': '123456789ABCDEFGH_current_power_w',
|
||||
'unit_of_measurement': 'W',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_current_consumption-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'my_device Current consumption',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'W',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_current_consumption',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_device_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_device_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Device time',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'device_time',
|
||||
'unique_id': '123456789ABCDEFGH_device_time',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_device_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'humidity',
|
||||
'unique_id': '123456789ABCDEFGH_humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'my_device Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '12',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_on_since-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_on_since',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'On since',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'on_since',
|
||||
'unique_id': '123456789ABCDEFGH_on_since',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_report_interval-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_report_interval',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Report interval',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'report_interval',
|
||||
'unique_id': '123456789ABCDEFGH_report_interval',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_signal_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_signal_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Signal level',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'signal_level',
|
||||
'unique_id': '123456789ABCDEFGH_signal_level',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_signal_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Signal level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_signal_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_signal_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_signal_strength',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Signal strength',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'rssi',
|
||||
'unique_id': '123456789ABCDEFGH_rssi',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_ssid-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_ssid',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'SSID',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ssid',
|
||||
'unique_id': '123456789ABCDEFGH_ssid',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature',
|
||||
'unique_id': '123456789ABCDEFGH_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_this_month_s_consumption-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_this_month_s_consumption',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 3,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': "This month's consumption",
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'consumption_this_month',
|
||||
'unique_id': '123456789ABCDEFGH_consumption_this_month',
|
||||
'unit_of_measurement': 'kWh',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_this_month_s_consumption-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': "my_device This month's consumption",
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': 'kWh',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_this_month_s_consumption',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '15.345',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_today_s_consumption-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_today_s_consumption',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 3,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': "Today's consumption",
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'consumption_today',
|
||||
'unique_id': '123456789ABCDEFGH_today_energy_kwh',
|
||||
'unit_of_measurement': 'kWh',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_today_s_consumption-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': "my_device Today's consumption",
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': 'kWh',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_today_s_consumption',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.23',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_total_consumption-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_device_total_consumption',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 3,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Total consumption',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'consumption_total',
|
||||
'unique_id': '123456789ABCDEFGH_total_energy_kwh',
|
||||
'unit_of_measurement': 'kWh',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_total_consumption-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'my_device Total consumption',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': 'kWh',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_total_consumption',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '30.005',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_voltage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_device_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Voltage',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'voltage',
|
||||
'unique_id': '123456789ABCDEFGH_voltage',
|
||||
'unit_of_measurement': 'v',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[sensor.my_device_voltage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'voltage',
|
||||
'friendly_name': 'my_device Voltage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'v',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_device_voltage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '121.1',
|
||||
})
|
||||
# ---
|
311
tests/components/tplink/snapshots/test_switch.ambr
Normal file
311
tests/components/tplink/snapshots/test_switch.ambr
Normal file
|
@ -0,0 +1,311 @@
|
|||
# serializer version: 1
|
||||
# name: test_states[my_device-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '1.0.0',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tplink',
|
||||
'123456789ABCDEFGH',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'TP-Link',
|
||||
'model': 'HS100',
|
||||
'name': 'my_device',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.0.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.my_device',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '123456789ABCDEFGH',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.my_device',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_auto_off_enabled-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.my_device_auto_off_enabled',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Auto off enabled',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'auto_off_enabled',
|
||||
'unique_id': '123456789ABCDEFGH_auto_off_enabled',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_auto_off_enabled-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Auto off enabled',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.my_device_auto_off_enabled',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_auto_update_enabled-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.my_device_auto_update_enabled',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Auto update enabled',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'auto_update_enabled',
|
||||
'unique_id': '123456789ABCDEFGH_auto_update_enabled',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_auto_update_enabled-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Auto update enabled',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.my_device_auto_update_enabled',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_fan_sleep_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.my_device_fan_sleep_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Fan sleep mode',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'fan_sleep_mode',
|
||||
'unique_id': '123456789ABCDEFGH_fan_sleep_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_fan_sleep_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Fan sleep mode',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.my_device_fan_sleep_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_led-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.my_device_led',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'LED',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'led',
|
||||
'unique_id': '123456789ABCDEFGH_led',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_led-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device LED',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.my_device_led',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_smooth_transitions-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.my_device_smooth_transitions',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Smooth transitions',
|
||||
'platform': 'tplink',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'smooth_transitions',
|
||||
'unique_id': '123456789ABCDEFGH_smooth_transitions',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_states[switch.my_device_smooth_transitions-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Smooth transitions',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.my_device_smooth_transitions',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
124
tests/components/tplink/test_binary_sensor.py
Normal file
124
tests/components/tplink/test_binary_sensor.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
"""Tests for tplink binary_sensor platform."""
|
||||
|
||||
from kasa import Feature
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.tplink.binary_sensor import BINARY_SENSOR_DESCRIPTIONS
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.components.tplink.entity import EXCLUDED_FEATURES
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
DEVICE_ID,
|
||||
MAC_ADDRESS,
|
||||
_mocked_device,
|
||||
_mocked_feature,
|
||||
_mocked_strip_children,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
setup_platform_for_device,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_feature_binary_sensor() -> Feature:
|
||||
"""Return mocked tplink binary sensor feature."""
|
||||
return _mocked_feature(
|
||||
"overheated",
|
||||
value=False,
|
||||
name="Overheated",
|
||||
type_=Feature.Type.BinarySensor,
|
||||
category=Feature.Category.Primary,
|
||||
)
|
||||
|
||||
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
features = {description.key for description in BINARY_SENSOR_DESCRIPTIONS}
|
||||
features.update(EXCLUDED_FEATURES)
|
||||
device = _mocked_device(alias="my_device", features=features)
|
||||
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.BINARY_SENSOR, device
|
||||
)
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
for excluded in EXCLUDED_FEATURES:
|
||||
assert hass.states.get(f"sensor.my_device_{excluded}") is None
|
||||
|
||||
|
||||
async def test_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mocked_feature_binary_sensor: Feature,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
mocked_feature = mocked_feature_binary_sensor
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
|
||||
plug = _mocked_device(alias="my_plug", features=[mocked_feature])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The entity_id is based on standard name from core.
|
||||
entity_id = "binary_sensor.my_plug_overheated"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}"
|
||||
|
||||
|
||||
async def test_binary_sensor_children(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mocked_feature_binary_sensor: Feature,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
mocked_feature = mocked_feature_binary_sensor
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_device(
|
||||
alias="my_plug",
|
||||
features=[mocked_feature],
|
||||
children=_mocked_strip_children(features=[mocked_feature]),
|
||||
)
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "binary_sensor.my_plug_overheated"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
device = device_registry.async_get(entity.device_id)
|
||||
|
||||
for plug_id in range(2):
|
||||
child_entity_id = f"binary_sensor.my_plug_plug{plug_id}_overheated"
|
||||
child_entity = entity_registry.async_get(child_entity_id)
|
||||
assert child_entity
|
||||
assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}"
|
||||
assert child_entity.device_id != entity.device_id
|
||||
child_device = device_registry.async_get(child_entity.device_id)
|
||||
assert child_device
|
||||
assert child_device.via_device_id == device.id
|
153
tests/components/tplink/test_button.py
Normal file
153
tests/components/tplink/test_button.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
"""Tests for tplink button platform."""
|
||||
|
||||
from kasa import Feature
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.components.tplink.button import BUTTON_DESCRIPTIONS
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.components.tplink.entity import EXCLUDED_FEATURES
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
DEVICE_ID,
|
||||
MAC_ADDRESS,
|
||||
_mocked_device,
|
||||
_mocked_feature,
|
||||
_mocked_strip_children,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
setup_platform_for_device,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_feature_button() -> Feature:
|
||||
"""Return mocked tplink binary sensor feature."""
|
||||
return _mocked_feature(
|
||||
"test_alarm",
|
||||
value="<Action>",
|
||||
name="Test alarm",
|
||||
type_=Feature.Type.Action,
|
||||
category=Feature.Category.Primary,
|
||||
)
|
||||
|
||||
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
features = {description.key for description in BUTTON_DESCRIPTIONS}
|
||||
features.update(EXCLUDED_FEATURES)
|
||||
device = _mocked_device(alias="my_device", features=features)
|
||||
|
||||
await setup_platform_for_device(hass, mock_config_entry, Platform.BUTTON, device)
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
for excluded in EXCLUDED_FEATURES:
|
||||
assert hass.states.get(f"sensor.my_device_{excluded}") is None
|
||||
|
||||
|
||||
async def test_button(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mocked_feature_button: Feature,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
mocked_feature = mocked_feature_button
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
|
||||
plug = _mocked_device(alias="my_plug", features=[mocked_feature])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The entity_id is based on standard name from core.
|
||||
entity_id = "button.my_plug_test_alarm"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}"
|
||||
|
||||
|
||||
async def test_button_children(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mocked_feature_button: Feature,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
mocked_feature = mocked_feature_button
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_device(
|
||||
alias="my_plug",
|
||||
features=[mocked_feature],
|
||||
children=_mocked_strip_children(features=[mocked_feature]),
|
||||
)
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "button.my_plug_test_alarm"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
device = device_registry.async_get(entity.device_id)
|
||||
|
||||
for plug_id in range(2):
|
||||
child_entity_id = f"button.my_plug_plug{plug_id}_test_alarm"
|
||||
child_entity = entity_registry.async_get(child_entity_id)
|
||||
assert child_entity
|
||||
assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}"
|
||||
assert child_entity.device_id != entity.device_id
|
||||
child_device = device_registry.async_get(child_entity.device_id)
|
||||
assert child_device
|
||||
assert child_device.via_device_id == device.id
|
||||
|
||||
|
||||
async def test_button_press(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mocked_feature_button: Feature,
|
||||
) -> None:
|
||||
"""Test a number entity limits and setting values."""
|
||||
mocked_feature = mocked_feature_button
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_device(alias="my_plug", features=[mocked_feature])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "button.my_plug_test_alarm"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_test_alarm"
|
||||
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
mocked_feature.set_value.assert_called_with(True)
|
226
tests/components/tplink/test_climate.py
Normal file
226
tests/components/tplink/test_climate.py
Normal file
|
@ -0,0 +1,226 @@
|
|||
"""Tests for tplink climate platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from kasa import Device, Feature
|
||||
from kasa.smart.modules.temperaturecontrol import ThermostatState
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_TEMPERATURE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import (
|
||||
DEVICE_ID,
|
||||
_mocked_device,
|
||||
_mocked_feature,
|
||||
setup_platform_for_device,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
ENTITY_ID = "climate.thermostat"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mocked_hub(hass: HomeAssistant) -> Device:
|
||||
"""Return mocked tplink binary sensor feature."""
|
||||
|
||||
features = [
|
||||
_mocked_feature(
|
||||
"temperature", value=20, category=Feature.Category.Primary, unit="celsius"
|
||||
),
|
||||
_mocked_feature(
|
||||
"target_temperature",
|
||||
value=22,
|
||||
type_=Feature.Type.Number,
|
||||
category=Feature.Category.Primary,
|
||||
unit="celsius",
|
||||
),
|
||||
_mocked_feature(
|
||||
"state",
|
||||
value=True,
|
||||
type_=Feature.Type.Switch,
|
||||
category=Feature.Category.Primary,
|
||||
),
|
||||
_mocked_feature(
|
||||
"thermostat_mode",
|
||||
value=ThermostatState.Heating,
|
||||
type_=Feature.Type.Choice,
|
||||
category=Feature.Category.Primary,
|
||||
),
|
||||
]
|
||||
|
||||
thermostat = _mocked_device(
|
||||
alias="thermostat", features=features, device_type=Device.Type.Thermostat
|
||||
)
|
||||
|
||||
return _mocked_device(
|
||||
alias="hub", children=[thermostat], device_type=Device.Type.Hub
|
||||
)
|
||||
|
||||
|
||||
async def test_climate(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mocked_hub: Device,
|
||||
) -> None:
|
||||
"""Test initialization."""
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.CLIMATE, mocked_hub
|
||||
)
|
||||
|
||||
entity = entity_registry.async_get(ENTITY_ID)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_climate"
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] is HVACAction.HEATING
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 22
|
||||
|
||||
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mocked_hub: Device,
|
||||
) -> None:
|
||||
"""Snapshot test."""
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.CLIMATE, mocked_hub
|
||||
)
|
||||
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
async def test_set_temperature(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device
|
||||
) -> None:
|
||||
"""Test that set_temperature service calls the setter."""
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.CLIMATE, mocked_hub
|
||||
)
|
||||
|
||||
mocked_thermostat = mocked_hub.children[0]
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 10},
|
||||
blocking=True,
|
||||
)
|
||||
target_temp_feature = mocked_thermostat.features["target_temperature"]
|
||||
target_temp_feature.set_value.assert_called_with(10)
|
||||
|
||||
|
||||
async def test_set_hvac_mode(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device
|
||||
) -> None:
|
||||
"""Test that set_hvac_mode service works."""
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.CLIMATE, mocked_hub
|
||||
)
|
||||
|
||||
mocked_thermostat = mocked_hub.children[0]
|
||||
mocked_state = mocked_thermostat.features["state"]
|
||||
assert mocked_state is not None
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mocked_state.set_value.assert_called_with(False)
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: [ENTITY_ID], ATTR_HVAC_MODE: HVACMode.HEAT},
|
||||
blocking=True,
|
||||
)
|
||||
mocked_state.set_value.assert_called_with(True)
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: [ENTITY_ID], ATTR_HVAC_MODE: HVACMode.DRY},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_turn_on_and_off(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device
|
||||
) -> None:
|
||||
"""Test that turn_on and turn_off services work as expected."""
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.CLIMATE, mocked_hub
|
||||
)
|
||||
|
||||
mocked_thermostat = mocked_hub.children[0]
|
||||
mocked_state = mocked_thermostat.features["state"]
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: [ENTITY_ID]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mocked_state.set_value.assert_called_with(False)
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: [ENTITY_ID]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mocked_state.set_value.assert_called_with(True)
|
||||
|
||||
|
||||
async def test_unknown_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mocked_hub: Device,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that unknown device modes log a warning and default to off."""
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.CLIMATE, mocked_hub
|
||||
)
|
||||
|
||||
mocked_thermostat = mocked_hub.children[0]
|
||||
mocked_state = mocked_thermostat.features["thermostat_mode"]
|
||||
mocked_state.value = ThermostatState.Unknown
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF
|
||||
assert "Unknown thermostat state, defaulting to OFF" in caplog.text
|
|
@ -2,17 +2,17 @@
|
|||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from kasa import TimeoutException
|
||||
from kasa import TimeoutError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import dhcp
|
||||
from homeassistant.components.tplink import (
|
||||
DOMAIN,
|
||||
AuthenticationException,
|
||||
AuthenticationError,
|
||||
Credentials,
|
||||
DeviceConfig,
|
||||
SmartDeviceException,
|
||||
KasaException,
|
||||
)
|
||||
from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
@ -40,7 +40,7 @@ from . import (
|
|||
MAC_ADDRESS,
|
||||
MAC_ADDRESS2,
|
||||
MODULE,
|
||||
_mocked_bulb,
|
||||
_mocked_device,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
_patch_single_discovery,
|
||||
|
@ -120,7 +120,7 @@ async def test_discovery_auth(
|
|||
) -> None:
|
||||
"""Test authenticated discovery."""
|
||||
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationException
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
|
@ -155,8 +155,8 @@ async def test_discovery_auth(
|
|||
@pytest.mark.parametrize(
|
||||
("error_type", "errors_msg", "error_placement"),
|
||||
[
|
||||
(AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD),
|
||||
(SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"),
|
||||
(AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD),
|
||||
(KasaException("smart_device_error_details"), "cannot_connect", "base"),
|
||||
],
|
||||
ids=["invalid-auth", "unknown-error"],
|
||||
)
|
||||
|
@ -170,7 +170,7 @@ async def test_discovery_auth_errors(
|
|||
error_placement,
|
||||
) -> None:
|
||||
"""Test handling of discovery authentication errors."""
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationException
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
default_connect_side_effect = mock_connect["connect"].side_effect
|
||||
mock_connect["connect"].side_effect = error_type
|
||||
|
||||
|
@ -223,7 +223,7 @@ async def test_discovery_new_credentials(
|
|||
mock_init,
|
||||
) -> None:
|
||||
"""Test setting up discovery with new credentials."""
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationException
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
|
@ -272,10 +272,10 @@ async def test_discovery_new_credentials_invalid(
|
|||
mock_init,
|
||||
) -> None:
|
||||
"""Test setting up discovery with new invalid credentials."""
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationException
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
default_connect_side_effect = mock_connect["connect"].side_effect
|
||||
|
||||
mock_connect["connect"].side_effect = AuthenticationException
|
||||
mock_connect["connect"].side_effect = AuthenticationError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
|
@ -514,7 +514,7 @@ async def test_manual_auth(
|
|||
assert result["step_id"] == "user"
|
||||
assert not result["errors"]
|
||||
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationException
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: IP_ADDRESS}
|
||||
|
@ -544,8 +544,8 @@ async def test_manual_auth(
|
|||
@pytest.mark.parametrize(
|
||||
("error_type", "errors_msg", "error_placement"),
|
||||
[
|
||||
(AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD),
|
||||
(SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"),
|
||||
(AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD),
|
||||
(KasaException("smart_device_error_details"), "cannot_connect", "base"),
|
||||
],
|
||||
ids=["invalid-auth", "unknown-error"],
|
||||
)
|
||||
|
@ -566,7 +566,7 @@ async def test_manual_auth_errors(
|
|||
assert result["step_id"] == "user"
|
||||
assert not result["errors"]
|
||||
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationException
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
default_connect_side_effect = mock_connect["connect"].side_effect
|
||||
mock_connect["connect"].side_effect = error_type
|
||||
|
||||
|
@ -765,7 +765,7 @@ async def test_integration_discovery_with_ip_change(
|
|||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
mock_connect["connect"].side_effect = SmartDeviceException()
|
||||
mock_connect["connect"].side_effect = KasaException()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
@ -797,7 +797,7 @@ async def test_integration_discovery_with_ip_change(
|
|||
config = DeviceConfig.from_dict(DEVICE_CONFIG_DICT_AUTH)
|
||||
|
||||
mock_connect["connect"].reset_mock(side_effect=True)
|
||||
bulb = _mocked_bulb(
|
||||
bulb = _mocked_device(
|
||||
device_config=config,
|
||||
mac=mock_config_entry.unique_id,
|
||||
)
|
||||
|
@ -818,7 +818,7 @@ async def test_dhcp_discovery_with_ip_change(
|
|||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test dhcp discovery with an IP change."""
|
||||
mock_connect["connect"].side_effect = SmartDeviceException()
|
||||
mock_connect["connect"].side_effect = KasaException()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
@ -883,7 +883,7 @@ async def test_reauth_update_from_discovery(
|
|||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
mock_connect["connect"].side_effect = AuthenticationException
|
||||
mock_connect["connect"].side_effect = AuthenticationError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
@ -920,7 +920,7 @@ async def test_reauth_update_from_discovery_with_ip_change(
|
|||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
mock_connect["connect"].side_effect = AuthenticationException()
|
||||
mock_connect["connect"].side_effect = AuthenticationError()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
@ -957,7 +957,7 @@ async def test_reauth_no_update_if_config_and_ip_the_same(
|
|||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth discovery does not update when the host and config are the same."""
|
||||
mock_connect["connect"].side_effect = AuthenticationException()
|
||||
mock_connect["connect"].side_effect = AuthenticationError()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry,
|
||||
|
@ -996,8 +996,8 @@ async def test_reauth_no_update_if_config_and_ip_the_same(
|
|||
@pytest.mark.parametrize(
|
||||
("error_type", "errors_msg", "error_placement"),
|
||||
[
|
||||
(AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD),
|
||||
(SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"),
|
||||
(AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD),
|
||||
(KasaException("smart_device_error_details"), "cannot_connect", "base"),
|
||||
],
|
||||
ids=["invalid-auth", "unknown-error"],
|
||||
)
|
||||
|
@ -1060,8 +1060,8 @@ async def test_reauth_errors(
|
|||
@pytest.mark.parametrize(
|
||||
("error_type", "expected_flow"),
|
||||
[
|
||||
(AuthenticationException, FlowResultType.FORM),
|
||||
(SmartDeviceException, FlowResultType.ABORT),
|
||||
(AuthenticationError, FlowResultType.FORM),
|
||||
(KasaException, FlowResultType.ABORT),
|
||||
],
|
||||
ids=["invalid-auth", "unknown-error"],
|
||||
)
|
||||
|
@ -1119,7 +1119,7 @@ async def test_discovery_timeout_connect(
|
|||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
mock_discovery["discover_single"].side_effect = TimeoutException
|
||||
mock_discovery["discover_single"].side_effect = TimeoutError
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
@ -1149,7 +1149,7 @@ async def test_reauth_update_other_flows(
|
|||
unique_id=MAC_ADDRESS2,
|
||||
)
|
||||
default_side_effect = mock_connect["connect"].side_effect
|
||||
mock_connect["connect"].side_effect = AuthenticationException()
|
||||
mock_connect["connect"].side_effect = AuthenticationError()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_config_entry2.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
import json
|
||||
|
||||
from kasa import SmartDevice
|
||||
from kasa import Device
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import _mocked_bulb, _mocked_plug, initialize_config_entry_for_device
|
||||
from . import _mocked_device, initialize_config_entry_for_device
|
||||
|
||||
from tests.common import load_fixture
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
|
@ -18,13 +18,13 @@ from tests.typing import ClientSessionGenerator
|
|||
("mocked_dev", "fixture_file", "sysinfo_vars", "expected_oui"),
|
||||
[
|
||||
(
|
||||
_mocked_bulb(),
|
||||
_mocked_device(),
|
||||
"tplink-diagnostics-data-bulb-kl130.json",
|
||||
["mic_mac", "deviceId", "oemId", "hwId", "alias"],
|
||||
"AA:BB:CC",
|
||||
),
|
||||
(
|
||||
_mocked_plug(),
|
||||
_mocked_device(),
|
||||
"tplink-diagnostics-data-plug-hs110.json",
|
||||
["mac", "deviceId", "oemId", "hwId", "alias", "longitude_i", "latitude_i"],
|
||||
"AA:BB:CC",
|
||||
|
@ -34,7 +34,7 @@ from tests.typing import ClientSessionGenerator
|
|||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mocked_dev: SmartDevice,
|
||||
mocked_dev: Device,
|
||||
fixture_file: str,
|
||||
sysinfo_vars: list[str],
|
||||
expected_oui: str | None,
|
||||
|
|
154
tests/components/tplink/test_fan.py
Normal file
154
tests/components/tplink/test_fan.py
Normal file
|
@ -0,0 +1,154 @@
|
|||
"""Tests for fan platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from kasa import Device, Module
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_PERCENTAGE,
|
||||
DOMAIN as FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import DEVICE_ID, _mocked_device, setup_platform_for_device, snapshot_platform
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test a fan state."""
|
||||
child_fan_1 = _mocked_device(
|
||||
modules=[Module.Fan], alias="my_fan_0", device_id=f"{DEVICE_ID}00"
|
||||
)
|
||||
child_fan_2 = _mocked_device(
|
||||
modules=[Module.Fan], alias="my_fan_1", device_id=f"{DEVICE_ID}01"
|
||||
)
|
||||
parent_device = _mocked_device(
|
||||
device_id=DEVICE_ID,
|
||||
alias="my_device",
|
||||
children=[child_fan_1, child_fan_2],
|
||||
modules=[Module.Fan],
|
||||
device_type=Device.Type.WallSwitch,
|
||||
)
|
||||
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.FAN, parent_device
|
||||
)
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
async def test_fan_unique_id(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test a fan unique id."""
|
||||
fan = _mocked_device(modules=[Module.Fan], alias="my_fan")
|
||||
await setup_platform_for_device(hass, mock_config_entry, Platform.FAN, fan)
|
||||
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert device_entries
|
||||
entity_id = "fan.my_fan"
|
||||
entity_registry = er.async_get(hass)
|
||||
assert entity_registry.async_get(entity_id).unique_id == DEVICE_ID
|
||||
|
||||
|
||||
async def test_fan(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None:
|
||||
"""Test a color fan and that all transitions are correctly passed."""
|
||||
device = _mocked_device(modules=[Module.Fan], alias="my_fan")
|
||||
fan = device.modules[Module.Fan]
|
||||
fan.fan_speed_level = 0
|
||||
await setup_platform_for_device(hass, mock_config_entry, Platform.FAN, device)
|
||||
|
||||
entity_id = "fan.my_fan"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
fan.set_fan_speed_level.assert_called_once_with(4)
|
||||
fan.set_fan_speed_level.reset_mock()
|
||||
|
||||
fan.fan_speed_level = 4
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
fan.set_fan_speed_level.assert_called_once_with(0)
|
||||
fan.set_fan_speed_level.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 50},
|
||||
blocking=True,
|
||||
)
|
||||
fan.set_fan_speed_level.assert_called_once_with(2)
|
||||
fan.set_fan_speed_level.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 25},
|
||||
blocking=True,
|
||||
)
|
||||
fan.set_fan_speed_level.assert_called_once_with(1)
|
||||
fan.set_fan_speed_level.reset_mock()
|
||||
|
||||
|
||||
async def test_fan_child(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test child fans are added to parent device with the right ids."""
|
||||
child_fan_1 = _mocked_device(
|
||||
modules=[Module.Fan], alias="my_fan_0", device_id=f"{DEVICE_ID}00"
|
||||
)
|
||||
child_fan_2 = _mocked_device(
|
||||
modules=[Module.Fan], alias="my_fan_1", device_id=f"{DEVICE_ID}01"
|
||||
)
|
||||
parent_device = _mocked_device(
|
||||
device_id=DEVICE_ID,
|
||||
alias="my_device",
|
||||
children=[child_fan_1, child_fan_2],
|
||||
modules=[Module.Fan],
|
||||
device_type=Device.Type.WallSwitch,
|
||||
)
|
||||
await setup_platform_for_device(
|
||||
hass, mock_config_entry, Platform.FAN, parent_device
|
||||
)
|
||||
|
||||
entity_id = "fan.my_device"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
|
||||
for fan_id in range(2):
|
||||
child_entity_id = f"fan.my_device_my_fan_{fan_id}"
|
||||
child_entity = entity_registry.async_get(child_entity_id)
|
||||
assert child_entity
|
||||
assert child_entity.unique_id == f"{DEVICE_ID}0{fan_id}"
|
||||
assert child_entity.device_id == entity.device_id
|
|
@ -4,10 +4,10 @@ from __future__ import annotations
|
|||
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from kasa.exceptions import AuthenticationException
|
||||
from kasa import AuthenticationError, Feature, KasaException, Module
|
||||
import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
|
@ -21,19 +21,20 @@ from homeassistant.const import (
|
|||
CONF_USERNAME,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import (
|
||||
CREATE_ENTRY_DATA_AUTH,
|
||||
CREATE_ENTRY_DATA_LEGACY,
|
||||
DEVICE_CONFIG_AUTH,
|
||||
IP_ADDRESS,
|
||||
MAC_ADDRESS,
|
||||
_mocked_dimmer,
|
||||
_mocked_plug,
|
||||
_mocked_device,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
_patch_single_discovery,
|
||||
|
@ -100,12 +101,12 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None:
|
|||
|
||||
|
||||
async def test_dimmer_switch_unique_id_fix_original_entity_still_exists(
|
||||
hass: HomeAssistant, entity_reg: EntityRegistry
|
||||
hass: HomeAssistant, entity_reg: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test no migration happens if the original entity id still exists."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS)
|
||||
config_entry.add_to_hass(hass)
|
||||
dimmer = _mocked_dimmer()
|
||||
dimmer = _mocked_device(alias="My dimmer", modules=[Module.Light])
|
||||
rollout_unique_id = MAC_ADDRESS.replace(":", "").upper()
|
||||
original_unique_id = tplink.legacy_device_id(dimmer)
|
||||
original_dimmer_entity_reg = entity_reg.async_get_or_create(
|
||||
|
@ -129,7 +130,7 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists(
|
|||
_patch_connect(device=dimmer),
|
||||
):
|
||||
await setup.async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
migrated_dimmer_entity_reg = entity_reg.async_get_or_create(
|
||||
config_entry=config_entry,
|
||||
|
@ -238,8 +239,8 @@ async def test_config_entry_device_config_invalid(
|
|||
@pytest.mark.parametrize(
|
||||
("error_type", "entry_state", "reauth_flows"),
|
||||
[
|
||||
(tplink.AuthenticationException, ConfigEntryState.SETUP_ERROR, True),
|
||||
(tplink.SmartDeviceException, ConfigEntryState.SETUP_RETRY, False),
|
||||
(tplink.AuthenticationError, ConfigEntryState.SETUP_ERROR, True),
|
||||
(tplink.KasaException, ConfigEntryState.SETUP_RETRY, False),
|
||||
],
|
||||
ids=["invalid-auth", "unknown-error"],
|
||||
)
|
||||
|
@ -275,15 +276,15 @@ async def test_plug_auth_fails(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
plug = _mocked_plug()
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
device = _mocked_device(alias="my_plug", features=["state"])
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "switch.my_plug"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
plug.update = AsyncMock(side_effect=AuthenticationException)
|
||||
device.update = AsyncMock(side_effect=AuthenticationError)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
|
||||
await hass.async_block_till_done()
|
||||
|
@ -298,3 +299,166 @@ async def test_plug_auth_fails(hass: HomeAssistant) -> None:
|
|||
)
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
async def test_update_attrs_fails_in_init(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a smart plug auth failure."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
light = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light_module = light.modules[Module.Light]
|
||||
p = PropertyMock(side_effect=KasaException)
|
||||
type(light_module).color_temp = p
|
||||
light.__str__ = lambda _: "MockLight"
|
||||
with _patch_discovery(device=light), _patch_connect(device=light):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_light"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert "Unable to read data for MockLight None:" in caplog.text
|
||||
|
||||
|
||||
async def test_update_attrs_fails_on_update(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a smart plug auth failure."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
light = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light_module = light.modules[Module.Light]
|
||||
|
||||
with _patch_discovery(device=light), _patch_connect(device=light):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_light"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
p = PropertyMock(side_effect=KasaException)
|
||||
type(light_module).color_temp = p
|
||||
light.__str__ = lambda _: "MockLight"
|
||||
freezer.tick(5)
|
||||
async_fire_time_changed(hass)
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert f"Unable to read data for MockLight {entity_id}:" in caplog.text
|
||||
# Check only logs once
|
||||
caplog.clear()
|
||||
freezer.tick(5)
|
||||
async_fire_time_changed(hass)
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert f"Unable to read data for MockLight {entity_id}:" not in caplog.text
|
||||
|
||||
|
||||
async def test_feature_no_category(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a strip unique id."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
dev = _mocked_device(
|
||||
alias="my_plug",
|
||||
features=["led"],
|
||||
)
|
||||
dev.features["led"].category = Feature.Category.Unset
|
||||
with _patch_discovery(device=dev), _patch_connect(device=dev):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "switch.my_plug_led"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.entity_category == EntityCategory.DIAGNOSTIC
|
||||
assert "Unhandled category Category.Unset, fallback to DIAGNOSTIC" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("identifier_base", "expected_message", "expected_count"),
|
||||
[
|
||||
pytest.param("C0:06:C3:42:54:2B", "Replaced", 1, id="success"),
|
||||
pytest.param("123456789", "Unable to replace", 3, id="failure"),
|
||||
],
|
||||
)
|
||||
async def test_unlink_devices(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
identifier_base,
|
||||
expected_message,
|
||||
expected_count,
|
||||
) -> None:
|
||||
"""Test for unlinking child device ids."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**CREATE_ENTRY_DATA_LEGACY},
|
||||
entry_id="123456",
|
||||
unique_id="any",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# Setup initial device registry, with linkages
|
||||
mac = "C0:06:C3:42:54:2B"
|
||||
identifiers = [
|
||||
(DOMAIN, identifier_base),
|
||||
(DOMAIN, f"{identifier_base}_0001"),
|
||||
(DOMAIN, f"{identifier_base}_0002"),
|
||||
]
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id="123456",
|
||||
connections={
|
||||
(dr.CONNECTION_NETWORK_MAC, mac.lower()),
|
||||
},
|
||||
identifiers=set(identifiers),
|
||||
model="hs300",
|
||||
name="dummy",
|
||||
)
|
||||
device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
|
||||
assert device_entries[0].connections == {
|
||||
(dr.CONNECTION_NETWORK_MAC, mac.lower()),
|
||||
}
|
||||
assert device_entries[0].identifiers == set(identifiers)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
|
||||
assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, mac.lower())}
|
||||
# If expected count is 1 will be the first identifier only
|
||||
expected_identifiers = identifiers[:expected_count]
|
||||
assert device_entries[0].identifiers == set(expected_identifiers)
|
||||
assert entry.version == 1
|
||||
assert entry.minor_version == 3
|
||||
|
||||
msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}"
|
||||
assert msg in caplog.text
|
||||
|
|
|
@ -5,7 +5,16 @@ from __future__ import annotations
|
|||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
from kasa import AuthenticationException, SmartDeviceException, TimeoutException
|
||||
from kasa import (
|
||||
AuthenticationError,
|
||||
DeviceType,
|
||||
KasaException,
|
||||
LightState,
|
||||
Module,
|
||||
TimeoutError,
|
||||
)
|
||||
from kasa.interfaces import LightEffect
|
||||
from kasa.iot import IotDevice
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import tplink
|
||||
|
@ -23,6 +32,7 @@ from homeassistant.components.light import (
|
|||
ATTR_TRANSITION,
|
||||
ATTR_XY_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
EFFECT_OFF,
|
||||
)
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
|
@ -34,9 +44,9 @@ from homeassistant.setup import async_setup_component
|
|||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import (
|
||||
DEVICE_ID,
|
||||
MAC_ADDRESS,
|
||||
_mocked_bulb,
|
||||
_mocked_smart_light_strip,
|
||||
_mocked_device,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
_patch_single_discovery,
|
||||
|
@ -45,37 +55,77 @@ from . import (
|
|||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_type"),
|
||||
[
|
||||
pytest.param(DeviceType.Dimmer, id="Dimmer"),
|
||||
pytest.param(DeviceType.Bulb, id="Bulb"),
|
||||
pytest.param(DeviceType.LightStrip, id="LightStrip"),
|
||||
pytest.param(DeviceType.WallSwitch, id="WallSwitch"),
|
||||
],
|
||||
)
|
||||
async def test_light_unique_id(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, device_type
|
||||
) -> None:
|
||||
"""Test a light unique id."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.color_temp = None
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
light = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light.device_type = device_type
|
||||
with _patch_discovery(device=light), _patch_connect(device=light):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
assert entity_registry.async_get(entity_id).unique_id == "AABBCCDDEEFF"
|
||||
entity_id = "light.my_light"
|
||||
assert (
|
||||
entity_registry.async_get(entity_id).unique_id
|
||||
== MAC_ADDRESS.replace(":", "").upper()
|
||||
)
|
||||
|
||||
|
||||
async def test_legacy_dimmer_unique_id(hass: HomeAssistant) -> None:
|
||||
"""Test a light unique id."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
light = _mocked_device(
|
||||
modules=[Module.Light],
|
||||
alias="my_light",
|
||||
spec=IotDevice,
|
||||
device_id="aa:bb:cc:dd:ee:ff",
|
||||
)
|
||||
light.device_type = DeviceType.Dimmer
|
||||
|
||||
with _patch_discovery(device=light), _patch_connect(device=light):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_light"
|
||||
entity_registry = er.async_get(hass)
|
||||
assert entity_registry.async_get(entity_id).unique_id == "aa:bb:cc:dd:ee:ff"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bulb", "transition"), [(_mocked_bulb(), 2.0), (_mocked_smart_light_strip(), None)]
|
||||
("device", "transition"),
|
||||
[
|
||||
(_mocked_device(modules=[Module.Light]), 2.0),
|
||||
(_mocked_device(modules=[Module.Light, Module.LightEffect]), None),
|
||||
],
|
||||
)
|
||||
async def test_color_light(
|
||||
hass: HomeAssistant, bulb: MagicMock, transition: float | None
|
||||
hass: HomeAssistant, device: MagicMock, transition: float | None
|
||||
) -> None:
|
||||
"""Test a color light and that all transitions are correctly passed."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb.color_temp = None
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
light = device.modules[Module.Light]
|
||||
light.color_temp = None
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -101,11 +151,16 @@ async def test_color_light(
|
|||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True
|
||||
)
|
||||
bulb.turn_off.assert_called_once_with(transition=KASA_TRANSITION_VALUE)
|
||||
light.set_state.assert_called_once_with(
|
||||
LightState(light_on=False, transition=KASA_TRANSITION_VALUE)
|
||||
)
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(LIGHT_DOMAIN, "turn_on", BASE_PAYLOAD, blocking=True)
|
||||
bulb.turn_on.assert_called_once_with(transition=KASA_TRANSITION_VALUE)
|
||||
bulb.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once_with(
|
||||
LightState(light_on=True, transition=KASA_TRANSITION_VALUE)
|
||||
)
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -113,8 +168,8 @@ async def test_color_light(
|
|||
{**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE)
|
||||
bulb.set_brightness.reset_mock()
|
||||
light.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE)
|
||||
light.set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -122,10 +177,10 @@ async def test_color_light(
|
|||
{**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(
|
||||
light.set_color_temp.assert_called_with(
|
||||
6666, brightness=None, transition=KASA_TRANSITION_VALUE
|
||||
)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
light.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -133,10 +188,10 @@ async def test_color_light(
|
|||
{**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(
|
||||
light.set_color_temp.assert_called_with(
|
||||
6666, brightness=None, transition=KASA_TRANSITION_VALUE
|
||||
)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
light.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -144,8 +199,8 @@ async def test_color_light(
|
|||
{**BASE_PAYLOAD, ATTR_HS_COLOR: (10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE)
|
||||
bulb.set_hsv.reset_mock()
|
||||
light.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE)
|
||||
light.set_hsv.reset_mock()
|
||||
|
||||
|
||||
async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
||||
|
@ -154,14 +209,15 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.is_variable_color_temp = False
|
||||
type(bulb).color_temp = PropertyMock(side_effect=Exception)
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light = device.modules[Module.Light]
|
||||
light.is_variable_color_temp = False
|
||||
type(light).color_temp = PropertyMock(side_effect=Exception)
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
|
@ -176,13 +232,14 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
|||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_off.assert_called_once()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_on.assert_called_once()
|
||||
bulb.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -190,8 +247,8 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_brightness.assert_called_with(39, transition=None)
|
||||
bulb.set_brightness.reset_mock()
|
||||
light.set_brightness.assert_called_with(39, transition=None)
|
||||
light.set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -199,12 +256,16 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_hsv.assert_called_with(10, 30, None, transition=None)
|
||||
bulb.set_hsv.reset_mock()
|
||||
light.set_hsv.assert_called_with(10, 30, None, transition=None)
|
||||
light.set_hsv.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bulb", "is_color"), [(_mocked_bulb(), True), (_mocked_smart_light_strip(), False)]
|
||||
("bulb", "is_color"),
|
||||
[
|
||||
(_mocked_device(modules=[Module.Light], alias="my_light"), True),
|
||||
(_mocked_device(modules=[Module.Light], alias="my_light"), False),
|
||||
],
|
||||
)
|
||||
async def test_color_temp_light(
|
||||
hass: HomeAssistant, bulb: MagicMock, is_color: bool
|
||||
|
@ -214,22 +275,24 @@ async def test_color_temp_light(
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb.is_color = is_color
|
||||
bulb.color_temp = 4000
|
||||
bulb.is_variable_color_temp = True
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light = device.modules[Module.Light]
|
||||
light.is_color = is_color
|
||||
light.color_temp = 4000
|
||||
light.is_variable_color_temp = True
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert attributes[ATTR_COLOR_MODE] == "color_temp"
|
||||
if bulb.is_color:
|
||||
if light.is_color:
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"]
|
||||
else:
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"]
|
||||
|
@ -240,13 +303,14 @@ async def test_color_temp_light(
|
|||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_off.assert_called_once()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_on.assert_called_once()
|
||||
bulb.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -254,8 +318,8 @@ async def test_color_temp_light(
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_brightness.assert_called_with(39, transition=None)
|
||||
bulb.set_brightness.reset_mock()
|
||||
light.set_brightness.assert_called_with(39, transition=None)
|
||||
light.set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -263,8 +327,8 @@ async def test_color_temp_light(
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 6666},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
light.set_color_temp.assert_called_with(6666, brightness=None, transition=None)
|
||||
light.set_color_temp.reset_mock()
|
||||
|
||||
# Verify color temp is clamped to the valid range
|
||||
await hass.services.async_call(
|
||||
|
@ -273,8 +337,8 @@ async def test_color_temp_light(
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 20000},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(9000, brightness=None, transition=None)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
light.set_color_temp.assert_called_with(9000, brightness=None, transition=None)
|
||||
light.set_color_temp.reset_mock()
|
||||
|
||||
# Verify color temp is clamped to the valid range
|
||||
await hass.services.async_call(
|
||||
|
@ -283,8 +347,8 @@ async def test_color_temp_light(
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 1},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(4000, brightness=None, transition=None)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
light.set_color_temp.assert_called_with(4000, brightness=None, transition=None)
|
||||
light.set_color_temp.reset_mock()
|
||||
|
||||
|
||||
async def test_brightness_only_light(hass: HomeAssistant) -> None:
|
||||
|
@ -293,15 +357,16 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.is_color = False
|
||||
bulb.is_variable_color_temp = False
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light = device.modules[Module.Light]
|
||||
light.is_color = False
|
||||
light.is_variable_color_temp = False
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
|
@ -313,13 +378,14 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None:
|
|||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_off.assert_called_once()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_on.assert_called_once()
|
||||
bulb.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -327,8 +393,8 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None:
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_brightness.assert_called_with(39, transition=None)
|
||||
bulb.set_brightness.reset_mock()
|
||||
light.set_brightness.assert_called_with(39, transition=None)
|
||||
light.set_brightness.reset_mock()
|
||||
|
||||
|
||||
async def test_on_off_light(hass: HomeAssistant) -> None:
|
||||
|
@ -337,16 +403,17 @@ async def test_on_off_light(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.is_color = False
|
||||
bulb.is_variable_color_temp = False
|
||||
bulb.is_dimmable = False
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light = device.modules[Module.Light]
|
||||
light.is_color = False
|
||||
light.is_variable_color_temp = False
|
||||
light.is_dimmable = False
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
|
@ -356,13 +423,14 @@ async def test_on_off_light(hass: HomeAssistant) -> None:
|
|||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_off.assert_called_once()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_on.assert_called_once()
|
||||
bulb.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
|
||||
async def test_off_at_start_light(hass: HomeAssistant) -> None:
|
||||
|
@ -371,17 +439,18 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.is_color = False
|
||||
bulb.is_variable_color_temp = False
|
||||
bulb.is_dimmable = False
|
||||
bulb.is_on = False
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light = device.modules[Module.Light]
|
||||
light.is_color = False
|
||||
light.is_variable_color_temp = False
|
||||
light.is_dimmable = False
|
||||
light.state = LightState(light_on=False)
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "off"
|
||||
|
@ -395,15 +464,16 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.is_dimmer = True
|
||||
bulb.is_on = False
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light = device.modules[Module.Light]
|
||||
device.device_type = DeviceType.Dimmer
|
||||
light.state = LightState(light_on=False)
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "off"
|
||||
|
@ -411,8 +481,17 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None:
|
|||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_on.assert_called_once_with(transition=1)
|
||||
bulb.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once_with(
|
||||
LightState(
|
||||
light_on=True,
|
||||
brightness=None,
|
||||
hue=None,
|
||||
saturation=None,
|
||||
color_temp=None,
|
||||
transition=1,
|
||||
)
|
||||
)
|
||||
light.set_state.reset_mock()
|
||||
|
||||
|
||||
async def test_smart_strip_effects(hass: HomeAssistant) -> None:
|
||||
|
@ -421,22 +500,26 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
strip = _mocked_smart_light_strip()
|
||||
device = _mocked_device(
|
||||
modules=[Module.Light, Module.LightEffect], alias="my_light"
|
||||
)
|
||||
light = device.modules[Module.Light]
|
||||
light_effect = device.modules[Module.LightEffect]
|
||||
|
||||
with (
|
||||
_patch_discovery(device=strip),
|
||||
_patch_single_discovery(device=strip),
|
||||
_patch_connect(device=strip),
|
||||
_patch_discovery(device=device),
|
||||
_patch_single_discovery(device=device),
|
||||
_patch_connect(device=device),
|
||||
):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_EFFECT] == "Effect1"
|
||||
assert state.attributes[ATTR_EFFECT_LIST] == ["Effect1", "Effect2"]
|
||||
assert state.attributes[ATTR_EFFECT_LIST] == ["Off", "Effect1", "Effect2"]
|
||||
|
||||
# Ensure setting color temp when an effect
|
||||
# is in progress calls set_hsv to clear the effect
|
||||
|
@ -446,10 +529,10 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None:
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 4000},
|
||||
blocking=True,
|
||||
)
|
||||
strip.set_hsv.assert_called_once_with(0, 0, None)
|
||||
strip.set_color_temp.assert_called_once_with(4000, brightness=None, transition=None)
|
||||
strip.set_hsv.reset_mock()
|
||||
strip.set_color_temp.reset_mock()
|
||||
light.set_hsv.assert_called_once_with(0, 0, None)
|
||||
light.set_color_temp.assert_called_once_with(4000, brightness=None, transition=None)
|
||||
light.set_hsv.reset_mock()
|
||||
light.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
|
@ -457,21 +540,20 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None:
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Effect2"},
|
||||
blocking=True,
|
||||
)
|
||||
strip.set_effect.assert_called_once_with(
|
||||
light_effect.set_effect.assert_called_once_with(
|
||||
"Effect2", brightness=None, transition=None
|
||||
)
|
||||
strip.set_effect.reset_mock()
|
||||
light_effect.set_effect.reset_mock()
|
||||
|
||||
strip.effect = {"name": "Effect1", "enable": 0, "custom": 0}
|
||||
light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_EFFECT] is None
|
||||
assert state.attributes[ATTR_EFFECT] == EFFECT_OFF
|
||||
|
||||
strip.is_off = True
|
||||
strip.is_on = False
|
||||
light.state = LightState(light_on=False)
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -485,12 +567,11 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None:
|
|||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
strip.turn_on.assert_called_once()
|
||||
strip.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
strip.is_off = False
|
||||
strip.is_on = True
|
||||
strip.effect_list = None
|
||||
light.state = LightState(light_on=True)
|
||||
light_effect.effect_list = None
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -505,13 +586,17 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
strip = _mocked_smart_light_strip()
|
||||
device = _mocked_device(
|
||||
modules=[Module.Light, Module.LightEffect], alias="my_light"
|
||||
)
|
||||
light = device.modules[Module.Light]
|
||||
light_effect = device.modules[Module.LightEffect]
|
||||
|
||||
with _patch_discovery(device=strip), _patch_connect(device=strip):
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
@ -526,7 +611,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
},
|
||||
blocking=True,
|
||||
)
|
||||
strip.set_custom_effect.assert_called_once_with(
|
||||
light_effect.set_custom_effect.assert_called_once_with(
|
||||
{
|
||||
"custom": 1,
|
||||
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
|
||||
|
@ -543,7 +628,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
"backgrounds": [(340, 20, 50), (20, 50, 50), (0, 100, 50)],
|
||||
}
|
||||
)
|
||||
strip.set_custom_effect.reset_mock()
|
||||
light_effect.set_custom_effect.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
|
@ -555,7 +640,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
},
|
||||
blocking=True,
|
||||
)
|
||||
strip.set_custom_effect.assert_called_once_with(
|
||||
light_effect.set_custom_effect.assert_called_once_with(
|
||||
{
|
||||
"custom": 1,
|
||||
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
|
||||
|
@ -571,9 +656,9 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
"random_seed": 600,
|
||||
}
|
||||
)
|
||||
strip.set_custom_effect.reset_mock()
|
||||
light_effect.set_custom_effect.reset_mock()
|
||||
|
||||
strip.effect = {
|
||||
light_effect.effect = {
|
||||
"custom": 1,
|
||||
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
|
||||
"brightness": 100,
|
||||
|
@ -586,15 +671,8 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
strip.is_off = True
|
||||
strip.is_on = False
|
||||
strip.effect = {
|
||||
"custom": 1,
|
||||
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
|
||||
"brightness": 100,
|
||||
"name": "Custom",
|
||||
"enable": 0,
|
||||
}
|
||||
light.state = LightState(light_on=False)
|
||||
light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -608,8 +686,8 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
strip.turn_on.assert_called_once()
|
||||
strip.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
|
@ -631,7 +709,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
strip.set_custom_effect.assert_called_once_with(
|
||||
light_effect.set_custom_effect.assert_called_once_with(
|
||||
{
|
||||
"custom": 1,
|
||||
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
|
||||
|
@ -653,7 +731,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
|
|||
"transition_range": [2000, 3000],
|
||||
}
|
||||
)
|
||||
strip.set_custom_effect.reset_mock()
|
||||
light_effect.set_custom_effect.reset_mock()
|
||||
|
||||
|
||||
async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> None:
|
||||
|
@ -662,19 +740,17 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) ->
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
strip = _mocked_smart_light_strip()
|
||||
strip.effect = {
|
||||
"custom": 1,
|
||||
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
|
||||
"brightness": 100,
|
||||
"name": "Custom",
|
||||
"enable": 0,
|
||||
}
|
||||
with _patch_discovery(device=strip), _patch_connect(device=strip):
|
||||
device = _mocked_device(
|
||||
modules=[Module.Light, Module.LightEffect], alias="my_light"
|
||||
)
|
||||
light = device.modules[Module.Light]
|
||||
light_effect = device.modules[Module.LightEffect]
|
||||
light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
@ -685,8 +761,8 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) ->
|
|||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
strip.turn_on.assert_called_once()
|
||||
strip.turn_on.reset_mock()
|
||||
light.set_state.assert_called_once()
|
||||
light.set_state.reset_mock()
|
||||
|
||||
|
||||
async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None:
|
||||
|
@ -695,13 +771,16 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
strip = _mocked_smart_light_strip()
|
||||
device = _mocked_device(
|
||||
modules=[Module.Light, Module.LightEffect], alias="my_light"
|
||||
)
|
||||
light_effect = device.modules[Module.LightEffect]
|
||||
|
||||
with _patch_discovery(device=strip), _patch_connect(device=strip):
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
@ -715,7 +794,7 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None:
|
|||
},
|
||||
blocking=True,
|
||||
)
|
||||
strip.set_custom_effect.assert_called_once_with(
|
||||
light_effect.set_custom_effect.assert_called_once_with(
|
||||
{
|
||||
"custom": 1,
|
||||
"id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
|
||||
|
@ -733,24 +812,24 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None:
|
|||
"direction": 4,
|
||||
}
|
||||
)
|
||||
strip.set_custom_effect.reset_mock()
|
||||
light_effect.set_custom_effect.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception_type", "msg", "reauth_expected"),
|
||||
[
|
||||
(
|
||||
AuthenticationException,
|
||||
AuthenticationError,
|
||||
"Device authentication error async_turn_on: test error",
|
||||
True,
|
||||
),
|
||||
(
|
||||
TimeoutException,
|
||||
TimeoutError,
|
||||
"Timeout communicating with the device async_turn_on: test error",
|
||||
False,
|
||||
),
|
||||
(
|
||||
SmartDeviceException,
|
||||
KasaException,
|
||||
"Unable to communicate with the device async_turn_on: test error",
|
||||
False,
|
||||
),
|
||||
|
@ -768,14 +847,15 @@ async def test_light_errors_when_turned_on(
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.turn_on.side_effect = exception_type(msg)
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light = device.modules[Module.Light]
|
||||
light.set_state.side_effect = exception_type(msg)
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
entity_id = "light.my_light"
|
||||
|
||||
assert not any(
|
||||
already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})
|
||||
|
@ -786,7 +866,7 @@ async def test_light_errors_when_turned_on(
|
|||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert bulb.turn_on.call_count == 1
|
||||
assert light.set_state.call_count == 1
|
||||
assert (
|
||||
any(
|
||||
flow
|
||||
|
@ -797,3 +877,42 @@ async def test_light_errors_when_turned_on(
|
|||
)
|
||||
== reauth_expected
|
||||
)
|
||||
|
||||
|
||||
async def test_light_child(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test child lights are added to parent device with the right ids."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
|
||||
child_light_1 = _mocked_device(
|
||||
modules=[Module.Light], alias="my_light_0", device_id=f"{DEVICE_ID}00"
|
||||
)
|
||||
child_light_2 = _mocked_device(
|
||||
modules=[Module.Light], alias="my_light_1", device_id=f"{DEVICE_ID}01"
|
||||
)
|
||||
parent_device = _mocked_device(
|
||||
device_id=DEVICE_ID,
|
||||
alias="my_device",
|
||||
children=[child_light_1, child_light_2],
|
||||
modules=[Module.Light],
|
||||
)
|
||||
|
||||
with _patch_discovery(device=parent_device), _patch_connect(device=parent_device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_device"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
|
||||
for light_id in range(2):
|
||||
child_entity_id = f"light.my_device_my_light_{light_id}"
|
||||
child_entity = entity_registry.async_get(child_entity_id)
|
||||
assert child_entity
|
||||
assert child_entity.unique_id == f"{DEVICE_ID}0{light_id}"
|
||||
assert child_entity.device_id == entity.device_id
|
||||
|
|
163
tests/components/tplink/test_number.py
Normal file
163
tests/components/tplink/test_number.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
"""Tests for tplink number platform."""
|
||||
|
||||
from kasa import Feature
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.number import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.components.tplink.entity import EXCLUDED_FEATURES
|
||||
from homeassistant.components.tplink.number import NUMBER_DESCRIPTIONS
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
DEVICE_ID,
|
||||
MAC_ADDRESS,
|
||||
_mocked_device,
|
||||
_mocked_feature,
|
||||
_mocked_strip_children,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
setup_platform_for_device,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
features = {description.key for description in NUMBER_DESCRIPTIONS}
|
||||
features.update(EXCLUDED_FEATURES)
|
||||
device = _mocked_device(alias="my_device", features=features)
|
||||
|
||||
await setup_platform_for_device(hass, mock_config_entry, Platform.NUMBER, device)
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
for excluded in EXCLUDED_FEATURES:
|
||||
assert hass.states.get(f"sensor.my_device_{excluded}") is None
|
||||
|
||||
|
||||
async def test_number(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
new_feature = _mocked_feature(
|
||||
"temperature_offset",
|
||||
value=10,
|
||||
name="Temperature offset",
|
||||
type_=Feature.Type.Number,
|
||||
category=Feature.Category.Config,
|
||||
minimum_value=1,
|
||||
maximum_value=100,
|
||||
)
|
||||
plug = _mocked_device(alias="my_plug", features=[new_feature])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "number.my_plug_temperature_offset"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_temperature_offset"
|
||||
|
||||
|
||||
async def test_number_children(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
new_feature = _mocked_feature(
|
||||
"temperature_offset",
|
||||
value=10,
|
||||
name="Some number",
|
||||
type_=Feature.Type.Number,
|
||||
category=Feature.Category.Config,
|
||||
minimum_value=1,
|
||||
maximum_value=100,
|
||||
)
|
||||
plug = _mocked_device(
|
||||
alias="my_plug",
|
||||
features=[new_feature],
|
||||
children=_mocked_strip_children(features=[new_feature]),
|
||||
)
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "number.my_plug_temperature_offset"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
device = device_registry.async_get(entity.device_id)
|
||||
|
||||
for plug_id in range(2):
|
||||
child_entity_id = f"number.my_plug_plug{plug_id}_temperature_offset"
|
||||
child_entity = entity_registry.async_get(child_entity_id)
|
||||
assert child_entity
|
||||
assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_temperature_offset"
|
||||
assert child_entity.device_id != entity.device_id
|
||||
child_device = device_registry.async_get(child_entity.device_id)
|
||||
assert child_device
|
||||
assert child_device.via_device_id == device.id
|
||||
|
||||
|
||||
async def test_number_set(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test a number entity limits and setting values."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
new_feature = _mocked_feature(
|
||||
"temperature_offset",
|
||||
value=10,
|
||||
name="Some number",
|
||||
type_=Feature.Type.Number,
|
||||
category=Feature.Category.Config,
|
||||
minimum_value=1,
|
||||
maximum_value=200,
|
||||
)
|
||||
plug = _mocked_device(alias="my_plug", features=[new_feature])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "number.my_plug_temperature_offset"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_temperature_offset"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "10"
|
||||
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50},
|
||||
blocking=True,
|
||||
)
|
||||
new_feature.set_value.assert_called_with(50)
|
158
tests/components/tplink/test_select.py
Normal file
158
tests/components/tplink/test_select.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
"""Tests for tplink select platform."""
|
||||
|
||||
from kasa import Feature
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.select import (
|
||||
ATTR_OPTION,
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.components.tplink.entity import EXCLUDED_FEATURES
|
||||
from homeassistant.components.tplink.select import SELECT_DESCRIPTIONS
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
DEVICE_ID,
|
||||
MAC_ADDRESS,
|
||||
_mocked_device,
|
||||
_mocked_feature,
|
||||
_mocked_strip_children,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
setup_platform_for_device,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_feature_select() -> Feature:
|
||||
"""Return mocked tplink binary sensor feature."""
|
||||
return _mocked_feature(
|
||||
"light_preset",
|
||||
value="First choice",
|
||||
name="light_preset",
|
||||
choices=["First choice", "Second choice"],
|
||||
type_=Feature.Type.Choice,
|
||||
category=Feature.Category.Config,
|
||||
)
|
||||
|
||||
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
features = {description.key for description in SELECT_DESCRIPTIONS}
|
||||
features.update(EXCLUDED_FEATURES)
|
||||
device = _mocked_device(alias="my_device", features=features)
|
||||
|
||||
await setup_platform_for_device(hass, mock_config_entry, Platform.SELECT, device)
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
for excluded in EXCLUDED_FEATURES:
|
||||
assert hass.states.get(f"sensor.my_device_{excluded}") is None
|
||||
|
||||
|
||||
async def test_select(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mocked_feature_select: Feature,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
mocked_feature = mocked_feature_select
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
|
||||
plug = _mocked_device(alias="my_plug", features=[mocked_feature])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The entity_id is based on standard name from core.
|
||||
entity_id = "select.my_plug_light_preset"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}"
|
||||
|
||||
|
||||
async def test_select_children(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mocked_feature_select: Feature,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
mocked_feature = mocked_feature_select
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_device(
|
||||
alias="my_plug",
|
||||
features=[mocked_feature],
|
||||
children=_mocked_strip_children(features=[mocked_feature]),
|
||||
)
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "select.my_plug_light_preset"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
device = device_registry.async_get(entity.device_id)
|
||||
|
||||
for plug_id in range(2):
|
||||
child_entity_id = f"select.my_plug_plug{plug_id}_light_preset"
|
||||
child_entity = entity_registry.async_get(child_entity_id)
|
||||
assert child_entity
|
||||
assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}"
|
||||
assert child_entity.device_id != entity.device_id
|
||||
child_device = device_registry.async_get(child_entity.device_id)
|
||||
assert child_device
|
||||
assert child_device.via_device_id == device.id
|
||||
|
||||
|
||||
async def test_select_select(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mocked_feature_select: Feature,
|
||||
) -> None:
|
||||
"""Test a select setting values."""
|
||||
mocked_feature = mocked_feature_select
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_device(alias="my_plug", features=[mocked_feature])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "select.my_plug_light_preset"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_light_preset"
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Second choice"},
|
||||
blocking=True,
|
||||
)
|
||||
mocked_feature.set_value.assert_called_with("Second choice")
|
|
@ -1,35 +1,71 @@
|
|||
"""Tests for light platform."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
from kasa import Device, Feature, Module
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.components.tplink.entity import EXCLUDED_FEATURES
|
||||
from homeassistant.components.tplink.sensor import SENSOR_DESCRIPTIONS
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import MAC_ADDRESS, _mocked_bulb, _mocked_plug, _patch_connect, _patch_discovery
|
||||
from . import (
|
||||
DEVICE_ID,
|
||||
MAC_ADDRESS,
|
||||
_mocked_device,
|
||||
_mocked_energy_features,
|
||||
_mocked_feature,
|
||||
_mocked_strip_children,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
setup_platform_for_device,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
features = {description.key for description in SENSOR_DESCRIPTIONS}
|
||||
features.update(EXCLUDED_FEATURES)
|
||||
device = _mocked_device(alias="my_device", features=features)
|
||||
|
||||
await setup_platform_for_device(hass, mock_config_entry, Platform.SENSOR, device)
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
for excluded in EXCLUDED_FEATURES:
|
||||
assert hass.states.get(f"sensor.my_device_{excluded}") is None
|
||||
|
||||
|
||||
async def test_color_light_with_an_emeter(hass: HomeAssistant) -> None:
|
||||
"""Test a light with an emeter."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.color_temp = None
|
||||
bulb.has_emeter = True
|
||||
bulb.emeter_realtime = Mock(
|
||||
emeter_features = _mocked_energy_features(
|
||||
power=None,
|
||||
total=None,
|
||||
voltage=None,
|
||||
current=5,
|
||||
today=5000.0036,
|
||||
)
|
||||
bulb = _mocked_device(
|
||||
alias="my_bulb", modules=[Module.Light], features=["state", *emeter_features]
|
||||
)
|
||||
bulb.emeter_today = 5000.0036
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
@ -60,16 +96,13 @@ async def test_plug_with_an_emeter(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_plug()
|
||||
plug.color_temp = None
|
||||
plug.has_emeter = True
|
||||
plug.emeter_realtime = Mock(
|
||||
emeter_features = _mocked_energy_features(
|
||||
power=100.06,
|
||||
total=30.0049,
|
||||
voltage=121.19,
|
||||
current=5.035,
|
||||
)
|
||||
plug.emeter_today = None
|
||||
plug = _mocked_device(alias="my_plug", features=["state", *emeter_features])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
@ -95,8 +128,7 @@ async def test_color_light_no_emeter(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.color_temp = None
|
||||
bulb = _mocked_device(alias="my_bulb", modules=[Module.Light])
|
||||
bulb.has_emeter = False
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
|
@ -126,26 +158,175 @@ async def test_sensor_unique_id(
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_plug()
|
||||
plug.color_temp = None
|
||||
plug.has_emeter = True
|
||||
plug.emeter_realtime = Mock(
|
||||
emeter_features = _mocked_energy_features(
|
||||
power=100,
|
||||
total=30,
|
||||
voltage=121,
|
||||
current=5,
|
||||
today=None,
|
||||
)
|
||||
plug.emeter_today = None
|
||||
plug = _mocked_device(alias="my_plug", features=emeter_features)
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
expected = {
|
||||
"sensor.my_plug_current_consumption": "aa:bb:cc:dd:ee:ff_current_power_w",
|
||||
"sensor.my_plug_total_consumption": "aa:bb:cc:dd:ee:ff_total_energy_kwh",
|
||||
"sensor.my_plug_today_s_consumption": "aa:bb:cc:dd:ee:ff_today_energy_kwh",
|
||||
"sensor.my_plug_voltage": "aa:bb:cc:dd:ee:ff_voltage",
|
||||
"sensor.my_plug_current": "aa:bb:cc:dd:ee:ff_current_a",
|
||||
"sensor.my_plug_current_consumption": f"{DEVICE_ID}_current_power_w",
|
||||
"sensor.my_plug_total_consumption": f"{DEVICE_ID}_total_energy_kwh",
|
||||
"sensor.my_plug_today_s_consumption": f"{DEVICE_ID}_today_energy_kwh",
|
||||
"sensor.my_plug_voltage": f"{DEVICE_ID}_voltage",
|
||||
"sensor.my_plug_current": f"{DEVICE_ID}_current_a",
|
||||
}
|
||||
for sensor_entity_id, value in expected.items():
|
||||
assert entity_registry.async_get(sensor_entity_id).unique_id == value
|
||||
|
||||
|
||||
async def test_undefined_sensor(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a message is logged when discovering a feature without a description."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
new_feature = _mocked_feature(
|
||||
"consumption_this_fortnight",
|
||||
value=5.2,
|
||||
name="Consumption for fortnight",
|
||||
type_=Feature.Type.Sensor,
|
||||
category=Feature.Category.Primary,
|
||||
unit="A",
|
||||
precision_hint=2,
|
||||
)
|
||||
plug = _mocked_device(alias="my_plug", features=[new_feature])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
msg = (
|
||||
"Device feature: Consumption for fortnight (consumption_this_fortnight) "
|
||||
"needs an entity description defined in HA"
|
||||
)
|
||||
assert msg in caplog.text
|
||||
|
||||
|
||||
async def test_sensor_children_on_parent(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test a WallSwitch sensor entities are added to parent."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
feature = _mocked_feature(
|
||||
"consumption_this_month",
|
||||
value=5.2,
|
||||
# integration should ignore name and use the value from strings.json:
|
||||
# This month's consumption
|
||||
name="Consumption for month",
|
||||
type_=Feature.Type.Sensor,
|
||||
category=Feature.Category.Primary,
|
||||
unit="A",
|
||||
precision_hint=2,
|
||||
)
|
||||
plug = _mocked_device(
|
||||
alias="my_plug",
|
||||
features=[feature],
|
||||
children=_mocked_strip_children(features=[feature]),
|
||||
device_type=Device.Type.WallSwitch,
|
||||
)
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "sensor.my_plug_this_month_s_consumption"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
device = device_registry.async_get(entity.device_id)
|
||||
|
||||
for plug_id in range(2):
|
||||
child_entity_id = f"sensor.my_plug_plug{plug_id}_this_month_s_consumption"
|
||||
child_entity = entity_registry.async_get(child_entity_id)
|
||||
assert child_entity
|
||||
assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_consumption_this_month"
|
||||
child_device = device_registry.async_get(child_entity.device_id)
|
||||
assert child_device
|
||||
|
||||
assert child_entity.device_id == entity.device_id
|
||||
assert child_device.connections == device.connections
|
||||
|
||||
|
||||
async def test_sensor_children_on_child(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test strip sensors are on child device."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
feature = _mocked_feature(
|
||||
"consumption_this_month",
|
||||
value=5.2,
|
||||
# integration should ignore name and use the value from strings.json:
|
||||
# This month's consumption
|
||||
name="Consumption for month",
|
||||
type_=Feature.Type.Sensor,
|
||||
category=Feature.Category.Primary,
|
||||
unit="A",
|
||||
precision_hint=2,
|
||||
)
|
||||
plug = _mocked_device(
|
||||
alias="my_plug",
|
||||
features=[feature],
|
||||
children=_mocked_strip_children(features=[feature]),
|
||||
device_type=Device.Type.Strip,
|
||||
)
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "sensor.my_plug_this_month_s_consumption"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
device = device_registry.async_get(entity.device_id)
|
||||
|
||||
for plug_id in range(2):
|
||||
child_entity_id = f"sensor.my_plug_plug{plug_id}_this_month_s_consumption"
|
||||
child_entity = entity_registry.async_get(child_entity_id)
|
||||
assert child_entity
|
||||
assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_consumption_this_month"
|
||||
child_device = device_registry.async_get(child_entity.device_id)
|
||||
assert child_device
|
||||
|
||||
assert child_entity.device_id != entity.device_id
|
||||
assert child_device.via_device_id == device.id
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
async def test_new_datetime_sensor(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
# Skipped temporarily while datetime handling on hold.
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_device(alias="my_plug", features=["on_since"])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "sensor.my_plug_on_since"
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == f"{DEVICE_ID}_on_since"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["device_class"] == "timestamp"
|
||||
|
|
|
@ -3,12 +3,16 @@
|
|||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from kasa import AuthenticationException, SmartDeviceException, TimeoutException
|
||||
from kasa import AuthenticationError, Device, KasaException, Module, TimeoutError
|
||||
from kasa.iot import IotStrip
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.components.tplink.entity import EXCLUDED_FEATURES
|
||||
from homeassistant.components.tplink.switch import SWITCH_DESCRIPTIONS
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
|
@ -16,32 +20,57 @@ from homeassistant.const import (
|
|||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from . import (
|
||||
DEVICE_ID,
|
||||
MAC_ADDRESS,
|
||||
_mocked_dimmer,
|
||||
_mocked_plug,
|
||||
_mocked_strip,
|
||||
_mocked_device,
|
||||
_mocked_strip_children,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
setup_platform_for_device,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test a sensor unique ids."""
|
||||
features = {description.key for description in SWITCH_DESCRIPTIONS}
|
||||
features.update(EXCLUDED_FEATURES)
|
||||
device = _mocked_device(alias="my_device", features=features)
|
||||
|
||||
await setup_platform_for_device(hass, mock_config_entry, Platform.SWITCH, device)
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
for excluded in EXCLUDED_FEATURES:
|
||||
assert hass.states.get(f"sensor.my_device_{excluded}") is None
|
||||
|
||||
|
||||
async def test_plug(hass: HomeAssistant) -> None:
|
||||
"""Test a smart plug."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_plug()
|
||||
plug = _mocked_device(alias="my_plug", features=["state"])
|
||||
feat = plug.features["state"]
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
@ -53,29 +82,42 @@ async def test_plug(hass: HomeAssistant) -> None:
|
|||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
plug.turn_off.assert_called_once()
|
||||
plug.turn_off.reset_mock()
|
||||
feat.set_value.assert_called_once()
|
||||
feat.set_value.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
plug.turn_on.assert_called_once()
|
||||
plug.turn_on.reset_mock()
|
||||
feat.set_value.assert_called_once()
|
||||
feat.set_value.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("dev", "domain"),
|
||||
[
|
||||
(_mocked_plug(), "switch"),
|
||||
(_mocked_strip(), "switch"),
|
||||
(_mocked_dimmer(), "light"),
|
||||
(_mocked_device(alias="my_plug", features=["state", "led"]), "switch"),
|
||||
(
|
||||
_mocked_device(
|
||||
alias="my_strip",
|
||||
features=["state", "led"],
|
||||
children=_mocked_strip_children(),
|
||||
),
|
||||
"switch",
|
||||
),
|
||||
(
|
||||
_mocked_device(
|
||||
alias="my_light", modules=[Module.Light], features=["state", "led"]
|
||||
),
|
||||
"light",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None:
|
||||
async def test_led_switch(hass: HomeAssistant, dev: Device, domain: str) -> None:
|
||||
"""Test LED setting for plugs, strips and dimmers."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
feat = dev.features["led"]
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
with _patch_discovery(device=dev), _patch_connect(device=dev):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
|
@ -91,14 +133,14 @@ async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None:
|
|||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: led_entity_id}, blocking=True
|
||||
)
|
||||
dev.set_led.assert_called_once_with(False)
|
||||
dev.set_led.reset_mock()
|
||||
feat.set_value.assert_called_once_with(False)
|
||||
feat.set_value.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: led_entity_id}, blocking=True
|
||||
)
|
||||
dev.set_led.assert_called_once_with(True)
|
||||
dev.set_led.reset_mock()
|
||||
feat.set_value.assert_called_once_with(True)
|
||||
feat.set_value.reset_mock()
|
||||
|
||||
|
||||
async def test_plug_unique_id(
|
||||
|
@ -109,13 +151,13 @@ async def test_plug_unique_id(
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_plug()
|
||||
plug = _mocked_device(alias="my_plug", features=["state", "led"])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "switch.my_plug"
|
||||
assert entity_registry.async_get(entity_id).unique_id == "aa:bb:cc:dd:ee:ff"
|
||||
assert entity_registry.async_get(entity_id).unique_id == DEVICE_ID
|
||||
|
||||
|
||||
async def test_plug_update_fails(hass: HomeAssistant) -> None:
|
||||
|
@ -124,7 +166,7 @@ async def test_plug_update_fails(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_plug()
|
||||
plug = _mocked_device(alias="my_plug", features=["state", "led"])
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
@ -132,7 +174,7 @@ async def test_plug_update_fails(hass: HomeAssistant) -> None:
|
|||
entity_id = "switch.my_plug"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
plug.update = AsyncMock(side_effect=SmartDeviceException)
|
||||
plug.update = AsyncMock(side_effect=KasaException)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
|
||||
await hass.async_block_till_done()
|
||||
|
@ -146,15 +188,18 @@ async def test_strip(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
strip = _mocked_strip()
|
||||
strip = _mocked_device(
|
||||
alias="my_strip",
|
||||
children=_mocked_strip_children(features=["state"]),
|
||||
features=["state", "led"],
|
||||
spec=IotStrip,
|
||||
)
|
||||
strip.children[0].features["state"].value = True
|
||||
strip.children[1].features["state"].value = False
|
||||
with _patch_discovery(device=strip), _patch_connect(device=strip):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify we only create entities for the children
|
||||
# since this is what the previous version did
|
||||
assert hass.states.get("switch.my_strip") is None
|
||||
|
||||
entity_id = "switch.my_strip_plug0"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
@ -162,14 +207,15 @@ async def test_strip(hass: HomeAssistant) -> None:
|
|||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
strip.children[0].turn_off.assert_called_once()
|
||||
strip.children[0].turn_off.reset_mock()
|
||||
feat = strip.children[0].features["state"]
|
||||
feat.set_value.assert_called_once()
|
||||
feat.set_value.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
strip.children[0].turn_on.assert_called_once()
|
||||
strip.children[0].turn_on.reset_mock()
|
||||
feat.set_value.assert_called_once()
|
||||
feat.set_value.reset_mock()
|
||||
|
||||
entity_id = "switch.my_strip_plug1"
|
||||
state = hass.states.get(entity_id)
|
||||
|
@ -178,14 +224,15 @@ async def test_strip(hass: HomeAssistant) -> None:
|
|||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
strip.children[1].turn_off.assert_called_once()
|
||||
strip.children[1].turn_off.reset_mock()
|
||||
feat = strip.children[1].features["state"]
|
||||
feat.set_value.assert_called_once()
|
||||
feat.set_value.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
strip.children[1].turn_on.assert_called_once()
|
||||
strip.children[1].turn_on.reset_mock()
|
||||
feat.set_value.assert_called_once()
|
||||
feat.set_value.reset_mock()
|
||||
|
||||
|
||||
async def test_strip_unique_ids(
|
||||
|
@ -196,7 +243,11 @@ async def test_strip_unique_ids(
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
strip = _mocked_strip()
|
||||
strip = _mocked_device(
|
||||
alias="my_strip",
|
||||
children=_mocked_strip_children(features=["state"]),
|
||||
features=["state", "led"],
|
||||
)
|
||||
with _patch_discovery(device=strip), _patch_connect(device=strip):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
@ -208,21 +259,45 @@ async def test_strip_unique_ids(
|
|||
)
|
||||
|
||||
|
||||
async def test_strip_blank_alias(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test a strip unique id."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
strip = _mocked_device(
|
||||
alias="",
|
||||
model="KS123",
|
||||
children=_mocked_strip_children(features=["state", "led"], alias=""),
|
||||
features=["state", "led"],
|
||||
)
|
||||
with _patch_discovery(device=strip), _patch_connect(device=strip):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for plug_id in range(2):
|
||||
entity_id = f"switch.unnamed_ks123_stripsocket_{plug_id + 1}"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.name == f"Unnamed KS123 Stripsocket {plug_id + 1}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception_type", "msg", "reauth_expected"),
|
||||
[
|
||||
(
|
||||
AuthenticationException,
|
||||
AuthenticationError,
|
||||
"Device authentication error async_turn_on: test error",
|
||||
True,
|
||||
),
|
||||
(
|
||||
TimeoutException,
|
||||
TimeoutError,
|
||||
"Timeout communicating with the device async_turn_on: test error",
|
||||
False,
|
||||
),
|
||||
(
|
||||
SmartDeviceException,
|
||||
KasaException,
|
||||
"Unable to communicate with the device async_turn_on: test error",
|
||||
False,
|
||||
),
|
||||
|
@ -240,8 +315,9 @@ async def test_plug_errors_when_turned_on(
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
plug = _mocked_plug()
|
||||
plug.turn_on.side_effect = exception_type("test error")
|
||||
plug = _mocked_device(alias="my_plug", features=["state", "led"])
|
||||
feat = plug.features["state"]
|
||||
feat.set_value.side_effect = exception_type("test error")
|
||||
|
||||
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
|
@ -258,7 +334,7 @@ async def test_plug_errors_when_turned_on(
|
|||
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert plug.turn_on.call_count == 1
|
||||
assert feat.set_value.call_count == 1
|
||||
assert (
|
||||
any(
|
||||
flow
|
||||
|
|
Loading…
Add table
Reference in a new issue