Merge pull request #57355 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-10-08 15:47:54 -07:00 committed by GitHub
commit b110a5bbfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 175 additions and 31 deletions

View File

@ -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"

View File

@ -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]

View File

@ -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."""

View File

@ -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": [

View File

@ -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

View File

@ -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),
}

View File

@ -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

View File

@ -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": [],

View File

@ -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)

View File

@ -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, []):

View File

@ -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": [

View File

@ -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"],

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: "",

View File

@ -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