mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 16:17:20 +00:00
Merge pull request #57294 from home-assistant/rc
This commit is contained in:
commit
5bb4bc3d13
@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20211006.0"
|
||||
"home-assistant-frontend==20211007.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "HomeKit",
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit",
|
||||
"requirements": [
|
||||
"HAP-python==4.2.1",
|
||||
"HAP-python==4.3.0",
|
||||
"fnvhash==0.1.0",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
@ -81,6 +81,30 @@ def has_date_or_time(conf):
|
||||
raise vol.Invalid("Entity needs at least a date or a time")
|
||||
|
||||
|
||||
def valid_initial(conf):
|
||||
"""Check the initial value is valid."""
|
||||
initial = conf.get(CONF_INITIAL)
|
||||
if not initial:
|
||||
return conf
|
||||
|
||||
if conf[CONF_HAS_DATE] and conf[CONF_HAS_TIME]:
|
||||
parsed_value = dt_util.parse_datetime(initial)
|
||||
if parsed_value is not None:
|
||||
return conf
|
||||
raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a datetime")
|
||||
|
||||
if conf[CONF_HAS_DATE]:
|
||||
parsed_value = dt_util.parse_date(initial)
|
||||
if parsed_value is not None:
|
||||
return conf
|
||||
raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a date")
|
||||
|
||||
parsed_value = dt_util.parse_time(initial)
|
||||
if parsed_value is not None:
|
||||
return conf
|
||||
raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a time")
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: cv.schema_with_slug_keys(
|
||||
@ -93,6 +117,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
},
|
||||
has_date_or_time,
|
||||
valid_initial,
|
||||
)
|
||||
)
|
||||
},
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "mill",
|
||||
"name": "Mill",
|
||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||
"requirements": ["millheater==0.6.0"],
|
||||
"requirements": ["millheater==0.6.1"],
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
|
@ -4,6 +4,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
||||
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
|
||||
SOURCE_TYPE_ROUTER,
|
||||
)
|
||||
@ -50,7 +51,7 @@ async def async_get_scanner(hass, config):
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
data=config[DEVICE_TRACKER_DOMAIN],
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -91,11 +91,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry_id = entry.entry_id
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN].setdefault(entry_id, {})
|
||||
http_session = requests.Session()
|
||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||
|
||||
password = entry.data.get(CONF_PASSWORD)
|
||||
power_wall = Powerwall(entry.data[CONF_IP_ADDRESS], http_session=http_session)
|
||||
power_wall = Powerwall(ip_address, http_session=http_session)
|
||||
try:
|
||||
powerwall_data = await hass.async_add_executor_job(
|
||||
_login_and_fetch_base_info, power_wall, password
|
||||
@ -115,13 +115,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await _migrate_old_unique_ids(hass, entry_id, powerwall_data)
|
||||
login_failed_count = 0
|
||||
|
||||
runtime_data = hass.data[DOMAIN][entry.entry_id] = {
|
||||
POWERWALL_API_CHANGED: False,
|
||||
POWERWALL_HTTP_SESSION: http_session,
|
||||
}
|
||||
|
||||
def _recreate_powerwall_login():
|
||||
nonlocal http_session
|
||||
nonlocal power_wall
|
||||
http_session.close()
|
||||
http_session = requests.Session()
|
||||
power_wall = Powerwall(ip_address, http_session=http_session)
|
||||
runtime_data[POWERWALL_OBJECT] = power_wall
|
||||
runtime_data[POWERWALL_HTTP_SESSION] = http_session
|
||||
power_wall.login("", password)
|
||||
|
||||
async def async_update_data():
|
||||
"""Fetch data from API endpoint."""
|
||||
# Check if we had an error before
|
||||
nonlocal login_failed_count
|
||||
_LOGGER.debug("Checking if update failed")
|
||||
if hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]:
|
||||
return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data
|
||||
if runtime_data[POWERWALL_API_CHANGED]:
|
||||
return runtime_data[POWERWALL_COORDINATOR].data
|
||||
|
||||
_LOGGER.debug("Updating data")
|
||||
try:
|
||||
@ -130,9 +145,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if password is None:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
# If the session expired, relogin, and try again
|
||||
# If the session expired, recreate, relogin, and try again
|
||||
try:
|
||||
await hass.async_add_executor_job(power_wall.login, "", password)
|
||||
await hass.async_add_executor_job(_recreate_powerwall_login)
|
||||
return await _async_update_powerwall_data(hass, entry, power_wall)
|
||||
except AccessDeniedError as ex:
|
||||
login_failed_count += 1
|
||||
@ -153,13 +168,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = powerwall_data
|
||||
hass.data[DOMAIN][entry.entry_id].update(
|
||||
runtime_data.update(
|
||||
{
|
||||
**powerwall_data,
|
||||
POWERWALL_OBJECT: power_wall,
|
||||
POWERWALL_COORDINATOR: coordinator,
|
||||
POWERWALL_HTTP_SESSION: http_session,
|
||||
POWERWALL_API_CHANGED: False,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -13,6 +13,7 @@ from sqlalchemy import bindparam, func
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext import baked
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
from sqlalchemy.sql.expression import true
|
||||
|
||||
from homeassistant.const import (
|
||||
PRESSURE_PA,
|
||||
@ -396,9 +397,9 @@ def get_metadata_with_session(
|
||||
StatisticsMeta.statistic_id.in_(bindparam("statistic_ids"))
|
||||
)
|
||||
if statistic_type == "mean":
|
||||
baked_query += lambda q: q.filter(StatisticsMeta.has_mean.isnot(False))
|
||||
baked_query += lambda q: q.filter(StatisticsMeta.has_mean == true())
|
||||
elif statistic_type == "sum":
|
||||
baked_query += lambda q: q.filter(StatisticsMeta.has_sum.isnot(False))
|
||||
baked_query += lambda q: q.filter(StatisticsMeta.has_sum == true())
|
||||
result = execute(baked_query(session).params(statistic_ids=statistic_ids))
|
||||
if not result:
|
||||
return {}
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from kasa import SmartDevice, SmartDeviceException
|
||||
@ -11,9 +12,15 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import network
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@ -32,6 +39,8 @@ from .migration import (
|
||||
async_migrate_yaml_entries,
|
||||
)
|
||||
|
||||
DISCOVERY_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@ -118,6 +127,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if discovered_devices:
|
||||
async_trigger_discovery(hass, discovered_devices)
|
||||
|
||||
async def _async_discovery(*_: Any) -> None:
|
||||
if discovered := await async_discover_devices(hass):
|
||||
async_trigger_discovery(hass, discovered)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_discovery)
|
||||
async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -63,10 +63,20 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
@async_refresh_after
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
transition = kwargs.get(ATTR_TRANSITION)
|
||||
if (transition := kwargs.get(ATTR_TRANSITION)) is not None:
|
||||
transition = int(transition * 1_000)
|
||||
|
||||
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.
|
||||
# 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.
|
||||
transition = 1
|
||||
|
||||
# Handle turning to temp mode
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
|
||||
@ -92,7 +102,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
@async_refresh_after
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
await self.device.turn_off(transition=kwargs.get(ATTR_TRANSITION))
|
||||
if (transition := kwargs.get(ATTR_TRANSITION)) is not None:
|
||||
transition = int(transition * 1_000)
|
||||
await self.device.turn_off(transition=transition)
|
||||
|
||||
@property
|
||||
def min_mireds(self) -> int:
|
||||
@ -145,7 +157,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
def color_mode(self) -> str | None:
|
||||
"""Return the active color mode."""
|
||||
if self.device.is_color:
|
||||
if self.device.color_temp:
|
||||
if self.device.is_variable_color_temp and self.device.color_temp:
|
||||
return COLOR_MODE_COLOR_TEMP
|
||||
return COLOR_MODE_HS
|
||||
if self.device.is_variable_color_temp:
|
||||
|
@ -146,9 +146,13 @@ class XiaomiAirHumidifierSelector(XiaomiSelector):
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
"""Fetch state from the device."""
|
||||
self._current_led_brightness = self._extract_value_from_attribute(
|
||||
led_brightness = self._extract_value_from_attribute(
|
||||
self.coordinator.data, self.entity_description.key
|
||||
)
|
||||
# Sometimes (quite rarely) the device returns None as the LED brightness so we
|
||||
# check that the value is not None before updating the state.
|
||||
if led_brightness:
|
||||
self._current_led_brightness = led_brightness
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "yeelight",
|
||||
"name": "Yeelight",
|
||||
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
||||
"requirements": ["yeelight==0.7.6", "async-upnp-client==0.22.5"],
|
||||
"requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.5"],
|
||||
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
|
@ -408,7 +408,7 @@ class ZWaveServices:
|
||||
async def async_set_value(self, service: ServiceCall) -> None:
|
||||
"""Set a value on a node."""
|
||||
# pylint: disable=no-self-use
|
||||
nodes = service.data[const.ATTR_NODES]
|
||||
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
|
||||
command_class = service.data[const.ATTR_COMMAND_CLASS]
|
||||
property_ = service.data[const.ATTR_PROPERTY]
|
||||
property_key = service.data.get(const.ATTR_PROPERTY_KEY)
|
||||
@ -418,15 +418,27 @@ class ZWaveServices:
|
||||
options = service.data.get(const.ATTR_OPTIONS)
|
||||
|
||||
for node in nodes:
|
||||
success = await node.async_set_value(
|
||||
get_value_id(
|
||||
value_id = get_value_id(
|
||||
node,
|
||||
command_class,
|
||||
property_,
|
||||
endpoint=endpoint,
|
||||
property_key=property_key,
|
||||
),
|
||||
new_value,
|
||||
)
|
||||
# If value has a string type but the new value is not a string, we need to
|
||||
# convert it to one. We use new variable `new_value_` to convert the data
|
||||
# so we can preserve the original `new_value` for every node.
|
||||
if (
|
||||
value_id in node.values
|
||||
and node.values[value_id].metadata.type == "string"
|
||||
and not isinstance(new_value, str)
|
||||
):
|
||||
new_value_ = str(new_value)
|
||||
else:
|
||||
new_value_ = new_value
|
||||
success = await node.async_set_value(
|
||||
value_id,
|
||||
new_value_,
|
||||
options=options,
|
||||
wait_for_result=wait_for_result,
|
||||
)
|
||||
@ -452,11 +464,16 @@ class ZWaveServices:
|
||||
await self.async_set_value(service)
|
||||
return
|
||||
|
||||
command_class = service.data[const.ATTR_COMMAND_CLASS]
|
||||
property_ = service.data[const.ATTR_PROPERTY]
|
||||
property_key = service.data.get(const.ATTR_PROPERTY_KEY)
|
||||
endpoint = service.data.get(const.ATTR_ENDPOINT)
|
||||
|
||||
value = {
|
||||
"commandClass": service.data[const.ATTR_COMMAND_CLASS],
|
||||
"property": service.data[const.ATTR_PROPERTY],
|
||||
"propertyKey": service.data.get(const.ATTR_PROPERTY_KEY),
|
||||
"endpoint": service.data.get(const.ATTR_ENDPOINT),
|
||||
"commandClass": command_class,
|
||||
"property": property_,
|
||||
"propertyKey": property_key,
|
||||
"endpoint": endpoint,
|
||||
}
|
||||
new_value = service.data[const.ATTR_VALUE]
|
||||
|
||||
@ -464,12 +481,30 @@ class ZWaveServices:
|
||||
# schema validation and can use that to get the client, otherwise we can just
|
||||
# get the client from the node.
|
||||
client: ZwaveClient = None
|
||||
first_node = next((node for node in nodes), None)
|
||||
first_node: ZwaveNode = next((node for node in nodes), None)
|
||||
if first_node:
|
||||
client = first_node.client
|
||||
else:
|
||||
entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id
|
||||
client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT]
|
||||
first_node = next(
|
||||
node
|
||||
for node in client.driver.controller.nodes.values()
|
||||
if get_value_id(node, command_class, property_, endpoint, property_key)
|
||||
in node.values
|
||||
)
|
||||
|
||||
# If value has a string type but the new value is not a string, we need to
|
||||
# convert it to one
|
||||
value_id = get_value_id(
|
||||
first_node, command_class, property_, endpoint, property_key
|
||||
)
|
||||
if (
|
||||
value_id in first_node.values
|
||||
and first_node.values[value_id].metadata.type == "string"
|
||||
and not isinstance(new_value, str)
|
||||
):
|
||||
new_value = str(new_value)
|
||||
|
||||
success = await async_multicast_set_value(
|
||||
client=client,
|
||||
|
@ -5,7 +5,7 @@ from typing import Final
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 10
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||
|
@ -15,7 +15,7 @@ ciso8601==2.2.0
|
||||
cryptography==3.4.8
|
||||
emoji==1.5.0
|
||||
hass-nabucasa==0.50.0
|
||||
home-assistant-frontend==20211006.0
|
||||
home-assistant-frontend==20211007.0
|
||||
httpx==0.19.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.1
|
||||
|
@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2
|
||||
# Adafruit_BBIO==1.1.1
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.2.1
|
||||
HAP-python==4.3.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==1.5.1
|
||||
@ -810,7 +810,7 @@ hole==0.5.1
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211006.0
|
||||
home-assistant-frontend==20211007.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -1005,7 +1005,7 @@ micloud==0.3
|
||||
miflora==0.7.0
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.6.0
|
||||
millheater==0.6.1
|
||||
|
||||
# homeassistant.components.minio
|
||||
minio==4.0.9
|
||||
@ -2459,7 +2459,7 @@ yalesmartalarmclient==0.3.4
|
||||
yalexs==1.1.13
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.6
|
||||
yeelight==0.7.7
|
||||
|
||||
# homeassistant.components.yeelightsunflower
|
||||
yeelightsunflower==0.0.10
|
||||
|
@ -7,7 +7,7 @@
|
||||
AEMET-OpenData==0.2.1
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.2.1
|
||||
HAP-python==4.3.0
|
||||
|
||||
# homeassistant.components.flick_electric
|
||||
PyFlick==0.0.2
|
||||
@ -485,7 +485,7 @@ hole==0.5.1
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211006.0
|
||||
home-assistant-frontend==20211007.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -585,7 +585,7 @@ mficlient==0.3.0
|
||||
micloud==0.3
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.6.0
|
||||
millheater==0.6.1
|
||||
|
||||
# homeassistant.components.minio
|
||||
minio==4.0.9
|
||||
@ -1403,7 +1403,7 @@ yalesmartalarmclient==0.3.4
|
||||
yalexs==1.1.13
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.6
|
||||
yeelight==0.7.7
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==0.13
|
||||
|
@ -297,7 +297,7 @@ async def test_fan_speed(hass, hk_driver, events):
|
||||
)
|
||||
await hass.async_add_executor_job(acc.char_speed.client_update_value, 42)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_speed.value == 42
|
||||
assert acc.char_speed.value == 50
|
||||
assert acc.char_active.value == 1
|
||||
|
||||
assert call_set_percentage[0]
|
||||
@ -309,7 +309,7 @@ async def test_fan_speed(hass, hk_driver, events):
|
||||
# Verify speed is preserved from off to on
|
||||
hass.states.async_set(entity_id, STATE_OFF, {ATTR_PERCENTAGE: 42})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_speed.value == 42
|
||||
assert acc.char_speed.value == 50
|
||||
assert acc.char_active.value == 0
|
||||
|
||||
hk_driver.set_characteristics(
|
||||
@ -325,7 +325,7 @@ async def test_fan_speed(hass, hk_driver, events):
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_speed.value == 42
|
||||
assert acc.char_speed.value == 50
|
||||
assert acc.char_active.value == 1
|
||||
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Test different accessory types: Media Players."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homekit.const import (
|
||||
ATTR_KEY_NAME,
|
||||
ATTR_VALUE,
|
||||
@ -353,6 +355,7 @@ async def test_media_player_television(hass, hk_driver, events, caplog):
|
||||
|
||||
hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Test different accessory types: Remotes."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homekit.const import (
|
||||
ATTR_KEY_NAME,
|
||||
ATTR_VALUE,
|
||||
@ -140,6 +142,7 @@ async def test_activity_remote(hass, hk_driver, events, caplog):
|
||||
|
||||
hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
acc.char_remote_key.client_update_value(20)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -1746,11 +1746,7 @@ async def test_water_heater(hass, hk_driver, events):
|
||||
assert len(events) == 1
|
||||
assert events[-1].data[ATTR_VALUE] == f"52.0{TEMP_CELSIUS}"
|
||||
|
||||
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 0)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_target_heat_cool.value == 1
|
||||
|
||||
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 2)
|
||||
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 1)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_target_heat_cool.value == 1
|
||||
|
||||
|
@ -744,3 +744,30 @@ async def test_timestamp(hass):
|
||||
|
||||
finally:
|
||||
dt_util.set_default_time_zone(ORIG_TIMEZONE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config, error",
|
||||
[
|
||||
(
|
||||
{"has_time": True, "has_date": True, "initial": "abc"},
|
||||
"'abc' can't be parsed as a datetime",
|
||||
),
|
||||
(
|
||||
{"has_time": False, "has_date": True, "initial": "abc"},
|
||||
"'abc' can't be parsed as a date",
|
||||
),
|
||||
(
|
||||
{"has_time": True, "has_date": False, "initial": "abc"},
|
||||
"'abc' can't be parsed as a time",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_invalid_initial(hass, caplog, config, error):
|
||||
"""Test configuration is rejected if the initial value is invalid."""
|
||||
assert not await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{DOMAIN: {"test_date": config}},
|
||||
)
|
||||
assert error in caplog.text
|
||||
|
@ -33,6 +33,7 @@ def _mocked_bulb() -> SmartBulb:
|
||||
bulb.is_color = True
|
||||
bulb.is_strip = False
|
||||
bulb.is_plug = False
|
||||
bulb.is_dimmer = False
|
||||
bulb.hsv = (10, 30, 5)
|
||||
bulb.device_id = MAC_ADDRESS
|
||||
bulb.valid_temperature_range.min = 4000
|
||||
|
@ -1,27 +1,41 @@
|
||||
"""Tests for the TP-Link component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_single_discovery
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_configuring_tplink_causes_discovery(hass):
|
||||
"""Test that specifying empty config does discovery."""
|
||||
with patch("homeassistant.components.tplink.Discover.discover") as discover:
|
||||
discover.return_value = {"host": 1234}
|
||||
discover.return_value = {MagicMock(): MagicMock()}
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
call_count = len(discover.mock_calls)
|
||||
assert discover.mock_calls
|
||||
|
||||
assert len(discover.mock_calls) == 1
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(discover.mock_calls) == call_count * 2
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15))
|
||||
await hass.async_block_till_done()
|
||||
assert len(discover.mock_calls) == call_count * 3
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30))
|
||||
await hass.async_block_till_done()
|
||||
assert len(discover.mock_calls) == call_count * 4
|
||||
|
||||
|
||||
async def test_config_entry_reload(hass):
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Tests for light platform."""
|
||||
|
||||
from typing import Optional
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import tplink
|
||||
@ -12,6 +15,7 @@ from homeassistant.components.light import (
|
||||
ATTR_MIN_MIREDS,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ATTR_TRANSITION,
|
||||
ATTR_XY_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
)
|
||||
@ -43,8 +47,9 @@ async def test_light_unique_id(hass: HomeAssistant) -> None:
|
||||
assert entity_registry.async_get(entity_id).unique_id == "AABBCCDDEEFF"
|
||||
|
||||
|
||||
async def test_color_light(hass: HomeAssistant) -> None:
|
||||
"""Test a light."""
|
||||
@pytest.mark.parametrize("transition", [2.0, None])
|
||||
async def test_color_light(hass: HomeAssistant, transition: Optional[float]) -> None:
|
||||
"""Test a color light and that all transitions are correctly passed."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
@ -56,6 +61,11 @@ async def test_color_light(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
KASA_TRANSITION_VALUE = transition * 1_000 if transition is not None else None
|
||||
|
||||
BASE_PAYLOAD = {ATTR_ENTITY_ID: entity_id}
|
||||
if transition:
|
||||
BASE_PAYLOAD[ATTR_TRANSITION] = transition
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
@ -69,6 +79,81 @@ async def test_color_light(hass: HomeAssistant) -> None:
|
||||
assert attributes[ATTR_RGB_COLOR] == (255, 191, 178)
|
||||
assert attributes[ATTR_XY_COLOR] == (0.42, 0.336)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True
|
||||
)
|
||||
bulb.turn_off.assert_called_once_with(transition=KASA_TRANSITION_VALUE)
|
||||
|
||||
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()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE)
|
||||
bulb.set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{**BASE_PAYLOAD, ATTR_COLOR_TEMP: 150},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(
|
||||
6666, brightness=None, transition=KASA_TRANSITION_VALUE
|
||||
)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{**BASE_PAYLOAD, ATTR_COLOR_TEMP: 150},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(
|
||||
6666, brightness=None, transition=KASA_TRANSITION_VALUE
|
||||
)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{**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()
|
||||
|
||||
|
||||
async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
||||
"""Test a light."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, 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_single_discovery(device=bulb):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert attributes[ATTR_COLOR_MODE] == "hs"
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness", "hs"]
|
||||
assert attributes[ATTR_HS_COLOR] == (10, 30)
|
||||
assert attributes[ATTR_RGB_COLOR] == (255, 191, 178)
|
||||
assert attributes[ATTR_XY_COLOR] == (0.42, 0.336)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
@ -89,24 +174,6 @@ async def test_color_light(hass: HomeAssistant) -> None:
|
||||
bulb.set_brightness.assert_called_with(39, transition=None)
|
||||
bulb.set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
@ -282,3 +349,29 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None:
|
||||
assert state.state == "off"
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"]
|
||||
|
||||
|
||||
async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None:
|
||||
"""Test a light."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.is_dimmer = True
|
||||
bulb.is_on = False
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_single_discovery(device=bulb):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "off"
|
||||
|
||||
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()
|
||||
|
@ -43,6 +43,7 @@ from .common import (
|
||||
CLIMATE_DANFOSS_LC13_ENTITY,
|
||||
CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY,
|
||||
CLIMATE_RADIO_THERMOSTAT_ENTITY,
|
||||
SCHLAGE_BE469_LOCK_ENTITY,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@ -1021,6 +1022,51 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration):
|
||||
)
|
||||
|
||||
|
||||
async def test_set_value_string(
|
||||
hass, client, climate_danfoss_lc_13, lock_schlage_be469, integration
|
||||
):
|
||||
"""Test set_value service converts number to string when needed."""
|
||||
client.async_send_command.return_value = {"success": True}
|
||||
|
||||
# Test that number gets converted to a string when needed
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY,
|
||||
ATTR_COMMAND_CLASS: 99,
|
||||
ATTR_PROPERTY: "userCode",
|
||||
ATTR_PROPERTY_KEY: 1,
|
||||
ATTR_VALUE: 12345,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "node.set_value"
|
||||
assert args["nodeId"] == lock_schlage_be469.node_id
|
||||
assert args["valueId"] == {
|
||||
"commandClassName": "User Code",
|
||||
"commandClass": 99,
|
||||
"endpoint": 0,
|
||||
"property": "userCode",
|
||||
"propertyName": "userCode",
|
||||
"propertyKey": 1,
|
||||
"propertyKeyName": "1",
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"readable": True,
|
||||
"writeable": True,
|
||||
"minLength": 4,
|
||||
"maxLength": 10,
|
||||
"label": "User Code (1)",
|
||||
},
|
||||
"value": "**********",
|
||||
}
|
||||
assert args["value"] == "12345"
|
||||
|
||||
|
||||
async def test_set_value_options(hass, client, aeon_smart_switch_6, integration):
|
||||
"""Test set_value service with options."""
|
||||
await hass.services.async_call(
|
||||
@ -1381,6 +1427,41 @@ async def test_multicast_set_value_options(
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
|
||||
async def test_multicast_set_value_string(
|
||||
hass,
|
||||
client,
|
||||
lock_id_lock_as_id150,
|
||||
lock_schlage_be469,
|
||||
integration,
|
||||
):
|
||||
"""Test multicast_set_value service converts number to string when needed."""
|
||||
client.async_send_command.return_value = {"success": True}
|
||||
|
||||
# Test that number gets converted to a string when needed
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_MULTICAST_SET_VALUE,
|
||||
{
|
||||
ATTR_BROADCAST: True,
|
||||
ATTR_COMMAND_CLASS: 99,
|
||||
ATTR_PROPERTY: "userCode",
|
||||
ATTR_PROPERTY_KEY: 1,
|
||||
ATTR_VALUE: 12345,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "broadcast_node.set_value"
|
||||
assert args["valueId"] == {
|
||||
"commandClass": 99,
|
||||
"property": "userCode",
|
||||
"propertyKey": 1,
|
||||
}
|
||||
assert args["value"] == "12345"
|
||||
|
||||
|
||||
async def test_ping(
|
||||
hass,
|
||||
client,
|
||||
|
Loading…
x
Reference in New Issue
Block a user