diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 53bee3d8519..1e8df555a22 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.5"], + "requirements": ["async-upnp-client==0.22.8"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 45d88a07f88..68ad61c33fc 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -259,25 +259,31 @@ class CoverGroup(GroupEntity, CoverEntity): """Update state and attributes.""" self._attr_assumed_state = False - self._attr_is_closed = None + self._attr_is_closed = True self._attr_is_closing = False self._attr_is_opening = False + has_valid_state = False for entity_id in self._entities: state = self.hass.states.get(entity_id) if not state: continue if state.state == STATE_OPEN: self._attr_is_closed = False + has_valid_state = True continue if state.state == STATE_CLOSED: - self._attr_is_closed = True + has_valid_state = True continue if state.state == STATE_CLOSING: self._attr_is_closing = True + has_valid_state = True continue if state.state == STATE_OPENING: self._attr_is_opening = True + has_valid_state = True continue + if not has_valid_state: + self._attr_is_closed = None position_covers = self._covers[KEY_POSITION] all_position_states = [self.hass.states.get(x) for x in position_covers] diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 5902beba226..23d9032ff07 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -168,8 +168,8 @@ class NanoleafLight(LightEntity): async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - transition = kwargs.get(ATTR_TRANSITION) - await self._nanoleaf.turn_off(transition) + transition: float | None = kwargs.get(ATTR_TRANSITION) + await self._nanoleaf.turn_off(None if transition is None else int(transition)) async def async_update(self) -> None: """Fetch new state data for this light.""" diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 133257dc7fe..501f1beb75a 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nanoleaf", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["aionanoleaf==0.0.2"], + "requirements": ["aionanoleaf==0.0.3"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], "homekit" : { "models": [ diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 18813ac27cd..62985c7104c 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Netgear integration.""" +import logging from urllib.parse import urlparse from pynetgear import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER @@ -20,6 +21,8 @@ from .const import CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, DEFAULT_NAME, DOMA from .errors import CannotLoginException from .router import get_api +_LOGGER = logging.getLogger(__name__) + def _discovery_schema_with_defaults(discovery_info): return vol.Schema(_ordered_shared_schema(discovery_info)) @@ -120,15 +123,19 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): device_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) if device_url.hostname: updated_data[CONF_HOST] = device_url.hostname - if device_url.port: - updated_data[CONF_PORT] = device_url.port if device_url.scheme == "https": updated_data[CONF_SSL] = True else: updated_data[CONF_SSL] = False + _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) + await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) self._abort_if_unique_id_configured(updates=updated_data) + + if device_url.port: + updated_data[CONF_PORT] = device_url.port + self.placeholders.update(updated_data) self.discovered = True diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 53cc4f32728..cc508f043ff 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -274,8 +274,8 @@ class NetgearDeviceEntity(Entity): """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "name": self._device_name, - "model": self._device["device_model"], + "default_name": self._device_name, + "default_model": self._device["device_model"], "via_device": (DOMAIN, self._router.unique_id), } diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index c485622af80..30b5a4605ef 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -7,6 +7,7 @@ import datetime import itertools import logging import math +from typing import Any from sqlalchemy.orm.session import Session @@ -362,13 +363,14 @@ def _wanted_statistics(sensor_states: list[State]) -> dict[str, set[str]]: return wanted_statistics -def _last_reset_as_utc_isoformat( - last_reset_s: str | None, entity_id: str -) -> str | None: +def _last_reset_as_utc_isoformat(last_reset_s: Any, entity_id: str) -> str | None: """Parse last_reset and convert it to UTC.""" if last_reset_s is None: return None - last_reset = dt_util.parse_datetime(last_reset_s) + if isinstance(last_reset_s, str): + last_reset = dt_util.parse_datetime(last_reset_s) + else: + last_reset = None if last_reset is None: _LOGGER.warning( "Ignoring invalid last reset '%s' for %s", last_reset_s, entity_id diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 3e99a77e8bb..85e489a72dd 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.5"], + "requirements": ["async-upnp-client==0.22.8"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 9f3658cf2cd..ff0526490f5 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -120,6 +120,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_migrate_legacy_entries( hass, hosts_by_mac, config_entries_by_mac, legacy_entry ) + # Migrate the yaml entry that was previously imported + async_migrate_yaml_entries(hass, legacy_entry.data) if conf is not None: async_migrate_yaml_entries(hass, conf) diff --git a/homeassistant/components/tplink/migration.py b/homeassistant/components/tplink/migration.py index af81323d39f..9344dd1532f 100644 --- a/homeassistant/components/tplink/migration.py +++ b/homeassistant/components/tplink/migration.py @@ -2,6 +2,8 @@ from __future__ import annotations from datetime import datetime +from types import MappingProxyType +from typing import Any from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry @@ -65,7 +67,9 @@ def async_migrate_legacy_entries( @callback -def async_migrate_yaml_entries(hass: HomeAssistant, conf: ConfigType) -> None: +def async_migrate_yaml_entries( + hass: HomeAssistant, conf: ConfigType | MappingProxyType[str, Any] +) -> None: """Migrate yaml to config entries.""" for device_type in (CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER): for device in conf.get(device_type, []): diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 9a1875777a6..4029ff5c3bb 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.5"], + "requirements": ["async-upnp-client==0.22.8"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 1b8c959cb52..632fdf426f2 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.7", "async-upnp-client==0.22.5"], + "requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.8"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/const.py b/homeassistant/const.py index c8ddfb2f4d3..1281d39fad4 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 = "1" +PATCH_VERSION: Final = "2" __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 35d5649a1b4..3c3645f5ece 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.4 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.5 +async-upnp-client==0.22.8 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.8.1 diff --git a/requirements_all.txt b/requirements_all.txt index 1d3667d04b2..d708ec1497e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,7 +219,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.9.2 # homeassistant.components.nanoleaf -aionanoleaf==0.0.2 +aionanoleaf==0.0.3 # homeassistant.components.keyboard_remote aionotify==0.2.0 @@ -330,7 +330,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.5 +async-upnp-client==0.22.8 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ddd959692f..f7f5c0328a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.9.2 # homeassistant.components.nanoleaf -aionanoleaf==0.0.2 +aionanoleaf==0.0.3 # homeassistant.components.notion aionotion==3.0.2 @@ -224,7 +224,7 @@ arcam-fmj==0.7.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.5 +async-upnp-client==0.22.8 # homeassistant.components.aurora auroranoaa==0.0.2 diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 9d16be9150b..cf1fba992e7 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -96,6 +96,106 @@ async def setup_comp(hass, config_count): await hass.async_block_till_done() +@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) +async def test_state(hass, setup_comp): + """Test handling of state.""" + state = hass.states.get(COVER_GROUP) + # No entity has a valid state -> group state unknown + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert state.attributes[ATTR_ENTITY_ID] == [ + DEMO_COVER, + DEMO_COVER_POS, + DEMO_COVER_TILT, + DEMO_TILT, + ] + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert ATTR_CURRENT_POSITION not in state.attributes + assert ATTR_CURRENT_TILT_POSITION not in state.attributes + + # Set all entities as closed -> group state closed + hass.states.async_set(DEMO_COVER, STATE_CLOSED, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + + # Set all entities as open -> group state open + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_OPEN, {}) + hass.states.async_set(DEMO_TILT, STATE_OPEN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + + # Set first entity as open -> group state open + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + + # Set last entity as open -> group state open + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + + # Set conflicting valid states -> opening state has priority + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSING, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPENING + + # Set all entities to unknown state -> group state unknown + hass.states.async_set(DEMO_COVER, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_TILT, STATE_UNKNOWN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_UNKNOWN + + # Set one entity to unknown state -> open state has priority + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) + hass.states.async_set(DEMO_TILT, STATE_OPEN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + + # Set one entity to unknown state -> opening state has priority + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPENING + + # Set one entity to unknown state -> closing state has priority + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSING, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSING + + @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_attributes(hass, setup_comp): """Test handling of state attributes.""" @@ -196,7 +296,7 @@ async def test_attributes(hass, setup_comp): # ### Test assumed state ### # ########################## - # For covers + # For covers - assumed state set true if position differ hass.states.async_set( DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100} ) @@ -220,7 +320,7 @@ async def test_attributes(hass, setup_comp): assert ATTR_CURRENT_POSITION not in state.attributes assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60 - # For tilts + # For tilts - assumed state set true if tilt position differ hass.states.async_set( DEMO_TILT, STATE_OPEN, @@ -252,6 +352,7 @@ async def test_attributes(hass, setup_comp): state = hass.states.get(COVER_GROUP) assert state.attributes[ATTR_ASSUMED_STATE] is True + # Test entity registry integration entity_registry = er.async_get(hass) entry = entity_registry.async_get(COVER_GROUP) assert entry diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index f3ddab39c39..64edd9e8341 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -23,9 +23,9 @@ from tests.common import async_fire_time_changed def _ssdp_headers(headers): - return CaseInsensitiveDict( - headers, _timestamp=datetime(2021, 1, 1, 12, 00), _udn=udn_from_headers(headers) - ) + ssdp_headers = CaseInsensitiveDict(headers, _timestamp=datetime(2021, 1, 1, 12, 00)) + ssdp_headers["_udn"] = udn_from_headers(ssdp_headers) + return ssdp_headers async def init_ssdp_component(hass: homeassistant) -> SsdpListener: @@ -45,7 +45,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow mock_ssdp_search_response = _ssdp_headers( { "st": "mock-st", - "location": None, + "location": "http://1.1.1.1", "usn": "uuid:mock-udn::mock-st", "server": "mock-server", "ext": "", @@ -64,7 +64,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow } assert mock_flow_init.mock_calls[0][2]["data"] == { ssdp.ATTR_SSDP_ST: "mock-st", - ssdp.ATTR_SSDP_LOCATION: None, + ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st", ssdp.ATTR_SSDP_SERVER: "mock-server", ssdp.ATTR_SSDP_EXT: "", diff --git a/tests/components/tplink/test_migration.py b/tests/components/tplink/test_migration.py index 6cd82448ca2..a1cd581e211 100644 --- a/tests/components/tplink/test_migration.py +++ b/tests/components/tplink/test_migration.py @@ -239,3 +239,25 @@ async def test_migrate_from_yaml(hass: HomeAssistant): assert migrated_entry is not None assert migrated_entry.data[CONF_HOST] == IP_ADDRESS + + +async def test_migrate_from_legacy_entry(hass: HomeAssistant): + """Test migrate from legacy entry that was already imported from yaml.""" + data = { + CONF_DISCOVERY: False, + CONF_SWITCH: [{CONF_HOST: IP_ADDRESS}], + } + config_entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_single_discovery(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + migrated_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == MAC_ADDRESS: + migrated_entry = entry + break + + assert migrated_entry is not None + assert migrated_entry.data[CONF_HOST] == IP_ADDRESS