Merge pull request #55673 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-09-03 10:53:16 -07:00 committed by GitHub
commit 33047d7260
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 259 additions and 96 deletions

View File

@ -342,7 +342,11 @@ def async_enable_logging(
err_log_path, backupCount=1
)
err_handler.doRollover()
try:
err_handler.doRollover()
except OSError as err:
_LOGGER.error("Error rolling over log file: %s", err)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Iterable, Mapping
from functools import wraps
import logging
from types import ModuleType
from typing import Any
@ -27,7 +28,6 @@ from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig
DOMAIN = "device_automation"
DEVICE_TRIGGER_BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "device",
@ -174,6 +174,13 @@ async def _async_get_device_automations(
device_results, InvalidDeviceAutomationConfig
):
continue
if isinstance(device_results, Exception):
logging.getLogger(__name__).error(
"Unexpected error fetching device %ss",
automation_type,
exc_info=device_results,
)
continue
for automation in device_results:
combined_results[automation["device_id"]].append(automation)

View File

@ -7,6 +7,8 @@ from homeassistant.components.device_automation import (
)
from homeassistant.const import CONF_DOMAIN
from .exceptions import InvalidDeviceAutomationConfig
# mypy: allow-untyped-defs, no-check-untyped-defs
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
@ -17,10 +19,13 @@ async def async_validate_trigger_config(hass, config):
platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], "trigger"
)
if hasattr(platform, "async_validate_trigger_config"):
return await getattr(platform, "async_validate_trigger_config")(hass, config)
if not hasattr(platform, "async_validate_trigger_config"):
return platform.TRIGGER_SCHEMA(config)
return platform.TRIGGER_SCHEMA(config)
try:
return await getattr(platform, "async_validate_trigger_config")(hass, config)
except InvalidDeviceAutomationConfig as err:
raise vol.Invalid(str(err) or "Invalid trigger configuration") from err
async def async_attach_trigger(hass, config, action, automation_info):

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import logging
from homeassistant.components.switch import DOMAIN, SwitchEntity
from homeassistant.const import STATE_OFF, STATE_ON
from . import ATTR_NEW, CecEntity
@ -34,17 +35,25 @@ class CecSwitchEntity(CecEntity, SwitchEntity):
def turn_on(self, **kwargs) -> None:
"""Turn device on."""
self._device.turn_on()
self._attr_is_on = True
self._state = STATE_ON
self.schedule_update_ha_state(force_refresh=False)
def turn_off(self, **kwargs) -> None:
"""Turn device off."""
self._device.turn_off()
self._attr_is_on = False
self._state = STATE_OFF
self.schedule_update_ha_state(force_refresh=False)
def toggle(self, **kwargs):
"""Toggle the entity."""
self._device.toggle()
self._attr_is_on = not self._attr_is_on
if self._state == STATE_ON:
self._state = STATE_OFF
else:
self._state = STATE_ON
self.schedule_update_ha_state(force_refresh=False)
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
return self._state == STATE_ON

View File

@ -118,12 +118,16 @@ async def async_validate_trigger_config(hass, config):
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
if (
not device
or device.model not in REMOTES
or trigger not in REMOTES[device.model]
):
raise InvalidDeviceAutomationConfig
if not device:
raise InvalidDeviceAutomationConfig("Device {config[CONF_DEVICE_ID]} not found")
if device.model not in REMOTES:
raise InvalidDeviceAutomationConfig(
f"Device model {device.model} is not a remote"
)
if trigger not in REMOTES[device.model]:
raise InvalidDeviceAutomationConfig("Device does not support trigger {trigger}")
return config

View File

@ -32,6 +32,8 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES]
CONFIG_SCHEMA = vol.Schema(
vol.All(
# Deprecated in Home Assistant 2021.6
@ -46,8 +48,8 @@ CONFIG_SCHEMA = vol.Schema(
): cv.positive_time_period,
vol.Optional(CONF_MANUAL, default=False): cv.boolean,
vol.Optional(
CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)
): vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]),
CONF_MONITORED_CONDITIONS, default=list(SENSOR_KEYS)
): vol.All(cv.ensure_list, [vol.In(list(SENSOR_KEYS))]),
}
)
},

View File

@ -289,7 +289,7 @@ class Scanner:
def _async_unsee(self, header_st: str | None, header_location: str | None) -> None:
"""If we see a device in a new location, unsee the original location."""
if header_st is not None:
self.seen.remove((header_st, header_location))
self.seen.discard((header_st, header_location))
async def _async_process_entry(self, headers: Mapping[str, str]) -> None:
"""Process SSDP entries."""

