diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 121cf6ea754..f36d72a967c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -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", diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 2589a1ac6ec..d23aa11b4ea 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -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" diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 84a7fb89fe1..2713eef17f8 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -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, ) ) }, diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index bf332487a88..e5dbbdfc1e8 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -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" diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index f568a506552..0d3a1098f0c 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -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], ) ) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 5560c51f72b..2158075e5fa 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -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, } ) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7b7e349b843..d253d1e2275 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -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 {} diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 5c40f61e2df..9f3658cf2cd 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -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 diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index f1d936ecdfe..3f4b130a5cc 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -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: diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index daa721fef95..9e9cdcec1ae 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -146,10 +146,14 @@ 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 ) - 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 def current_option(self): diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 561606f5509..1b8c959cb52 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -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"], diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 08841465321..ac3f233ba49 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -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: + 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( - get_value_id( - node, - command_class, - property_, - endpoint=endpoint, - property_key=property_key, - ), - new_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, diff --git a/homeassistant/const.py b/homeassistant/const.py index 707e7d87b01..c8ddfb2f4d3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c263e0f4f3c..35d5649a1b4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 92a334de096..1d3667d04b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d00bf4a27b..3ddd959692f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 15e2366a883..85d00dcb287 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -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 diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 33cac7bcf8a..c0184667e2c 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -1,5 +1,7 @@ """Test different accessory types: Media Players.""" +import pytest + from homeassistant.components.homekit.const import ( ATTR_KEY_NAME, 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) - await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20) - await hass.async_block_till_done() + with pytest.raises(ValueError): + 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_block_till_done() diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py index ee71d7f4e3c..5c5b5ee6cd9 100644 --- a/tests/components/homekit/test_type_remote.py +++ b/tests/components/homekit/test_type_remote.py @@ -1,5 +1,7 @@ """Test different accessory types: Remotes.""" +import pytest + from homeassistant.components.homekit.const import ( ATTR_KEY_NAME, 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) - acc.char_remote_key.client_update_value(20) - await hass.async_block_till_done() + with pytest.raises(ValueError): + acc.char_remote_key.client_update_value(20) + await hass.async_block_till_done() acc.char_remote_key.client_update_value(7) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index e73465b0ab0..ef517f4ab96 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -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 diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 39497e1164c..0a968caf67f 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -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 diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 870e05e970b..f25fc13784a 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -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 diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index c3f7e814ed6..c166fccc9b5 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -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): diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 19116005c37..1017ad38eae 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -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() diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 0831d08b216..571190bd35c 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -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,