Merge pull request #57294 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-10-07 20:57:08 -07:00 committed by GitHub
commit 5bb4bc3d13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 409 additions and 84 deletions

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend", "name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [ "requirements": [
"home-assistant-frontend==20211006.0" "home-assistant-frontend==20211007.0"
], ],
"dependencies": [ "dependencies": [
"api", "api",

View File

@ -3,7 +3,7 @@
"name": "HomeKit", "name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit", "documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": [ "requirements": [
"HAP-python==4.2.1", "HAP-python==4.3.0",
"fnvhash==0.1.0", "fnvhash==0.1.0",
"PyQRCode==1.2.1", "PyQRCode==1.2.1",
"base36==0.1.1" "base36==0.1.1"

View File

@ -81,6 +81,30 @@ def has_date_or_time(conf):
raise vol.Invalid("Entity needs at least a date or a time") 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( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: cv.schema_with_slug_keys( DOMAIN: cv.schema_with_slug_keys(
@ -93,6 +117,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_INITIAL): cv.string, vol.Optional(CONF_INITIAL): cv.string,
}, },
has_date_or_time, has_date_or_time,
valid_initial,
) )
) )
}, },

View File

@ -2,7 +2,7 @@
"domain": "mill", "domain": "mill",
"name": "Mill", "name": "Mill",
"documentation": "https://www.home-assistant.io/integrations/mill", "documentation": "https://www.home-assistant.io/integrations/mill",
"requirements": ["millheater==0.6.0"], "requirements": ["millheater==0.6.1"],
"codeowners": ["@danielhiversen"], "codeowners": ["@danielhiversen"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"

View File

@ -4,6 +4,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN as DEVICE_TRACKER_DOMAIN,
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
SOURCE_TYPE_ROUTER, SOURCE_TYPE_ROUTER,
) )
@ -50,7 +51,7 @@ async def async_get_scanner(hass, config):
hass.config_entries.flow.async_init( hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": SOURCE_IMPORT}, context={"source": SOURCE_IMPORT},
data=config, data=config[DEVICE_TRACKER_DOMAIN],
) )
) )

View File

@ -91,11 +91,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry_id = entry.entry_id entry_id = entry.entry_id
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(entry_id, {})
http_session = requests.Session() http_session = requests.Session()
ip_address = entry.data[CONF_IP_ADDRESS]
password = entry.data.get(CONF_PASSWORD) 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: try:
powerwall_data = await hass.async_add_executor_job( powerwall_data = await hass.async_add_executor_job(
_login_and_fetch_base_info, power_wall, password _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) await _migrate_old_unique_ids(hass, entry_id, powerwall_data)
login_failed_count = 0 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(): async def async_update_data():
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
# Check if we had an error before # Check if we had an error before
nonlocal login_failed_count nonlocal login_failed_count
_LOGGER.debug("Checking if update failed") _LOGGER.debug("Checking if update failed")
if hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]: if runtime_data[POWERWALL_API_CHANGED]:
return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data return runtime_data[POWERWALL_COORDINATOR].data
_LOGGER.debug("Updating data") _LOGGER.debug("Updating data")
try: try:
@ -130,9 +145,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if password is None: if password is None:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err
# If the session expired, relogin, and try again # If the session expired, recreate, relogin, and try again
try: 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) return await _async_update_powerwall_data(hass, entry, power_wall)
except AccessDeniedError as ex: except AccessDeniedError as ex:
login_failed_count += 1 login_failed_count += 1
@ -153,13 +168,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
update_interval=timedelta(seconds=UPDATE_INTERVAL), update_interval=timedelta(seconds=UPDATE_INTERVAL),
) )
hass.data[DOMAIN][entry.entry_id] = powerwall_data runtime_data.update(
hass.data[DOMAIN][entry.entry_id].update(
{ {
**powerwall_data,
POWERWALL_OBJECT: power_wall, POWERWALL_OBJECT: power_wall,
POWERWALL_COORDINATOR: coordinator, POWERWALL_COORDINATOR: coordinator,
POWERWALL_HTTP_SESSION: http_session,
POWERWALL_API_CHANGED: False,
} }
) )

View File