View File

@ -90,7 +90,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
sensor
for device in account.api.devices.values()
for description in SENSOR_TYPES
if (sensor := StarlineSensor(account, device, description)).state is not None
if (sensor := StarlineSensor(account, device, description)).native_value
is not None
]
async_add_entities(entities)

View File

@ -69,7 +69,7 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
# We make a copy so our initial render is 'unknown' and not 'unavailable'
self._rendered = dict(self._static_rendered)
self._parse_result = set()
self._parse_result = {CONF_AVAILABILITY}
@property
def name(self):

View File

@ -112,7 +112,7 @@ class USBDiscovery:
if not sys.platform.startswith("linux"):
return
info = await system_info.async_get_system_info(self.hass)
if info.get("docker") and not info.get("hassio"):
if info.get("docker"):
return
from pyudev import ( # pylint: disable=import-outside-toplevel

View File

@ -326,11 +326,6 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN):
device = discovery_info["device"]
manufacturer = discovery_info["manufacturer"]
description = discovery_info["description"]
# The Nortek sticks are a special case since they
# have a Z-Wave and a Zigbee radio. We need to reject
# the Zigbee radio.
if vid == "10C4" and pid == "8A2A" and "Z-Wave" not in description:
return self.async_abort(reason="not_zwave_device")
# Zooz uses this vid/pid, but so do 2652 sticks
if vid == "10C4" and pid == "EA60" and "2652" in description:
return self.async_abort(reason="not_zwave_device")

View File

@ -9,7 +9,7 @@
"iot_class": "local_push",
"usb": [
{"vid":"0658","pid":"0200","known_devices":["Aeotec Z-Stick Gen5+", "Z-WaveMe UZB"]},
{"vid":"10C4","pid":"8A2A","known_devices":["Nortek HUSBZB-1"]},
{"vid":"10C4","pid":"8A2A","description":"*z-wave*","known_devices":["Nortek HUSBZB-1"]},
{"vid":"10C4","pid":"EA60","known_devices":["Aeotec Z-Stick 7", "Silicon Labs UZB-7", "Zooz ZST10 700"]}
]
}

View File

@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 9
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

