From 7544ec2399d8c1e297312b5b72790c9c83309775 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 17:15:29 -1000 Subject: [PATCH 01/15] Recreate the powerwall session/object when attempting relogin (#56935) --- .../components/powerwall/__init__.py | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) 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, } ) From 7f49e02a4dc063ffb0db0eee5daca5b565c746b2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 7 Oct 2021 17:44:25 +0200 Subject: [PATCH 02/15] Update led brightness select state only if valid data is available, Xiaomi Miio integration (#57197) * Update state if there is valid data * Add comment --- homeassistant/components/xiaomi_miio/select.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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): From 691d8d6b80c3b6b6777a16d7deea652e75710c0b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 7 Oct 2021 16:22:33 -0400 Subject: [PATCH 03/15] Convert val to str when needed while calling zwave_js.set_value (#57216) --- homeassistant/components/zwave_js/services.py | 63 +++++++++++---- tests/components/zwave_js/test_services.py | 81 +++++++++++++++++++ 2 files changed, 130 insertions(+), 14 deletions(-) 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/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, From bf4a3d8d35d845c0d39b2621d1ae325c5a7639c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 05:52:24 -1000 Subject: [PATCH 04/15] Discover tplink devices periodically (#57221) - These devices sometimes do not respond on the first try or may be subject to transient broadcast failures, or overloads. We now try discovery periodically once the integration has been loaded. - We used to try this 4x at startup, but that solution seemed to aggressive as we want to be sure we pickup the devices after startup as well since the network will likely be more calm after startup. --- homeassistant/components/tplink/__init__.py | 18 +++++++++++++++- tests/components/tplink/test_init.py | 24 ++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) 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/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): From 06befe906b2c8690dda7de8b632ceb94bf37624a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Oct 2021 22:23:23 +0200 Subject: [PATCH 05/15] Correct SQL query generated by get_metadata_with_session (#57225) Co-authored-by: Franck Nijhof --- homeassistant/components/recorder/statistics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 {} From 0b26b1574998de243a31b47709c669b3edba5a91 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 7 Oct 2021 15:08:53 +0200 Subject: [PATCH 06/15] Fix netgear config flow import (#57253) --- homeassistant/components/netgear/device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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], ) ) From 425015eb8b9e92215f3249e0bd83874e13339aa5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Oct 2021 17:13:07 +0200 Subject: [PATCH 07/15] Validate initial value for input_datetime (#57256) --- .../components/input_datetime/__init__.py | 25 +++++++++++++++++ tests/components/input_datetime/test_init.py | 27 +++++++++++++++++++ 2 files changed, 52 insertions(+) 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/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 From ec0256e27f152fa191c4ac41798c5be7ddc4c07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 7 Oct 2021 18:23:03 +0200 Subject: [PATCH 08/15] Bump Mill library to 0.6.1 (#57261) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 92a334de096..c27afae95f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d00bf4a27b..8c0570c4f5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From fd371f2887babfad03e5e7726028d98ee5e2f6d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 10:14:14 -1000 Subject: [PATCH 09/15] Fix RGB only (no color temp) devices with tplink (#57267) --- homeassistant/components/tplink/light.py | 2 +- tests/components/tplink/test_light.py | 57 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index f1d936ecdfe..f0e911ea412 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -145,7 +145,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/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 19116005c37..501221f5a6f 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,5 +1,7 @@ """Tests for light platform.""" +from unittest.mock import PropertyMock + import pytest from homeassistant.components import tplink @@ -117,6 +119,61 @@ async def test_color_light(hass: HomeAssistant) -> None: 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 + ) + bulb.turn_off.assert_called_once() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turn_on.assert_called_once() + bulb.turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + 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_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.set_hsv.assert_called_with(10, 30, None, transition=None) + bulb.set_hsv.reset_mock() + + @pytest.mark.parametrize("is_color", [True, False]) async def test_color_temp_light(hass: HomeAssistant, is_color: bool) -> None: """Test a light.""" From 4e49e3a3fb1df82c5bc8e9787525d331ff37d62d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Oct 2021 22:18:12 +0200 Subject: [PATCH 10/15] Update frontend to 20211007.0 (#57268) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/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 c27afae95f6..0efe1bc5eea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c0570c4f5d..9c1defe013f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From e3e64130f1aab3b4e72bb4e84374c11e7b61ecff Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 8 Oct 2021 00:20:26 +0200 Subject: [PATCH 11/15] Fix transition handling for tplink lights (#57272) * Fix transition handling for tplink light * Apply suggestions from code review * Test that all transitions are passed correctly * Fix linting Co-authored-by: Paulus Schoutsen --- homeassistant/components/tplink/light.py | 8 +++-- tests/components/tplink/test_light.py | 42 +++++++++++++++--------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index f0e911ea412..49e8adbf08e 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -63,7 +63,9 @@ 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) @@ -92,7 +94,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: diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 501221f5a6f..93d8cdbf07d 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,5 +1,6 @@ """Tests for light platform.""" +from typing import Optional from unittest.mock import PropertyMock import pytest @@ -14,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, ) @@ -45,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 ) @@ -58,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" @@ -72,50 +80,52 @@ async def test_color_light(hass: HomeAssistant) -> None: 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 + LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True ) - bulb.turn_off.assert_called_once() + bulb.turn_off.assert_called_once_with(transition=KASA_TRANSITION_VALUE) - await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - bulb.turn_on.assert_called_once() + 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", - {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + {**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=None) + 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", - {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150}, + {**BASE_PAYLOAD, ATTR_COLOR_TEMP: 150}, blocking=True, ) - bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None) + 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", - {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150}, + {**BASE_PAYLOAD, ATTR_COLOR_TEMP: 150}, blocking=True, ) - bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None) + 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", - {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + {**BASE_PAYLOAD, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.set_hsv.assert_called_with(10, 30, None, transition=None) + bulb.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE) bulb.set_hsv.reset_mock() From ef7f1ffddc426f6318be735c8b180cf1304ee6ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 17:06:27 -1000 Subject: [PATCH 12/15] Bump HAP-python to 4.30 (#57284) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_type_fans.py | 6 +++--- tests/components/homekit/test_type_media_players.py | 7 +++++-- tests/components/homekit/test_type_remote.py | 7 +++++-- tests/components/homekit/test_type_thermostats.py | 6 +----- 7 files changed, 17 insertions(+), 15 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 0efe1bc5eea..7a0fc0e32f3 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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c1defe013f..54082e2a361 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 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 From ed4b44c1269e760e6f0ad6aae8cae08feaefb738 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 8 Oct 2021 05:15:13 +0200 Subject: [PATCH 13/15] Stopgap fix for inconsistent upstream API of tplink dimmers (#57285) --- homeassistant/components/tplink/light.py | 8 ++++++++ tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_light.py | 26 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 49e8adbf08e..3f4b130a5cc 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -69,6 +69,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: brightness = round((brightness * 100.0) / 255.0) + if self.device.is_dimmer and transition is None: + # This is a stopgap solution for inconsistent set_brightness handling + # in the upstream library, see #57265. + # 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])) 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_light.py b/tests/components/tplink/test_light.py index 93d8cdbf07d..1017ad38eae 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -349,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() From 1a376b25afa55177959d1714e586ea81106cc484 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 14:48:08 -1000 Subject: [PATCH 14/15] Bump yeelight to 0.7.7 (#57290) --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 7a0fc0e32f3..1d3667d04b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 54082e2a361..3ddd959692f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 387249b59327e6cf69c83ac02afdfdbb96f902e7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Oct 2021 20:19:53 -0700 Subject: [PATCH 15/15] Bumped version to 2021.10.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)