@ -13,6 +13,7 @@ from sqlalchemy import bindparam, func
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext import baked from sqlalchemy.ext import baked
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.sql.expression import true
from homeassistant.const import ( from homeassistant.const import (
PRESSURE_PA, PRESSURE_PA,
@ -396,9 +397,9 @@ def get_metadata_with_session(
StatisticsMeta.statistic_id.in_(bindparam("statistic_ids")) StatisticsMeta.statistic_id.in_(bindparam("statistic_ids"))
) )
if statistic_type == "mean": 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": 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)) result = execute(baked_query(session).params(statistic_ids=statistic_ids))
if not result: if not result:
return {} return {}

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta
from typing import Any from typing import Any
from kasa import SmartDevice, SmartDeviceException from kasa import SmartDevice, SmartDeviceException
@ -11,9 +12,15 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import network from homeassistant.components import network
from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady 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.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr 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 homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
@ -32,6 +39,8 @@ from .migration import (
async_migrate_yaml_entries, async_migrate_yaml_entries,
) )
DISCOVERY_INTERVAL = timedelta(minutes=15)
TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string})
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
@ -118,6 +127,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if discovered_devices: if discovered_devices:
async_trigger_discovery(hass, 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 return True

View File

@ -63,10 +63,20 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
@async_refresh_after @async_refresh_after
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on.""" """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: if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None:
brightness = round((brightness * 100.0) / 255.0) 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 # Handle turning to temp mode
if ATTR_COLOR_TEMP in kwargs: if ATTR_COLOR_TEMP in kwargs:
color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP])) color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
@ -92,7 +102,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
@async_refresh_after @async_refresh_after
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off.""" """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 @property
def min_mireds(self) -> int: def min_mireds(self) -> int:
@ -145,7 +157,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
def color_mode(self) -> str | None: def color_mode(self) -> str | None:
"""Return the active color mode.""" """Return the active color mode."""
if self.device.is_color: 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_COLOR_TEMP
return COLOR_MODE_HS return COLOR_MODE_HS
if self.device.is_variable_color_temp: if self.device.is_variable_color_temp:

View File