@ -32,7 +32,8 @@ USB = [
{
"domain": "zwave_js",
"vid": "10C4",
"pid": "8A2A"
"pid": "8A2A",
"description": "*z-wave*"
},
{
"domain": "zwave_js",

View File

@ -14,6 +14,7 @@ from unittest.mock import patch
from homeassistant import core
from homeassistant.config import get_default_config_dir
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import area_registry, device_registry, entity_registry
from homeassistant.helpers.check_config import async_check_ha_config_file
from homeassistant.util.yaml import Secrets
import homeassistant.util.yaml.loader as yaml_loader
@ -229,6 +230,9 @@ async def async_check_config(config_dir):
"""Check the HA config."""
hass = core.HomeAssistant()
hass.config.config_dir = config_dir
await area_registry.async_load(hass)
await device_registry.async_load(hass)
await entity_registry.async_load(hass)
components = await async_check_ha_config_file(hass)
await hass.async_stop(force=True)
return components

View File

@ -1,4 +1,6 @@
"""The test for light device automation."""
from unittest.mock import patch
import pytest
from homeassistant.components import device_automation
@ -443,6 +445,28 @@ async def test_async_get_device_automations_all_devices_action(
assert len(result[device_entry.id]) == 3
async def test_async_get_device_automations_all_devices_action_exception_throw(
hass, device_reg, entity_reg, caplog
):
"""Test we get can fetch all the actions when no device id is passed and can handle one throwing an exception."""
await async_setup_component(hass, "device_automation", {})
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
with patch(
"homeassistant.components.light.device_trigger.async_get_triggers",
side_effect=KeyError,
):
result = await device_automation.async_get_device_automations(hass, "trigger")
assert device_entry.id in result
assert len(result[device_entry.id]) == 0
assert "KeyError" in caplog.text
async def test_websocket_get_trigger_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):

View File

@ -991,9 +991,6 @@ async def test_location_change_evicts_prior_location_from_cache(hass, aioclient_
@callback
def _callback(*_):
import pprint
pprint.pprint(mock_ssdp_response)
hass.async_create_task(listener.async_callback(mock_ssdp_response))
listener.async_start = _async_callback
@ -1050,3 +1047,113 @@ async def test_location_change_evicts_prior_location_from_cache(hass, aioclient_
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
== mock_good_ip_ssdp_response["location"]
)
async def test_location_change_with_overlapping_udn_st_combinations(
hass, aioclient_mock
):
"""Test handling when a UDN and ST broadcast multiple locations."""
mock_get_ssdp = {
"test_integration": [
{"manufacturer": "test_manufacturer", "modelName": "test_model"}
]
}
hue_response = """
<root xmlns="urn:schemas-upnp-org:device-1-0">
<device>
<manufacturer>test_manufacturer</manufacturer>
<modelName>test_model</modelName>
</device>
</root>
"""
aioclient_mock.get(
"http://192.168.72.1:49154/wps_device.xml",
text=hue_response.format(ip_address="192.168.72.1"),
)
aioclient_mock.get(
"http://192.168.72.1:49152/wps_device.xml",
text=hue_response.format(ip_address="192.168.72.1"),
)
ssdp_response_without_location = {
"ST": "upnp:rootdevice",
"_udn": "uuid:a793d3cc-e802-44fb-84f4-5a30f33115b6",
"USN": "uuid:a793d3cc-e802-44fb-84f4-5a30f33115b6::upnp:rootdevice",
"EXT": "",
}
port_49154_response = CaseInsensitiveDict(
**ssdp_response_without_location,
**{"LOCATION": "http://192.168.72.1:49154/wps_device.xml"},
)
port_49152_response = CaseInsensitiveDict(
**ssdp_response_without_location,
**{"LOCATION": "http://192.168.72.1:49152/wps_device.xml"},
)
mock_ssdp_response = port_49154_response
def _generate_fake_ssdp_listener(*args, **kwargs):
listener = SSDPListener(*args, **kwargs)
async def _async_callback(*_):
pass
@callback
def _callback(*_):
hass.async_create_task(listener.async_callback(mock_ssdp_response))
listener.async_start = _async_callback
listener.async_search = _callback
return listener
with patch(
"homeassistant.components.ssdp.async_get_ssdp",
return_value=mock_get_ssdp,
), patch(
"homeassistant.components.ssdp.SSDPListener",
new=_generate_fake_ssdp_listener,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
await hass.async_block_till_done()
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "test_integration"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_SSDP
}
assert (
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
== port_49154_response["location"]
)
mock_init.reset_mock()
mock_ssdp_response = port_49152_response
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=400))
await hass.async_block_till_done()
assert mock_init.mock_calls[0][1][0] == "test_integration"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_SSDP
}
assert (
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
== port_49152_response["location"]
)
mock_init.reset_mock()
mock_ssdp_response = port_49154_response
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=600))
await hass.async_block_till_done()
assert mock_init.mock_calls[0][1][0] == "test_integration"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_SSDP
}
assert (
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
== port_49154_response["location"]
)

View File

