mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Merge pull request #57355 from home-assistant/rc
This commit is contained in:
commit
b110a5bbfc
@ -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"
|
||||
|
@ -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]
|
||||
|
@ -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."""
|
||||
|
@ -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": [
|
||||
|
@ -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
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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": [],
|
||||
|
@ -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)
|
||||
|
@ -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, []):
|
||||
|
@ -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": [
|
||||
|
@ -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"],
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: "",
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user