@ -146,10 +146,14 @@ class XiaomiAirHumidifierSelector(XiaomiSelector):
@callback @callback
def _handle_coordinator_update(self): def _handle_coordinator_update(self):
"""Fetch state from the device.""" """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 self.coordinator.data, self.entity_description.key
) )
self.async_write_ha_state() # 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 @property
def current_option(self): def current_option(self):

View File

@ -2,7 +2,7 @@
"domain": "yeelight", "domain": "yeelight",
"name": "Yeelight", "name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/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"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
"config_flow": true, "config_flow": true,
"dependencies": ["network"], "dependencies": ["network"],

View File

@ -408,7 +408,7 @@ class ZWaveServices:
async def async_set_value(self, service: ServiceCall) -> None: async def async_set_value(self, service: ServiceCall) -> None:
"""Set a value on a node.""" """Set a value on a node."""
# pylint: disable=no-self-use # 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] command_class = service.data[const.ATTR_COMMAND_CLASS]
property_ = service.data[const.ATTR_PROPERTY] property_ = service.data[const.ATTR_PROPERTY]
property_key = service.data.get(const.ATTR_PROPERTY_KEY) property_key = service.data.get(const.ATTR_PROPERTY_KEY)
@ -418,15 +418,27 @@ class ZWaveServices:
options = service.data.get(const.ATTR_OPTIONS) options = service.data.get(const.ATTR_OPTIONS)
for node in nodes: for node in nodes:
value_id = get_value_id(
node,
command_class,
property_,
endpoint=endpoint,
property_key=property_key,
)
# 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( success = await node.async_set_value(
get_value_id( value_id,
node, new_value_,
command_class,
property_,
endpoint=endpoint,
property_key=property_key,
),
new_value,
options=options, options=options,
wait_for_result=wait_for_result, wait_for_result=wait_for_result,
) )
@ -452,11 +464,16 @@ class ZWaveServices:
await self.async_set_value(service) await self.async_set_value(service)
return 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 = { value = {
"commandClass": service.data[const.ATTR_COMMAND_CLASS], "commandClass": command_class,
"property": service.data[const.ATTR_PROPERTY], "property": property_,
"propertyKey": service.data.get(const.ATTR_PROPERTY_KEY), "propertyKey": property_key,
"endpoint": service.data.get(const.ATTR_ENDPOINT), "endpoint": endpoint,
} }
new_value = service.data[const.ATTR_VALUE] 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 # schema validation and can use that to get the client, otherwise we can just
# get the client from the node. # get the client from the node.
client: ZwaveClient = None 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: if first_node:
client = first_node.client client = first_node.client
else: else:
entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id
client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT] 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( success = await async_multicast_set_value(
client=client, client=client,

View File

@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021 MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 10 MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -15,7 +15,7 @@ ciso8601==2.2.0
cryptography==3.4.8 cryptography==3.4.8
emoji==1.5.0 emoji==1.5.0
hass-nabucasa==0.50.0 hass-nabucasa==0.50.0
home-assistant-frontend==20211006.0 home-assistant-frontend==20211007.0
httpx==0.19.0 httpx==0.19.0
ifaddr==0.1.7 ifaddr==0.1.7
jinja2==3.0.1 jinja2==3.0.1

View File

@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2
# Adafruit_BBIO==1.1.1 # Adafruit_BBIO==1.1.1
# homeassistant.components.homekit # homeassistant.components.homekit
HAP-python==4.2.1 HAP-python==4.3.0
# homeassistant.components.mastodon # homeassistant.components.mastodon
Mastodon.py==1.5.1 Mastodon.py==1.5.1
@ -810,7 +810,7 @@ hole==0.5.1
holidays==0.11.3.1 holidays==0.11.3.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20211006.0 home-assistant-frontend==20211007.0
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@ -1005,7 +1005,7 @@ micloud==0.3
miflora==0.7.0 miflora==0.7.0
# homeassistant.components.mill # homeassistant.components.mill
millheater==0.6.0 millheater==0.6.1
# homeassistant.components.minio # homeassistant.components.minio
minio==4.0.9 minio==4.0.9
@ -2459,7 +2459,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13 yalexs==1.1.13
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.6 yeelight==0.7.7
# homeassistant.components.yeelightsunflower # homeassistant.components.yeelightsunflower
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10

View File

@ -7,7 +7,7 @@
AEMET-OpenData==0.2.1 AEMET-OpenData==0.2.1
# homeassistant.components.homekit # homeassistant.components.homekit
HAP-python==4.2.1 HAP-python==4.3.0
# homeassistant.components.flick_electric # homeassistant.components.flick_electric
PyFlick==0.0.2 PyFlick==0.0.2
@ -485,7 +485,7 @@ hole==0.5.1
holidays==0.11.3.1 holidays==0.11.3.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20211006.0 home-assistant-frontend==20211007.0
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@ -585,7 +585,7 @@ mficlient==0.3.0
micloud==0.3 micloud==0.3
# homeassistant.components.mill # homeassistant.components.mill
millheater==0.6.0 millheater==0.6.1
# homeassistant.components.minio # homeassistant.components.minio
minio==4.0.9 minio==4.0.9
@ -1403,7 +1403,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13 yalexs==1.1.13
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.6 yeelight==0.7.7
# homeassistant.components.youless # homeassistant.components.youless
youless-api==0.13 youless-api==0.13

View File

@ -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_add_executor_job(acc.char_speed.client_update_value, 42)
await hass.async_block_till_done() 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 acc.char_active.value == 1
assert call_set_percentage[0] 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 # Verify speed is preserved from off to on
hass.states.async_set(entity_id, STATE_OFF, {ATTR_PERCENTAGE: 42}) hass.states.async_set(entity_id, STATE_OFF, {ATTR_PERCENTAGE: 42})
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_speed.value == 42 assert acc.char_speed.value == 50
assert acc.char_active.value == 0 assert acc.char_active.value == 0
hk_driver.set_characteristics( hk_driver.set_characteristics(
@ -325,7 +325,7 @@ async def test_fan_speed(hass, hk_driver, events):
"mock_addr", "mock_addr",
) )
await hass.async_block_till_done() 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 acc.char_active.value == 1

View File

@ -1,5 +1,7 @@
"""Test different accessory types: Media Players.""" """Test different accessory types: Media Players."""
import pytest
from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.const import (
ATTR_KEY_NAME, ATTR_KEY_NAME,
ATTR_VALUE, ATTR_VALUE,
@ -353,8 +355,9 @@ async def test_media_player_television(hass, hk_driver, events, caplog):
hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener) hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener)
await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20) with pytest.raises(ValueError):
await hass.async_block_till_done() await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20)
await hass.async_block_till_done()
await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 7) await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 7)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -1,5 +1,7 @@
"""Test different accessory types: Remotes.""" """Test different accessory types: Remotes."""
import pytest
from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.const import (
ATTR_KEY_NAME, ATTR_KEY_NAME,
ATTR_VALUE, ATTR_VALUE,
@ -140,8 +142,9 @@ async def test_activity_remote(hass, hk_driver, events, caplog):
hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener) hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener)
acc.char_remote_key.client_update_value(20) with pytest.raises(ValueError):
await hass.async_block_till_done() acc.char_remote_key.client_update_value(20)
await hass.async_block_till_done()
acc.char_remote_key.client_update_value(7) acc.char_remote_key.client_update_value(7)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -1746,11 +1746,7 @@ async def test_water_heater(hass, hk_driver, events):
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == f"52.0{TEMP_CELSIUS}" 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_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
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 2)
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1

View File

@ -744,3 +744,30 @@ async def test_timestamp(hass):
finally: finally:
dt_util.set_default_time_zone(ORIG_TIMEZONE) 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

View File

@ -33,6 +33,7 @@ def _mocked_bulb() -> SmartBulb:
bulb.is_color = True bulb.is_color = True
bulb.is_strip = False bulb.is_strip = False
bulb.is_plug = False bulb.is_plug = False
bulb.is_dimmer = False
bulb.hsv = (10, 30, 5) bulb.hsv = (10, 30, 5)
bulb.device_id = MAC_ADDRESS bulb.device_id = MAC_ADDRESS
bulb.valid_temperature_range.min = 4000 bulb.valid_temperature_range.min = 4000

View File

@ -1,27 +1,41 @@
"""Tests for the TP-Link component.""" """Tests for the TP-Link component."""
from __future__ import annotations 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 import tplink
from homeassistant.components.tplink.const import DOMAIN from homeassistant.components.tplink.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState 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.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 . 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): async def test_configuring_tplink_causes_discovery(hass):
"""Test that specifying empty config does discovery.""" """Test that specifying empty config does discovery."""
with patch("homeassistant.components.tplink.Discover.discover") as discover: 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 async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done() 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): async def test_config_entry_reload(hass):

View File

@ -1,5 +1,8 @@
"""Tests for light platform.""" """Tests for light platform."""
from typing import Optional
from unittest.mock import PropertyMock
import pytest import pytest
from homeassistant.components import tplink from homeassistant.components import tplink
@ -12,6 +15,7 @@ from homeassistant.components.light import (
ATTR_MIN_MIREDS, ATTR_MIN_MIREDS,
ATTR_RGB_COLOR, ATTR_RGB_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
ATTR_XY_COLOR, ATTR_XY_COLOR,
DOMAIN as LIGHT_DOMAIN, 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" assert entity_registry.async_get(entity_id).unique_id == "AABBCCDDEEFF"
async def test_color_light(hass: HomeAssistant) -> None: @pytest.mark.parametrize("transition", [2.0, None])
"""Test a light.""" 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( already_migrated_config_entry = MockConfigEntry(
domain=DOMAIN, data={}, unique_id=MAC_ADDRESS 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() await hass.async_block_till_done()
entity_id = "light.my_bulb" 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) state = hass.states.get(entity_id)
assert state.state == "on" 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_RGB_COLOR] == (255, 191, 178)
assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) 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( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True 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.assert_called_with(39, transition=None)
bulb.set_brightness.reset_mock() 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( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
"turn_on", "turn_on",
@ -282,3 +349,29 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None:
assert state.state == "off" assert state.state == "off"
attributes = state.attributes attributes = state.attributes
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"] 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()

View File

@ -43,6 +43,7 @@ from .common import (
CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_DANFOSS_LC13_ENTITY,
CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY,
CLIMATE_RADIO_THERMOSTAT_ENTITY, CLIMATE_RADIO_THERMOSTAT_ENTITY,
SCHLAGE_BE469_LOCK_ENTITY,
) )
from tests.common import MockConfigEntry 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): async def test_set_value_options(hass, client, aeon_smart_switch_6, integration):
"""Test set_value service with options.""" """Test set_value service with options."""
await hass.services.async_call( await hass.services.async_call(
@ -1381,6 +1427,41 @@ async def test_multicast_set_value_options(
client.async_send_command.reset_mock() 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( async def test_ping(
hass, hass,
client, client,