@ -1038,6 +1038,7 @@ async def test_trigger_entity(hass):
"unique_id": "via_list-id",
"device_class": "battery",
"unit_of_measurement": "%",
"availability": "{{ True }}",
"state": "{{ trigger.event.data.beer + 1 }}",
"picture": "{{ '/local/dogs.png' }}",
"icon": "{{ 'mdi:pirate' }}",
@ -1197,3 +1198,44 @@ async def test_config_top_level(hass):
assert state.state == "5"
assert state.attributes["device_class"] == "battery"
assert state.attributes["state_class"] == "measurement"
async def test_trigger_entity_available(hass):
"""Test trigger entity availability works."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": [
{
"name": "Maybe Available",
"availability": "{{ trigger and trigger.event.data.beer == 2 }}",
"state": "{{ trigger.event.data.beer }}",
},
],
},
],
},
)
await hass.async_block_till_done()
# Sensors are unknown if never triggered
state = hass.states.get("sensor.maybe_available")
assert state is not None
assert state.state == STATE_UNKNOWN
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("sensor.maybe_available")
assert state.state == "2"
hass.bus.async_fire("test_event", {"beer": 1})
await hass.async_block_till_done()
state = hass.states.get("sensor.maybe_available")
assert state.state == "unavailable"

View File

@ -52,55 +52,6 @@ def mock_venv():
yield
@pytest.mark.skipif(
not sys.platform.startswith("linux"),
reason="Only works on linux",
)
async def test_discovered_by_observer_before_started(hass, operating_system):
"""Test a device is discovered by the observer before started."""
async def _mock_monitor_observer_callback(callback):
await hass.async_add_executor_job(
callback, MagicMock(action="add", device_path="/dev/new")
)
def _create_mock_monitor_observer(monitor, callback, name):
hass.async_create_task(_mock_monitor_observer_callback(callback))
return MagicMock()
new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}]
mock_comports = [
MagicMock(
device=slae_sh_device.device,
vid=12345,
pid=12345,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
)
]
with patch(
"homeassistant.components.usb.async_get_usb", return_value=new_usb
), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch(
"pyudev.MonitorObserver", new=_create_mock_monitor_observer
):
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
with patch("homeassistant.components.usb.comports", return_value=[]), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "test1"
@pytest.mark.skipif(
not sys.platform.startswith("linux"),
reason="Only works on linux",

View File

@ -756,10 +756,7 @@ async def test_usb_discovery_already_running(hass, supervisor, addon_running):
@pytest.mark.parametrize(
"discovery_info",
[
NORTEK_ZIGBEE_DISCOVERY_INFO,
CP2652_ZIGBEE_DISCOVERY_INFO,
],
[CP2652_ZIGBEE_DISCOVERY_INFO],
)
async def test_abort_usb_discovery_aborts_specific_devices(
hass, supervisor, addon_options, discovery_info

View File

@ -27,14 +27,23 @@ async def apply_stop_hass(stop_hass):
"""Make sure all hass are stopped."""
@pytest.fixture
def mock_is_file():
"""Mock is_file."""
# All files exist except for the old entity registry file
with patch(
"os.path.isfile", lambda path: not path.endswith("entity_registry.yaml")
):
yield
def normalize_yaml_files(check_dict):
"""Remove configuration path from ['yaml_files']."""
root = get_test_config_dir()
return [key.replace(root, "...") for key in sorted(check_dict["yaml_files"].keys())]
@patch("os.path.isfile", return_value=True)
def test_bad_core_config(isfile_patch, loop):
def test_bad_core_config(mock_is_file, loop):
"""Test a bad core config setup."""
files = {YAML_CONFIG_FILE: BAD_CORE_CONFIG}
with patch_yaml_files(files):
@ -43,8 +52,7 @@ def test_bad_core_config(isfile_patch, loop):
assert res["except"]["homeassistant"][1] == {"unit_system": "bad"}
@patch("os.path.isfile", return_value=True)
def test_config_platform_valid(isfile_patch, loop):
def test_config_platform_valid(mock_is_file, loop):
"""Test a valid platform setup."""
files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"}
with patch_yaml_files(files):
@ -57,8 +65,7 @@ def test_config_platform_valid(isfile_patch, loop):
assert len(res["yaml_files"]) == 1
@patch("os.path.isfile", return_value=True)
def test_component_platform_not_found(isfile_patch, loop):
def test_component_platform_not_found(mock_is_file, loop):
"""Test errors if component or platform not found."""
# Make sure they don't exist
files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"}
@ -89,8 +96,7 @@ def test_component_platform_not_found(isfile_patch, loop):
assert len(res["yaml_files"]) == 1
@patch("os.path.isfile", return_value=True)
def test_secrets(isfile_patch, loop):
def test_secrets(mock_is_file, loop):
"""Test secrets config checking method."""
secrets_path = get_test_config_dir("secrets.yaml")
@ -121,8 +127,7 @@ def test_secrets(isfile_patch, loop):
]
@patch("os.path.isfile", return_value=True)
def test_package_invalid(isfile_patch, loop):
def test_package_invalid(mock_is_file, loop):
"""Test an invalid package."""
files = {
YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]')

View File

@ -56,11 +56,14 @@ async def test_home_assistant_core_config_validation(hass):
assert result is None
async def test_async_enable_logging(hass):
async def test_async_enable_logging(hass, caplog):
"""Test to ensure logging is migrated to the queue handlers."""
with patch("logging.getLogger"), patch(
"homeassistant.bootstrap.async_activate_log_queue_handler"
) as mock_async_activate_log_queue_handler:
) as mock_async_activate_log_queue_handler, patch(
"homeassistant.bootstrap.logging.handlers.RotatingFileHandler.doRollover",
side_effect=OSError,
):
bootstrap.async_enable_logging(hass)
mock_async_activate_log_queue_handler.assert_called_once()
mock_async_activate_log_queue_handler.reset_mock()
@ -75,6 +78,8 @@ async def test_async_enable_logging(hass):
for f in glob.glob("testing_config/home-assistant.log*"):
os.remove(f)
assert "Error rolling over log file" in caplog.text
async def test_load_hassio(hass):
"""Test that we load Hass.io component."""