From 215d0ac612632b50449aeb66d33ce6a5ca021fc1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Dec 2021 20:29:15 +0100 Subject: [PATCH 001/366] Bumped version to 2021.12.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9da00de9a9a..73261963ce5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __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) From f78e59842d0a2ed57ed92aa74ee71168b665a5cb Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sun, 5 Dec 2021 11:20:40 +0100 Subject: [PATCH 002/366] Fix BMW Connected Drive (#60938) * Bump bimmer_connected to 0.8.5 * Always update HA states after service execution * Fix BMW device tracker & vehicle_finder service * Add charging_end_time sensor * Fix pylint & pytest * Remove unneeded DEFAULT_OPTION * Revert adding charging_end_time & state_attributes * Don't delete option data for CONF_USE_LOCATION * Remove stale string Co-authored-by: rikroe --- .../components/bmw_connected_drive/__init__.py | 14 ++++++++------ .../components/bmw_connected_drive/config_flow.py | 6 +----- .../bmw_connected_drive/device_tracker.py | 4 ++-- .../components/bmw_connected_drive/manifest.json | 2 +- .../components/bmw_connected_drive/strings.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bmw_connected_drive/test_config_flow.py | 10 +++------- 8 files changed, 18 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index a520214ca6a..e681cac8223 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -35,7 +35,6 @@ from .const import ( CONF_ACCOUNT, CONF_ALLOWED_REGIONS, CONF_READ_ONLY, - CONF_USE_LOCATION, DATA_ENTRIES, DATA_HASS_CONFIG, ) @@ -65,7 +64,6 @@ SERVICE_SCHEMA = vol.Schema( DEFAULT_OPTIONS = { CONF_READ_ONLY: False, - CONF_USE_LOCATION: False, } PLATFORMS = [ @@ -215,13 +213,10 @@ def setup_account( password: str = entry.data[CONF_PASSWORD] region: str = entry.data[CONF_REGION] read_only: bool = entry.options[CONF_READ_ONLY] - use_location: bool = entry.options[CONF_USE_LOCATION] _LOGGER.debug("Adding new account %s", name) - pos = ( - (hass.config.latitude, hass.config.longitude) if use_location else (None, None) - ) + pos = (hass.config.latitude, hass.config.longitude) cd_account = BMWConnectedDriveAccount( username, password, region, name, read_only, *pos ) @@ -258,6 +253,13 @@ def setup_account( function_call = getattr(vehicle.remote_services, function_name) function_call() + if call.service in [ + "find_vehicle", + "activate_air_conditioning", + "deactivate_air_conditioning", + ]: + cd_account.update() + if not read_only: # register the remote services for service in _SERVICE_MAP: diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 838c991edb3..3b07830c077 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from . import DOMAIN -from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION +from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY DATA_SCHEMA = vol.Schema( { @@ -115,10 +115,6 @@ class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow): CONF_READ_ONLY, default=self.config_entry.options.get(CONF_READ_ONLY, False), ): bool, - vol.Optional( - CONF_USE_LOCATION, - default=self.config_entry.options.get(CONF_USE_LOCATION, False), - ): bool, } ), ) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index d17920fef0c..0ba2d5012a1 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -35,7 +35,7 @@ async def async_setup_entry( for vehicle in account.account.vehicles: entities.append(BMWDeviceTracker(account, vehicle)) - if not vehicle.status.is_vehicle_tracking_enabled: + if not vehicle.is_vehicle_tracking_enabled: _LOGGER.info( "Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown", vehicle.name, @@ -83,6 +83,6 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): self._attr_extra_state_attributes = self._attrs self._location = ( self._vehicle.status.gps_position - if self._vehicle.status.is_vehicle_tracking_enabled + if self._vehicle.is_vehicle_tracking_enabled else None ) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 95ea2061fb4..fc641548aff 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.8.2"], + "requirements": ["bimmer_connected==0.8.5"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index c0c45b814a4..3e93cccb8c6 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Read-only (only sensors and notify, no execution of services, no lock)", - "use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)" + "read_only": "Read-only (only sensors and notify, no execution of services, no lock)" } } } diff --git a/requirements_all.txt b/requirements_all.txt index e49e0aae85d..840be1149fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ beautifulsoup4==4.10.0 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.2 +bimmer_connected==0.8.5 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82d1aefb1bc..ae996a17fb1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ base36==0.1.1 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.2 +bimmer_connected==0.8.5 # homeassistant.components.blebox blebox_uniapi==1.3.3 diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 6a0bd210387..b0bc3ce292c 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -3,10 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN -from homeassistant.components.bmw_connected_drive.const import ( - CONF_READ_ONLY, - CONF_USE_LOCATION, -) +from homeassistant.components.bmw_connected_drive.const import CONF_READ_ONLY from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from tests.common import MockConfigEntry @@ -28,7 +25,7 @@ FIXTURE_CONFIG_ENTRY = { CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], }, - "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + "options": {CONF_READ_ONLY: False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } @@ -137,14 +134,13 @@ async def test_options_flow_implementation(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + user_input={CONF_READ_ONLY: False}, ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { CONF_READ_ONLY: False, - CONF_USE_LOCATION: False, } assert len(mock_setup.mock_calls) == 1 From 6af9471710fdad43bf4b6686c3c5db93ab95dfa1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 3 Dec 2021 18:01:48 -0500 Subject: [PATCH 003/366] Fix nzbget datetime return value (#60953) --- homeassistant/components/nzbget/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 5bfde7e7c2b..9f94d458f42 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -127,6 +127,6 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): if "UpTimeSec" in sensor_type and value > 0: uptime = utcnow() - timedelta(seconds=value) - return uptime.replace(microsecond=0).isoformat() + return uptime.replace(microsecond=0) return value From 0b9efc2a06398d597b7a52218e6b84ddb70f3335 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 4 Dec 2021 09:34:24 -0700 Subject: [PATCH 004/366] Add missing SimpliSafe service information (#60958) --- homeassistant/components/simplisafe/services.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 273aa02c300..bdd7939a209 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,4 +1,16 @@ # Describes the format for available SimpliSafe services +clear_notifications: + name: Clear notifications + description: Clear any active SimpliSafe notificiations + fields: + device_id: + name: System + description: The system to remove the PIN from + required: true + selector: + device: + integration: simplisafe + model: alarm_control_panel remove_pin: name: Remove PIN description: Remove a PIN by its label or value. From 823e46ea26fd3ddb644bfc1becb0cd6ffbb179fc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 3 Dec 2021 21:05:01 -0700 Subject: [PATCH 005/366] Ensure that inactive RainMachine switch that is toggled on is toggled back off (#60959) --- homeassistant/components/rainmachine/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index ab39ca1a669..5a178718c9b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -277,6 +277,8 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if not self.coordinator.data[self.entity_description.uid]["active"]: + self._attr_is_on = False + self.async_write_ha_state() raise HomeAssistantError( f"Cannot turn on an inactive program/zone: {self.name}" ) From 4023d55229e8b9cee4ed1317472438f49fd7d04c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Dec 2021 00:17:13 -0800 Subject: [PATCH 006/366] Fix statistics registering at start callback (#60963) --- homeassistant/components/statistics/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 23a4a31d936..a931a4cf806 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -243,8 +243,7 @@ class StatisticsSensor(SensorEntity): self._add_state_to_queue(new_state) self.async_schedule_update_ha_state(True) - @callback - def async_stats_sensor_startup(_): + async def async_stats_sensor_startup(_): """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) From 53e2ebc688eb144ba5c2c971b9bcb3833702e845 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 3 Dec 2021 23:24:22 -0800 Subject: [PATCH 007/366] Correctly type the SSDP callback function (#60964) --- homeassistant/components/yeelight/scanner.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 648168ff84a..1756fbe865c 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address, IPv6Address import logging from urllib.parse import urlparse -from async_upnp_client.search import SsdpSearchListener +from async_upnp_client.search import SsdpHeaders, SsdpSearchListener from homeassistant import config_entries from homeassistant.components import network, ssdp @@ -174,10 +174,9 @@ class YeelightScanner: # of another discovery async_call_later(self._hass, 1, _async_start_flow) - async def _async_process_entry(self, response: ssdp.SsdpServiceInfo): + async def _async_process_entry(self, headers: SsdpHeaders): """Process a discovery.""" - _LOGGER.debug("Discovered via SSDP: %s", response) - headers = response.ssdp_headers + _LOGGER.debug("Discovered via SSDP: %s", headers) unique_id = headers["id"] host = urlparse(headers["location"]).hostname current_entry = self._unique_id_capabilities.get(unique_id) From fe46b2664ad2579f1bf9c35b24168550190a6c2b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Dec 2021 00:20:12 -0800 Subject: [PATCH 008/366] Handle invalid device registry entry type (#60966) Co-authored-by: Franck Nijhof --- homeassistant/helpers/device_registry.py | 6 ++++- tests/helpers/test_device_registry.py | 32 ++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c8ae7fd148c..e31b77d3ae2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -172,7 +172,11 @@ class DeviceRegistryStore(storage.Store): # From version 1.1 for device in old_data["devices"]: # Introduced in 0.110 - device["entry_type"] = device.get("entry_type") + try: + device["entry_type"] = DeviceEntryType(device.get("entry_type")) + except ValueError: + device["entry_type"] = None + # Introduced in 0.79 # renamed in 0.95 device["via_device_id"] = device.get("via_device_id") or device.get( diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index a689cc9ac3d..455c90b8f65 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -252,7 +252,19 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "model": "model", "name": "name", "sw_version": "version", - } + }, + # Invalid entry type + { + "config_entries": [None], + "connections": [], + "entry_type": "INVALID_VALUE", + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name": None, + "sw_version": None, + }, ], }, } @@ -300,7 +312,23 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "name_by_user": None, "sw_version": "new_version", "via_device_id": None, - } + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "sw_version": None, + "via_device_id": None, + }, ], "deleted_devices": [], }, From 11b343a513ef7cdc2fdfe85133b39129dce098d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 21:41:58 -1000 Subject: [PATCH 009/366] Fix yeelight name changing to ip address if discovery fails (#60967) --- homeassistant/components/yeelight/__init__.py | 2 +- homeassistant/components/yeelight/device.py | 4 +++- tests/components/yeelight/test_init.py | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 79249ae8c44..d1b8e9d4f46 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -244,7 +244,7 @@ async def _async_get_device( # Set up device bulb = AsyncBulb(host, model=model or None) - device = YeelightDevice(hass, host, entry.options, bulb) + device = YeelightDevice(hass, host, {**entry.options, **entry.data}, bulb) # start listening for local pushes await device.bulb.async_listen(device.async_update_callback) diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py index 5f70866b229..02228e5d9fc 100644 --- a/homeassistant/components/yeelight/device.py +++ b/homeassistant/components/yeelight/device.py @@ -7,7 +7,7 @@ import logging from yeelight import BulbException from yeelight.aio import KEY_CONNECTED -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -199,6 +199,8 @@ class YeelightDevice: elif self.capabilities: # Generate name from model and id when capabilities is available self._name = _async_unique_name(self.capabilities) + elif self.model and (id_ := self._config.get(CONF_ID)): + self._name = f"Yeelight {async_format_model_id(self.model, id_)}" else: self._name = self._host # Default name is host diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 13c71d656bb..dc3d602edeb 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -588,3 +588,25 @@ async def test_non_oserror_exception_on_first_update( await hass.async_block_till_done() assert hass.states.get("light.test_name").state != STATE_UNAVAILABLE + + +async def test_async_setup_with_discovery_not_working(hass: HomeAssistant): + """Test we can setup even if discovery is broken.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1", CONF_ID: ID}, + options={}, + unique_id=ID, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + assert hass.states.get("light.yeelight_color_0x15243f").state == STATE_ON From b5bfa728e9f5a05fd89bfd2176adcf755e0b8817 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 09:16:00 +0100 Subject: [PATCH 010/366] Upgrade luftdaten to 0.7.1 (#60970) --- homeassistant/components/luftdaten/__init__.py | 5 +---- homeassistant/components/luftdaten/config_flow.py | 4 +--- homeassistant/components/luftdaten/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index a87c67620cd..f8a67fff2f3 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -24,7 +24,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -175,11 +174,9 @@ async def async_setup_entry(hass, config_entry): hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) return False - session = async_get_clientsession(hass) - try: luftdaten = LuftDatenData( - Luftdaten(config_entry.data[CONF_SENSOR_ID], hass.loop, session), + Luftdaten(config_entry.data[CONF_SENSOR_ID]), config_entry.data.get(CONF_SENSORS, {}).get( CONF_MONITORED_CONDITIONS, SENSOR_KEYS ), diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index f13fcc831dc..56dee86e9fb 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -13,7 +13,6 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, ) from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -69,8 +68,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if sensor_id in configured_sensors(self.hass): return self._show_form({CONF_SENSOR_ID: "already_configured"}) - session = aiohttp_client.async_get_clientsession(self.hass) - luftdaten = Luftdaten(user_input[CONF_SENSOR_ID], self.hass.loop, session) + luftdaten = Luftdaten(user_input[CONF_SENSOR_ID]) try: await luftdaten.get_data() valid = await luftdaten.validate_sensor() diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index f296093b556..fd355bd8d3c 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -3,7 +3,7 @@ "name": "Luftdaten", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", - "requirements": ["luftdaten==0.6.5"], + "requirements": ["luftdaten==0.7.1"], "codeowners": ["@fabaff"], "quality_scale": "gold", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 840be1149fb..dba86955e03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -966,7 +966,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.5 +luftdaten==0.7.1 # homeassistant.components.lupusec lupupy==0.0.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae996a17fb1..a63eaf354c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -591,7 +591,7 @@ libsoundtouch==0.8 logi_circle==0.2.2 # homeassistant.components.luftdaten -luftdaten==0.6.5 +luftdaten==0.7.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 From 823a4578d777c5e2f098c8182506446257d3fad3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 09:15:26 +0100 Subject: [PATCH 011/366] Upgrade netdata to 1.0.1 (#60971) --- homeassistant/components/netdata/manifest.json | 2 +- homeassistant/components/netdata/sensor.py | 4 +--- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 9d79f54450c..34fbf45c529 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -2,7 +2,7 @@ "domain": "netdata", "name": "Netdata", "documentation": "https://www.home-assistant.io/integrations/netdata", - "requirements": ["netdata==0.2.0"], + "requirements": ["netdata==1.0.1"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index d1fa87a6e5d..3b1e9a0ed47 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -16,7 +16,6 @@ from homeassistant.const import ( PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -61,8 +60,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= port = config.get(CONF_PORT) resources = config.get(CONF_RESOURCES) - session = async_get_clientsession(hass) - netdata = NetdataData(Netdata(host, hass.loop, session, port=port)) + netdata = NetdataData(Netdata(host, port=port)) await netdata.async_update() if netdata.api.metrics is None: diff --git a/requirements_all.txt b/requirements_all.txt index dba86955e03..c3053c32dd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1050,7 +1050,7 @@ ndms2_client==0.1.1 nessclient==0.9.15 # homeassistant.components.netdata -netdata==0.2.0 +netdata==1.0.1 # homeassistant.components.discovery netdisco==3.0.0 From 5a3dd71bde73e03f287b200bc88533e2909eae44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 22:20:56 -1000 Subject: [PATCH 012/366] Fix dimmable effects for flux_led model 0x33 v9+ (#60972) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 60efb52934c..565b560b45e 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.10"], + "requirements": ["flux_led==0.25.12"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index c3053c32dd7..82d64dd8dc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.10 +flux_led==0.25.12 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a63eaf354c3..7e670e0eabb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.10 +flux_led==0.25.12 # homeassistant.components.homekit fnvhash==0.1.0 From 2ba7f9c584f908c441796cb0c70d13c6c8652e94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 22:44:16 -1000 Subject: [PATCH 013/366] Fix flood lights not turning on/off with flux_led (#60973) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 565b560b45e..1f7c84e73d6 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.12"], + "requirements": ["flux_led==0.25.13"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 82d64dd8dc4..dc1c4c8b27d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.12 +flux_led==0.25.13 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e670e0eabb..c1e3655f588 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.12 +flux_led==0.25.13 # homeassistant.components.homekit fnvhash==0.1.0 From 18f36b9c0b8e5e446559fb358f219028e026343f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 4 Dec 2021 10:29:48 +0100 Subject: [PATCH 014/366] Revert metoffice weather daytime (#60978) --- homeassistant/components/metoffice/const.py | 2 -- homeassistant/components/metoffice/weather.py | 9 ++------- tests/components/metoffice/test_weather.py | 11 ----------- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index d1f48eb4f2c..e413b102898 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -24,8 +24,6 @@ DOMAIN = "metoffice" DEFAULT_NAME = "Met Office" ATTRIBUTION = "Data provided by the Met Office" -ATTR_FORECAST_DAYTIME = "daytime" - DEFAULT_SCAN_INTERVAL = timedelta(minutes=15) METOFFICE_COORDINATES = "metoffice_coordinates" diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index d25df1d2654..79363db3667 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -15,7 +15,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import get_device_info from .const import ( - ATTR_FORECAST_DAYTIME, ATTRIBUTION, CONDITION_CLASSES, DEFAULT_NAME, @@ -47,7 +46,7 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep, use_3hourly): +def _build_forecast_data(timestep): data = {} data[ATTR_FORECAST_TIME] = timestep.date.isoformat() if timestep.weather: @@ -60,9 +59,6 @@ def _build_forecast_data(timestep, use_3hourly): data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value if timestep.wind_speed: data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value - if not use_3hourly: - # if it's close to noon, mark as Day, otherwise as Night - data[ATTR_FORECAST_DAYTIME] = abs(timestep.date.hour - 12) < 6 return data @@ -86,7 +82,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): ) self._attr_name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" self._attr_unique_id = hass_data[METOFFICE_COORDINATES] - self._use_3hourly = use_3hourly if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @@ -160,7 +155,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): if self.coordinator.data.forecast is None: return None return [ - _build_forecast_data(timestep, self._use_3hourly) + _build_forecast_data(timestep) for timestep in self.coordinator.data.forecast ] diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 1970217db5b..158e44ca15b 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -181,13 +181,6 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" - assert weather.attributes.get("forecast")[7]["daytime"] is True - - # Check that night entry is correctly marked as Night - assert ( - weather.attributes.get("forecast")[6]["datetime"] == "2020-04-29T00:00:00+00:00" - ) - assert weather.attributes.get("forecast")[6]["daytime"] is False @patch( @@ -263,7 +256,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[18]["temperature"] == 9 assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" - assert "daytime" not in weather.attributes.get("forecast")[18] # Wavertree daily weather platform expected results weather = hass.states.get("weather.met_office_wavertree_daily") @@ -287,7 +279,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" - assert weather.attributes.get("forecast")[7]["daytime"] is True # King's Lynn 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") @@ -312,7 +303,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[18]["temperature"] == 10 assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" - assert "daytime" not in weather.attributes.get("forecast")[18] # King's Lynn daily weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_daily") @@ -336,4 +326,3 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[5]["temperature"] == 11 assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" - assert weather.attributes.get("forecast")[5]["daytime"] is True From dd2e250c66479c7f6311e269e458802131308d7a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 12:33:34 +0100 Subject: [PATCH 015/366] Fix Xiaomi Miio providing strings as timestamps (#60979) --- .../components/xiaomi_miio/sensor.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 0d67014ced9..ccf55a04e17 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -49,6 +49,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import callback +from homeassistant.util import dt as dt_util from . import VacuumCoordinatorDataAttributes from .const import ( @@ -689,14 +690,24 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): def _determine_native_value(self): """Determine native value.""" if self.entity_description.parent_key is not None: - return self._extract_value_from_attribute( + native_value = self._extract_value_from_attribute( getattr(self.coordinator.data, self.entity_description.parent_key), self.entity_description.key, ) + else: + native_value = self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.key + ) - return self._extract_value_from_attribute( - self.coordinator.data, self.entity_description.key - ) + if ( + self.device_class == DEVICE_CLASS_TIMESTAMP + and native_value is not None + and (native_datetime := dt_util.parse_datetime(str(native_value))) + is not None + ): + return native_datetime.astimezone(dt_util.UTC) + + return native_value class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): From af1ad0e6f879eeb4519e2ce39a2ea53026eecc2d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:37:42 +0100 Subject: [PATCH 016/366] Only report deprecated device_state_attributes once (#60980) --- homeassistant/helpers/entity.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 86c8fa86af5..cd04f9db184 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -241,6 +241,9 @@ class Entity(ABC): # If we reported this entity is updated while disabled _disabled_reported = False + # If we reported this entity is using deprecated device_state_attributes + _deprecated_device_state_attributes_reported = False + # Protect for multiple updates _update_staged = False @@ -538,7 +541,10 @@ class Entity(ABC): extra_state_attributes = self.extra_state_attributes # Backwards compatibility for "device_state_attributes" deprecated in 2021.4 # Warning added in 2021.12, will be removed in 2022.4 - if self.device_state_attributes is not None: + if ( + self.device_state_attributes is not None + and not self._deprecated_device_state_attributes_reported + ): report_issue = self._suggest_report_issue() _LOGGER.warning( "Entity %s (%s) implements device_state_attributes. Please %s", @@ -546,6 +552,7 @@ class Entity(ABC): type(self), report_issue, ) + self._deprecated_device_state_attributes_reported = True if extra_state_attributes is None: extra_state_attributes = self.device_state_attributes attr.update(extra_state_attributes or {}) From 6a1dce852e18cc89440f5e61f0f0d14e642b4e21 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:31:34 +0100 Subject: [PATCH 017/366] Fix DSMR Reader providing strings as timestamps (#60988) --- homeassistant/components/dsmr_reader/definitions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 1c719bc890b..4645aef9a7a 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -24,6 +24,7 @@ from homeassistant.const import ( POWER_KILO_WATT, VOLUME_CUBIC_METERS, ) +from homeassistant.util import dt as dt_util PRICE_EUR_KWH: Final = f"EUR/{ENERGY_KILO_WATT_HOUR}" PRICE_EUR_M3: Final = f"EUR/{VOLUME_CUBIC_METERS}" @@ -202,6 +203,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Telegram timestamp", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, + state=dt_util.parse_datetime, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/delivered", @@ -222,6 +224,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Gas meter read", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, + state=dt_util.parse_datetime, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1", From 62a60f1cf69a31c69277f0da645d12bb2bc1fda6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:46:42 +0100 Subject: [PATCH 018/366] Fix str for device registry entry_type warnings caused by core (#60989) --- homeassistant/components/config/entity_registry.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 64cbfd7de1e..d42c5be08fc 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -10,7 +10,10 @@ from homeassistant.components.websocket_api.decorators import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import DISABLED_USER, async_get_registry +from homeassistant.helpers.entity_registry import ( + RegistryEntryDisabler, + async_get_registry, +) async def async_setup(hass): @@ -75,7 +78,12 @@ async def websocket_get_entity(hass, connection, msg): vol.Optional("name"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. - vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), + vol.Optional("disabled_by"): vol.Any( + None, + vol.All( + vol.Coerce(RegistryEntryDisabler), RegistryEntryDisabler.USER.value + ), + ), } ) async def websocket_update_entity(hass, connection, msg): From 0e70121a6fde75dbe93fbaefee10e08385a4b06b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:52:42 +0100 Subject: [PATCH 019/366] Fix typo in state_characteristic warning (#60990) --- homeassistant/components/statistics/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index a931a4cf806..f9ed6863af8 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -62,9 +62,9 @@ STAT_VARIANCE = "variance" STAT_DEFAULT = "default" DEPRECATION_WARNING = ( - "The configuration parameter 'state_characteristics' will become " + "The configuration parameter 'state_characteristic' will become " "mandatory in a future release of the statistics integration. " - "Please add 'state_characteristics: %s' to the configuration of " + "Please add 'state_characteristic: %s' to the configuration of " 'sensor "%s" to keep the current behavior. Read the documentation ' "for further details: " "https://www.home-assistant.io/integrations/statistics/" From 70814130c3b6040b0db6ab2ad03624b3b257fe8f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 4 Dec 2021 18:38:09 +0100 Subject: [PATCH 020/366] Fix translations for binary_sensor tampered device triggers (#60996) --- homeassistant/components/binary_sensor/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index eb97b370105..e2167c24f8a 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -90,8 +90,8 @@ "no_smoke": "{entity_name} stopped detecting smoke", "sound": "{entity_name} started detecting sound", "no_sound": "{entity_name} stopped detecting sound", - "is_tampered": "{entity_name} started detecting tampering", - "is_not_tampered": "{entity_name} stopped detecting tampering", + "tampered": "{entity_name} started detecting tampering", + "not_tampered": "{entity_name} stopped detecting tampering", "update": "{entity_name} got an update available", "no_update": "{entity_name} became up-to-date", "vibration": "{entity_name} started detecting vibration", From f81055dc09366064bb12ce9a0a0248060fcc8d07 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 4 Dec 2021 23:07:28 +0100 Subject: [PATCH 021/366] Add missing local_ip to KNX config flow and options flow (#61018) * Add missing local_ip to KNX config flow and options flow * Update strings --- homeassistant/components/knx/__init__.py | 1 + homeassistant/components/knx/config_flow.py | 8 +++ homeassistant/components/knx/strings.json | 6 +- .../components/knx/translations/en.json | 2 + tests/components/knx/test_config_flow.py | 56 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 3e75c614f1e..ba6689a023d 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -390,6 +390,7 @@ class KNXModule: connection_type=ConnectionType.TUNNELING, gateway_ip=self.config[CONF_HOST], gateway_port=self.config[CONF_PORT], + local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False), auto_reconnect=True, ) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 3fcf4069624..30071752731 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -121,6 +121,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ConnectionSchema.CONF_KNX_ROUTE_BACK: user_input[ ConnectionSchema.CONF_KNX_ROUTE_BACK ], + ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ), CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) @@ -134,6 +137,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False ): vol.Coerce(bool), + vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, } return self.async_show_form( @@ -243,6 +247,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): **DEFAULT_ENTRY_DATA, CONF_HOST: config[CONF_KNX_TUNNELING][CONF_HOST], CONF_PORT: config[CONF_KNX_TUNNELING][CONF_PORT], + ConnectionSchema.CONF_KNX_LOCAL_IP: config[CONF_KNX_TUNNELING].get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ), ConnectionSchema.CONF_KNX_ROUTE_BACK: config[CONF_KNX_TUNNELING][ ConnectionSchema.CONF_KNX_ROUTE_BACK ], @@ -299,6 +306,7 @@ class KNXOptionsFlowHandler(OptionsFlow): vol.Required( CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) ): cv.port, + vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=self.current_config.get( diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index ff191f7a4ce..7f770c25427 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -19,7 +19,8 @@ "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", "individual_address": "Individual address for the connection", - "route_back": "Route Back / NAT Mode" + "route_back": "Route Back / NAT Mode", + "local_ip": "Local IP (leave empty if unsure)" } }, "routing": { @@ -55,7 +56,8 @@ "data": { "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", - "route_back": "Route Back / NAT Mode" + "route_back": "Route Back / NAT Mode", + "local_ip": "Local IP (leave empty if unsure)" } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 80890538fbc..5320f0cfb03 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -12,6 +12,7 @@ "data": { "host": "Host", "individual_address": "Individual address for the connection", + "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" } diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 2b792044fe5..ff1fc362aa5 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -132,6 +132,57 @@ async def test_tunneling_setup(hass: HomeAssistant) -> None: CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: + """Test tunneling if only one gateway is found.""" + gateway = _gateway_descriptor("192.168.0.2", 3675) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "manual_tunnel" + assert not result2["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "Tunneling @ 192.168.0.2" + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } assert len(mock_setup_entry.mock_calls) == 1 @@ -188,6 +239,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, } assert len(mock_setup_entry.mock_calls) == 1 @@ -261,6 +313,7 @@ async def test_import_config_tunneling(hass: HomeAssistant) -> None: CONF_KNX_TUNNELING: { CONF_HOST: "192.168.1.1", CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", ConnectionSchema.CONF_KNX_ROUTE_BACK: True, }, } @@ -284,6 +337,7 @@ async def test_import_config_tunneling(hass: HomeAssistant) -> None: ConnectionSchema.CONF_KNX_STATE_UPDATER: True, ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, } @@ -509,6 +563,7 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) @@ -526,6 +581,7 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } From c4fe3d05f222b0fbd4ee708d702281e4271d0dd4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 00:39:18 -0800 Subject: [PATCH 022/366] Improve nest media source event timestamp display (#61027) Drop subsecond text from the nest media source event timestamp display, using a common date/time template string. --- homeassistant/components/nest/media_source.py | 4 ++-- tests/components/nest/test_media_source.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index e21be20380a..af51c296e43 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -46,7 +46,7 @@ from homeassistant.components.nest.const import DATA_SUBSCRIBER, DOMAIN from homeassistant.components.nest.device_info import NestDeviceInfo from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.helpers.template import DATE_STR_FORMAT _LOGGER = logging.getLogger(__name__) @@ -250,7 +250,7 @@ def _browse_event( media_content_type=MEDIA_TYPE_IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), - event_time=dt_util.as_local(event.timestamp), + event_time=event.timestamp.strftime(DATE_STR_FORMAT), ), can_play=True, can_expand=False, diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 4cda781ebeb..67d6ba2f229 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const from homeassistant.components.media_source.error import Unresolvable from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.template import DATE_STR_FORMAT import homeassistant.util.dt as dt_util from .common import async_setup_sdm_platform @@ -211,7 +212,7 @@ async def test_camera_event(hass, auth, hass_client): assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN assert browse.children[0].identifier == f"{device.id}/{event_id}" - event_timestamp_string = event_timestamp.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Person @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 @@ -291,7 +292,7 @@ async def test_event_order(hass, auth): assert len(browse.children) == 2 assert browse.children[0].domain == DOMAIN assert browse.children[0].identifier == f"{device.id}/{event_id2}" - event_timestamp_string = event_timestamp2.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -299,7 +300,7 @@ async def test_event_order(hass, auth): assert browse.children[1].domain == DOMAIN assert browse.children[1].identifier == f"{device.id}/{event_id1}" - event_timestamp_string = event_timestamp1.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand @@ -435,7 +436,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN actual_event_id = browse.children[0].identifier - event_timestamp_string = event_timestamp.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Event @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 From 4e957b1dbe4665fc459798ecb4066c2dfe4615b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 06:09:18 -1000 Subject: [PATCH 023/366] Fix lutron caseta discovery with newer firmwares (#61029) --- homeassistant/components/lutron_caseta/config_flow.py | 2 +- tests/components/lutron_caseta/test_config_flow.py | 6 +++--- tests/components/lutron_caseta/test_device_trigger.py | 8 -------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index d75fc77c66e..b198d5ddbee 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -68,7 +68,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" hostname = discovery_info.hostname - if hostname is None or not hostname.startswith("lutron-"): + if hostname is None or not hostname.lower().startswith("lutron-"): return self.async_abort(reason="not_lutron_device") self.lutron_id = hostname.split("-")[1].replace(".local.", "") diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index e22d759c1b3..2b947c36982 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -427,7 +427,7 @@ async def test_zeroconf_host_already_configured(hass, tmpdir): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, @@ -454,7 +454,7 @@ async def test_zeroconf_lutron_id_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, @@ -504,7 +504,7 @@ async def test_zeroconf(hass, source, tmpdir): context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 32d6eb3dc5f..23faa929574 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -335,11 +335,3 @@ async def test_validate_trigger_invalid_triggers(hass, device_reg): ] }, ) - - assert ( - len(entity_ids := hass.states.async_entity_ids("persistent_notification")) == 1 - ) - assert ( - "The following integrations and platforms could not be set up" - in hass.states.get(entity_ids[0]).attributes["message"] - ) From 576362bfe175b013dfe5d6a0b93fc8e69588757c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 01:50:47 -0800 Subject: [PATCH 024/366] Bump nest to version 0.4.2 (#61036) --- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 11 ++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 94b2c338528..3a4f64877d2 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.0"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.2"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index af51c296e43..140489bd63a 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -137,7 +137,7 @@ class NestMediaSource(MediaSource): raise Unresolvable( "Unable to find device with identifier: %s" % item.identifier ) - events = _get_events(device) + events = await _get_events(device) if media_id.event_id not in events: raise Unresolvable( "Unable to find event with identifier: %s" % item.identifier @@ -180,7 +180,7 @@ class NestMediaSource(MediaSource): # Browse a specific device and return child events browse_device = _browse_device(media_id, device) browse_device.children = [] - events = _get_events(device) + events = await _get_events(device) for child_event in events.values(): event_id = MediaId(media_id.device_id, child_event.event_id) browse_device.children.append( @@ -189,7 +189,7 @@ class NestMediaSource(MediaSource): return browse_device # Browse a specific event - events = _get_events(device) + events = await _get_events(device) if not (event := events.get(media_id.event_id)): raise BrowseError( "Unable to find event with identiifer: %s" % item.identifier @@ -201,9 +201,10 @@ class NestMediaSource(MediaSource): return await get_media_source_devices(self.hass) -def _get_events(device: Device) -> Mapping[str, ImageEventBase]: +async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" - return OrderedDict({e.event_id: e for e in device.event_media_manager.events}) + events = await device.event_media_manager.async_events() + return OrderedDict({e.event_id: e for e in events}) def _browse_root() -> BrowseMediaSource: diff --git a/requirements_all.txt b/requirements_all.txt index dc1c4c8b27d..74af9b74a0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.0 +google-nest-sdm==0.4.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1e3655f588..2db8378dbfe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.0 +google-nest-sdm==0.4.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 From eaf53c10edc9468a7f9e69f90019a69dce9669e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 5 Dec 2021 09:44:11 -0800 Subject: [PATCH 025/366] Bumped version to 2021.12.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 73261963ce5..f9c4b09944f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __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) From c70f833069561b315b2f2333375ab422324f708c Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 5 Dec 2021 18:48:25 +0100 Subject: [PATCH 026/366] Use STATE_DOCKED for emptying the bin for xiaomi_miio.vacuum (#60513) --- homeassistant/components/xiaomi_miio/vacuum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 60d557837fb..2362fcf8996 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -92,6 +92,7 @@ STATE_CODE_TO_STATE = { 16: STATE_CLEANING, # "Going to target" 17: STATE_CLEANING, # "Zoned cleaning" 18: STATE_CLEANING, # "Segment cleaning" + 22: STATE_DOCKED, # "Emptying the bin" on s7+ 100: STATE_DOCKED, # "Charging complete" 101: STATE_ERROR, # "Device offline" } From 7a4f1c3147dd00c5fc5aed66a76b704924c4417a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 5 Dec 2021 18:51:57 +0100 Subject: [PATCH 027/366] Handle unknown/unavailable state for mobile_app (#60974) --- homeassistant/components/mobile_app/entity.py | 13 ++++++++++++- homeassistant/components/mobile_app/sensor.py | 7 +++++-- tests/components/mobile_app/test_sensor.py | 14 ++++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 03b6d95c2a2..0c26533b7cb 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,6 +1,12 @@ """A entity class for mobile_app.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID +from homeassistant.const import ( + ATTR_ICON, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_WEBHOOK_ID, + STATE_UNAVAILABLE, +) from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -101,6 +107,11 @@ class MobileAppEntity(RestoreEntity): """Return device registry information for this entity.""" return device_info(self._registration) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._config.get(ATTR_SENSOR_STATE) != STATE_UNAVAILABLE + @callback def _handle_update(self, data): """Handle async event updates.""" diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index b58beef96ba..0631f8f72aa 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import ( CONF_WEBHOOK_ID, DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP, + STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.helpers import entity_registry as er @@ -88,9 +89,11 @@ class MobileAppSensor(MobileAppEntity, SensorEntity): @property def native_value(self): """Return the state of the sensor.""" + if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): + return None + if ( - (state := self._config[ATTR_SENSOR_STATE]) is not None - and self.device_class + self.device_class in ( DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP, diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index cfd9efa34c2..295e37ee7d9 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -4,7 +4,7 @@ from http import HTTPStatus import pytest from homeassistant.components.sensor import DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP -from homeassistant.const import PERCENTAGE, STATE_UNKNOWN +from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -89,7 +89,7 @@ async def test_sensor(hass, create_registrations, webhook_client): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() unloaded_entity = hass.states.get("sensor.test_1_battery_state") - assert unloaded_entity.state == "unavailable" + assert unloaded_entity.state == STATE_UNAVAILABLE await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -295,6 +295,16 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client "2021-11-18 20:25:00+01:00", "2021-11-18T19:25:00+00:00", ), + ( + DEVICE_CLASS_TIMESTAMP, + "unavailable", + STATE_UNAVAILABLE, + ), + ( + DEVICE_CLASS_TIMESTAMP, + "unknown", + STATE_UNKNOWN, + ), ], ) async def test_sensor_datetime( From 528d4bc6cecb1762f16386ba60a1cbf0425cde7b Mon Sep 17 00:00:00 2001 From: david reid Date: Sun, 5 Dec 2021 17:50:15 +0000 Subject: [PATCH 028/366] Catch ConnectionResetError (#60987) --- homeassistant/components/hassio/ingress.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 620c69f543d..6935bbdc7da 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -255,3 +255,5 @@ async def _websocket_forward(ws_from, ws_to): await ws_to.close(code=ws_to.close_code, message=msg.extra) except RuntimeError: _LOGGER.debug("Ingress Websocket runtime error") + except ConnectionResetError: + _LOGGER.debug("Ingress Websocket Connection Reset") From 974cc94f873604b11a05676b99e0c369642faa9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 07:48:03 -1000 Subject: [PATCH 029/366] Update flux_led models database to fix turn on for newer models (#61005) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 1f7c84e73d6..dc60a46d68c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.13"], + "requirements": ["flux_led==0.25.16"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 74af9b74a0c..4f0a63e6848 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.13 +flux_led==0.25.16 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2db8378dbfe..3943df51383 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.13 +flux_led==0.25.16 # homeassistant.components.homekit fnvhash==0.1.0 From c67b250be26b0416874418705ebd618043e9991b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:46:48 +0100 Subject: [PATCH 030/366] Fix Hue config flow (#61028) --- homeassistant/components/hue/config_flow.py | 34 +++++++++++---------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9d4bc87889d..a77a2ff101a 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -216,16 +216,17 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not url.hostname: return self.async_abort(reason="not_hue_bridge") - bridge = await self._get_bridge( + # abort if we already have exactly this bridge id/host + # reload the integration if the host got updated + bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) + await self.async_set_unique_id(bridge_id) + self._abort_if_unique_id_configured( + updates={CONF_HOST: url.hostname}, reload_on_update=True + ) + + self.bridge = await self._get_bridge( url.hostname, discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] ) - - await self.async_set_unique_id(bridge.id) - self._abort_if_unique_id_configured( - updates={CONF_HOST: bridge.host}, reload_on_update=False - ) - - self.bridge = bridge return await self.async_step_link() async def async_step_zeroconf( @@ -236,17 +237,18 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the Zeroconf component. It will check if the host is already configured and delegate to the import step if not. """ - bridge = await self._get_bridge( - discovery_info.host, - discovery_info.properties["bridgeid"], - ) - - await self.async_set_unique_id(bridge.id) + # abort if we already have exactly this bridge id/host + # reload the integration if the host got updated + bridge_id = normalize_bridge_id(discovery_info.properties["bridgeid"]) + await self.async_set_unique_id(bridge_id) self._abort_if_unique_id_configured( - updates={CONF_HOST: bridge.host}, reload_on_update=False + updates={CONF_HOST: discovery_info.host}, reload_on_update=True ) - self.bridge = bridge + # we need to query the other capabilities too + self.bridge = await self._get_bridge( + discovery_info.host, discovery_info.properties["bridgeid"] + ) return await self.async_step_link() async def async_step_homekit( From 90442d9e9e26b4b0b5093119590862eb2a14c2ac Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:47:24 +0100 Subject: [PATCH 031/366] Fix Hue migration (#61030) --- homeassistant/components/hue/migration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 5396e646ce1..cf8ff50bfad 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -167,6 +167,9 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N continue v1_id = f"/groups/{ent.unique_id}" hue_group = api.groups.room.get_by_v1_id(v1_id) + if hue_group is None or hue_group.grouped_light is None: + # try again with zone + hue_group = api.groups.zone.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # this may happen if we're looking at some orphaned entity LOGGER.warning( From a976ed2c72e35c59048e26219271174b1a4bd5f0 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:46:05 +0100 Subject: [PATCH 032/366] Add guard for empty mac address in Hue integration (#61037) --- homeassistant/components/hue/migration.py | 7 +++---- homeassistant/components/hue/v2/device.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index cf8ff50bfad..408ba3fc8e0 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -95,13 +95,12 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N # handle entities attached to device for hue_dev in api.devices: zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) - if not zigbee: - # not a zigbee device + if not zigbee or not zigbee.mac_address: + # not a zigbee device or invalid mac continue - mac = zigbee.mac_address # get/update existing device by V1 identifier (mac address) # the device will now have both the old and the new identifier - identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, mac)} + identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, zigbee.mac_address)} hass_dev = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, identifiers=identifiers ) diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 1608743cc48..64bdcc7a4f2 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -49,7 +49,8 @@ async def async_setup_devices(bridge: "HueBridge"): params[ATTR_IDENTIFIERS].add((DOMAIN, api.config.bridge_id)) else: params[ATTR_VIA_DEVICE] = (DOMAIN, api.config.bridge_device.id) - if zigbee := dev_controller.get_zigbee_connectivity(hue_device.id): + zigbee = dev_controller.get_zigbee_connectivity(hue_device.id) + if zigbee and zigbee.mac_address: params[ATTR_CONNECTIONS] = { (device_registry.CONNECTION_NETWORK_MAC, zigbee.mac_address) } From dd95b9b1e47dd7fd55870d88e87e6c072ad06304 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:47:44 +0100 Subject: [PATCH 033/366] Disable options flow for Hue V2 bridges (#61045) --- homeassistant/components/hue/config_flow.py | 11 +++++------ tests/components/hue/test_config_flow.py | 10 +++------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index a77a2ff101a..ceb5a9a1a8e 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -12,7 +12,7 @@ import async_timeout import slugify as unicode_slug import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback @@ -48,7 +48,10 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): config_entry: config_entries.ConfigEntry, ) -> HueOptionsFlowHandler: """Get the options flow for this handler.""" - return HueOptionsFlowHandler(config_entry) + if config_entry.data.get(CONF_API_VERSION, 1) == 1: + # Options for Hue are only applicable to V1 bridges. + return HueOptionsFlowHandler(config_entry) + raise data_entry_flow.UnknownHandler def __init__(self) -> None: """Initialize the Hue flow.""" @@ -292,10 +295,6 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - if self.config_entry.data.get(CONF_API_VERSION, 1) > 1: - # Options for Hue are only applicable to V1 bridges. - return self.async_show_form(step_id="init") - return self.async_show_form( step_id="init", data_schema=vol.Schema( diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 80e1a8909b9..65d3dd696d6 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -7,7 +7,7 @@ from aiohue.errors import LinkButtonNotPressed import pytest import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp, zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect @@ -706,12 +706,8 @@ async def test_options_flow_v2(hass): ) entry.add_to_hass(hass) - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "init" - # V2 bridge does not have config options - assert result["data_schema"] is None + with pytest.raises(data_entry_flow.UnknownHandler): + await hass.config_entries.options.async_init(entry.entry_id) async def test_bridge_zeroconf(hass, aioclient_mock): From 377046bff57aab617682accfecdcb1d8c96068ec Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 09:45:40 -0800 Subject: [PATCH 034/366] Fetch media for events for rendering in the nest media player (#61056) --- homeassistant/components/nest/__init__.py | 8 ++++++++ tests/components/nest/common.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index af3757d31da..fb39188710c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -86,6 +86,11 @@ PLATFORMS = ["sensor", "camera", "climate"] WEB_AUTH_DOMAIN = DOMAIN INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed" +# Fetch media for events with an in memory cache. The largest media items +# are mp4 clips at ~90kb each, so this totals a few MB per camera. +# Note: Media for events can only be published within 30 seconds of the event +EVENT_MEDIA_CACHE_SIZE = 64 + class WebAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): """OAuth implementation using OAuth for web applications.""" @@ -206,6 +211,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False + # Keep media for last N events in memory + subscriber.cache_policy.event_cache_size = EVENT_MEDIA_CACHE_SIZE + subscriber.cache_policy.fetch = True callback = SignalUpdateCallback(hass) subscriber.set_update_callback(callback.async_handle_event) diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index eb44b19d540..a0ba813ab28 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -6,6 +6,7 @@ from unittest.mock import patch from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventMessage +from google_nest_sdm.event_media import CachePolicy from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN @@ -98,6 +99,11 @@ class FakeSubscriber(GoogleNestSubscriber): """Return the fake device manager.""" return self._device_manager + @property + def cache_policy(self) -> CachePolicy: + """Return the cache policy.""" + return self._device_manager.cache_policy + def stop_async(self): """No-op to stop the subscriber.""" return None From 34f728e5d22d888edd7bf89e1b78fa91dfeadb8b Mon Sep 17 00:00:00 2001 From: schreyack Date: Sun, 5 Dec 2021 23:56:59 -0800 Subject: [PATCH 035/366] Fix previous setting briefly appearing on newer flux_led devices when turning on (#60004) Co-authored-by: J. Nick Koston --- homeassistant/components/flux_led/light.py | 9 +- tests/components/flux_led/__init__.py | 2 + tests/components/flux_led/test_light.py | 127 +++++++++++++++++++-- 3 files changed, 126 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index fafa6a0b22e..d364d8b9581 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -280,10 +280,11 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" - if not self.is_on: - await self._device.async_turn_on() - if not kwargs: - return + if self._device.requires_turn_on or not kwargs: + if not self.is_on: + await self._device.async_turn_on() + if not kwargs: + return if MODE_ATTRS.intersection(kwargs): await self._async_set_mode(**kwargs) diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 5b39c5656f6..764c33686b7 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -66,6 +66,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.data_receive_callback = callback bulb.device_type = DeviceType.Bulb + bulb.requires_turn_on = True bulb.async_setup = AsyncMock(side_effect=_save_setup_callback) bulb.effect_list = ["some_effect"] bulb.async_set_custom_pattern = AsyncMock() @@ -115,6 +116,7 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.data_receive_callback = callback switch.device_type = DeviceType.Switch + switch.requires_turn_on = True switch.async_setup = AsyncMock(side_effect=_save_setup_callback) switch.async_stop = AsyncMock() switch.async_update = AsyncMock() diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 4f401197173..6f08ae8a307 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -226,22 +226,19 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.async_set_levels.reset_mock() bulb.async_turn_on.reset_mock() - await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - bulb.async_turn_on.assert_called_once() - bulb.async_turn_on.reset_mock() - await async_mock_device_turn_on(hass, bulb) - assert hass.states.get(entity_id).state == STATE_ON - await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) + # If its off and the device requires the turn on + # command before setting brightness we need to make sure its called + bulb.async_turn_on.assert_called_once() bulb.async_set_brightness.assert_called_with(100) bulb.async_set_brightness.reset_mock() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, @@ -284,6 +281,120 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.async_set_effect.reset_mock() +async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: + """Test an rgb light that does not need the turn on command sent.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.requires_turn_on = False + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgb" + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + + await async_mock_device_turn_off(hass, bulb) + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + + # Should still be called with no kwargs + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON + bulb.async_turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 it means we could not read it because + # an effect is in progress so we use 255 + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(255, 191, 178, brightness=128) + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) + bulb.async_set_effect.reset_mock() + + async def test_rgb_cct_light(hass: HomeAssistant) -> None: """Test an rgb cct light.""" config_entry = MockConfigEntry( From e4d9d0d83e0ca13e154ab13726d8bb9c9681df0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20W?= Date: Mon, 6 Dec 2021 17:49:47 +0100 Subject: [PATCH 036/366] Add media player volume control in `fr-FR` with Alexa (#60489) * media player volume control in `fr-FR` with Alexa * Apply suggestions from code review Co-authored-by: Erik Montnemery --- homeassistant/components/alexa/capabilities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index ea8a1ed8681..0182d2aa085 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -695,6 +695,7 @@ class AlexaSpeaker(AlexaCapability): "en-US", "es-ES", "es-MX", + "fr-FR", # Not documented as of 2021-12-04, see PR #60489 "it-IT", "ja-JP", } @@ -752,6 +753,7 @@ class AlexaStepSpeaker(AlexaCapability): "en-IN", "en-US", "es-ES", + "fr-FR", # Not documented as of 2021-12-04, see PR #60489 "it-IT", } From 056575f491358b9c7b7661352dd12228063c5a0a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 13:30:02 -0800 Subject: [PATCH 037/366] Add debug logging for pip install command (#61057) --- homeassistant/util/package.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 609d09e4f55..a0b5c2832ad 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -93,6 +93,7 @@ def install_package( # Workaround for incompatible prefix setting # See http://stackoverflow.com/a/4495175 args += ["--prefix="] + _LOGGER.debug("Running pip command: args=%s", args) with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process: _, stderr = process.communicate() if process.returncode != 0: From c159790cafecee06432e81d23db93d3024ca3dee Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Dec 2021 11:38:27 -0700 Subject: [PATCH 038/366] Fix mispelling in SimpliSafe service description (#61058) --- homeassistant/components/simplisafe/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index bdd7939a209..3d0965b4b0b 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available SimpliSafe services clear_notifications: name: Clear notifications - description: Clear any active SimpliSafe notificiations + description: Clear any active SimpliSafe notifications fields: device_id: name: System From 0c87885f41ed5e2a2ee10cbdaf8437128b51d9f7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 13:02:37 -0800 Subject: [PATCH 039/366] Fix regression in nest event media player with multiple devices (#61064) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/common.py | 36 +++----- tests/components/nest/test_config_flow_sdm.py | 12 +-- tests/components/nest/test_media_source.py | 82 ++++++++++++++++++- 6 files changed, 97 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 3a4f64877d2..a82f8395733 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.2"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.3"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4f0a63e6848..22554d25fcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.2 +google-nest-sdm==0.4.3 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3943df51383..8d08bbbd92d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.2 +google-nest-sdm==0.4.3 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index a0ba813ab28..35183a441a5 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -53,31 +53,12 @@ def create_config_entry(hass, token_expiration_time=None) -> MockConfigEntry: return config_entry -class FakeDeviceManager(DeviceManager): - """Fake DeviceManager that can supply a list of devices and structures.""" - - def __init__(self, devices: dict, structures: dict): - """Initialize FakeDeviceManager.""" - super().__init__() - self._devices = devices - - @property - def structures(self) -> dict: - """Override structures with fake result.""" - return self._structures - - @property - def devices(self) -> dict: - """Override devices with fake result.""" - return self._devices - - class FakeSubscriber(GoogleNestSubscriber): """Fake subscriber that supplies a FakeDeviceManager.""" - def __init__(self, device_manager: FakeDeviceManager): + def __init__(self): """Initialize Fake Subscriber.""" - self._device_manager = device_manager + self._device_manager = DeviceManager() def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" @@ -121,8 +102,14 @@ async def async_setup_sdm_platform( """Set up the platform and prerequisites.""" if with_config: create_config_entry(hass) - device_manager = FakeDeviceManager(devices=devices, structures=structures) - subscriber = FakeSubscriber(device_manager) + subscriber = FakeSubscriber() + device_manager = await subscriber.async_get_device_manager() + if devices: + for device in devices.values(): + device_manager.add_device(device) + if structures: + for structure in structures.values(): + device_manager.add_structure(structure) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ), patch("homeassistant.components.nest.PLATFORMS", [platform]), patch( @@ -131,4 +118,7 @@ async def async_setup_sdm_platform( ): assert await async_setup_component(hass, DOMAIN, CONFIG) await hass.async_block_till_done() + # Disabled to reduce setup burden, and enabled manually by tests that + # need to exercise this + subscriber.cache_policy.fetch = False return subscriber diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 5d6987f94f7..d4af62cb255 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -17,7 +17,7 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .common import FakeDeviceManager, FakeSubscriber, MockConfigEntry +from .common import FakeSubscriber, MockConfigEntry CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -43,15 +43,9 @@ APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" @pytest.fixture -def device_manager() -> FakeDeviceManager: - """Create FakeDeviceManager.""" - return FakeDeviceManager(devices={}, structures={}) - - -@pytest.fixture -def subscriber(device_manager: FakeDeviceManager) -> FakeSubscriber: +def subscriber() -> FakeSubscriber: """Create FakeSubscriber.""" - return FakeSubscriber(device_manager) + return FakeSubscriber() def get_config_entry(hass): diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 67d6ba2f229..82c87579525 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -81,7 +81,7 @@ async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): return subscriber -def create_event(event_id, event_type, timestamp=None): +def create_event(event_id, event_type, timestamp=None, device_id=None): """Create an EventMessage for a single event type.""" if not timestamp: timestamp = dt_util.now() @@ -91,17 +91,19 @@ def create_event(event_id, event_type, timestamp=None): "eventId": event_id, }, } - return create_event_message(event_id, event_data, timestamp) + return create_event_message(event_id, event_data, timestamp, device_id=device_id) -def create_event_message(event_id, event_data, timestamp): +def create_event_message(event_id, event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" + if device_id is None: + device_id = DEVICE_ID return EventMessage( { "eventId": f"{event_id}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { - "name": DEVICE_ID, + "name": device_id, "events": event_data, }, }, @@ -568,3 +570,75 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin assert response.status == HTTPStatus.UNAUTHORIZED, ( "Response not matched: %s" % response ) + + +async def test_multiple_devices(hass, auth, hass_client): + """Test events received for multiple devices.""" + device_id1 = f"{DEVICE_ID}-1" + device_id2 = f"{DEVICE_ID}-2" + + devices = { + device_id1: Device.MakeDevice( + { + "name": device_id1, + "type": CAMERA_DEVICE_TYPE, + "traits": CAMERA_TRAITS, + }, + auth=auth, + ), + device_id2: Device.MakeDevice( + { + "name": device_id2, + "type": CAMERA_DEVICE_TYPE, + "traits": CAMERA_TRAITS, + }, + auth=auth, + ), + } + subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices) + + device_registry = dr.async_get(hass) + device1 = device_registry.async_get_device({(DOMAIN, device_id1)}) + assert device1 + device2 = device_registry.async_get_device({(DOMAIN, device_id2)}) + assert device2 + + # Very no events have been received yet + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 0 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 0 + + # Send events for device #1 + for i in range(0, 5): + await subscriber.async_receive_event( + create_event(f"event-id-{i}", PERSON_EVENT, device_id=device_id1) + ) + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 5 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 0 + + # Send events for device #2 + for i in range(0, 3): + await subscriber.async_receive_event( + create_event(f"other-id-{i}", PERSON_EVENT, device_id=device_id2) + ) + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 5 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 3 From 23ebde58cdfa81a18d106f1a872fb152ac1c69ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 15:56:35 -1000 Subject: [PATCH 040/366] Bump flux_led to 0.25.17 to fix missing push messages on 0xA3 models (#61070) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index dc60a46d68c..71d8fd350b7 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.16"], + "requirements": ["flux_led==0.25.17"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 22554d25fcc..63585a3c37b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.16 +flux_led==0.25.17 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d08bbbd92d..a37f93b1f87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.16 +flux_led==0.25.17 # homeassistant.components.homekit fnvhash==0.1.0 From a3ede8f8950561c1a286c8ea0b416c14ba14cc96 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 5 Dec 2021 20:36:05 -0500 Subject: [PATCH 041/366] Add 3157100-E model to Centralite thermostat (#61073) --- homeassistant/components/zha/climate.py | 2 +- homeassistant/components/zha/sensor.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 69c1ce35849..9ef7e8fdebc 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -599,7 +599,7 @@ class ZenWithinThermostat(Thermostat): channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN, manufacturers="Centralite", - models="3157100", + models={"3157100", "3157100-E"}, stop_on_match=True, ) class CentralitePearl(ZenWithinThermostat): diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 304a3d155f5..567d2a6065e 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -61,6 +61,7 @@ from .core import discovery from .core.const import ( CHANNEL_ANALOG_INPUT, CHANNEL_ELECTRICAL_MEASUREMENT, + CHANNEL_FAN, CHANNEL_HUMIDITY, CHANNEL_ILLUMINANCE, CHANNEL_LEAF_WETNESS, @@ -636,6 +637,13 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): self.async_write_ha_state() +@MULTI_MATCH( + channel_names=CHANNEL_THERMOSTAT, + aux_channels=CHANNEL_FAN, + manufacturers="Centralite", + models={"3157100", "3157100-E"}, + stop_on_match=True, +) @MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, manufacturers="Zen Within", From ec88a42948ed0bfadf4546edec17b38354a8be47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 18:41:09 -1000 Subject: [PATCH 042/366] Abort flux_led discovery if another device gets the ip (#61074) - If the dhcp reservation expired for the device that was at the ip and a new flux_led device appears we would discover it because the unique_id did not match --- .../components/flux_led/config_flow.py | 5 ++-- tests/components/flux_led/test_config_flow.py | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index ab39e5b8ace..87d07bba2b1 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -115,8 +115,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(mac) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == host and not entry.unique_id: - async_update_entry_from_discovery(self.hass, entry, device) + if entry.data[CONF_HOST] == host: + if not entry.unique_id: + async_update_entry_from_discovery(self.hass, entry, device) return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index af84d3561f7..4c956358818 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -40,6 +40,8 @@ from . import ( from tests.common import MockConfigEntry +MAC_ADDRESS_DIFFERENT = "ff:bb:ff:dd:ee:ff" + async def test_discovery(hass: HomeAssistant): """Test setting up discovery.""" @@ -472,6 +474,34 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( assert config_entry.unique_id == MAC_ADDRESS +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_DISCOVERY, FLUX_DISCOVERY), + ], +) +async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already_configured( + hass, source, data +): + """Test we abort if the host is already configured but the mac does not match.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS_DIFFERENT + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MAC_ADDRESS_DIFFERENT + + async def test_options(hass: HomeAssistant): """Test options flow.""" config_entry = MockConfigEntry( From d5f3e2a761ef5594a1315f8fbc483744bddbc1b9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 00:55:52 -0700 Subject: [PATCH 043/366] Deprecate `system_id` parameter in SimpliSafe service calls (#61076) --- .../components/simplisafe/__init__.py | 138 ++++++++++++------ 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 250b3e6c3b1..cd04de5d34c 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -144,57 +144,86 @@ SERVICES = ( SERVICE_NAME_SET_SYSTEM_PROPERTIES, ) - -SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, - } +SERVICE_CLEAR_NOTIFICATIONS_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) -SERVICE_SET_PIN_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(ATTR_PIN_LABEL): cv.string, - vol.Required(ATTR_PIN_VALUE): cv.string, - } +SERVICE_REMOVE_PIN_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) -SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_ALARM_DURATION): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), - ), - vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), - vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), - vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), - ), - vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(max=MAX_ENTRY_DELAY_HOME), - ), - vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), - ), - vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(max=MAX_EXIT_DELAY_HOME), - ), - vol.Optional(ATTR_LIGHT): cv.boolean, - vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( - vol.In(VOLUME_MAP), VOLUME_MAP.get - ), - } +SERVICE_SET_PIN_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Required(ATTR_PIN_LABEL): cv.string, + vol.Required(ATTR_PIN_VALUE): cv.string, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), +) + +SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Optional(ATTR_ALARM_DURATION): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), + ), + vol.Optional(ATTR_ALARM_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + vol.Optional(ATTR_CHIME_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), + ), + vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_ENTRY_DELAY_HOME), + ), + vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), + ), + vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_EXIT_DELAY_HOME), + ), + vol.Optional(ATTR_LIGHT): cv.boolean, + vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] @@ -216,6 +245,15 @@ def _async_get_system_for_service_call( hass: HomeAssistant, call: ServiceCall ) -> SystemType: """Get the SimpliSafe system related to a service call (by device ID).""" + if ATTR_SYSTEM_ID in call.data: + for entry in hass.config_entries.async_entries(DOMAIN): + simplisafe = hass.data[DOMAIN][entry.entry_id] + if ( + system := simplisafe.systems.get(int(call.data[ATTR_SYSTEM_ID])) + ) is None: + continue + return cast(SystemType, system) + device_id = call.data[ATTR_DEVICE_ID] device_registry = dr.async_get(hass) @@ -365,7 +403,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for service, method, schema in ( - (SERVICE_NAME_CLEAR_NOTIFICATIONS, async_clear_notifications, None), + ( + SERVICE_NAME_CLEAR_NOTIFICATIONS, + async_clear_notifications, + SERVICE_CLEAR_NOTIFICATIONS_SCHEMA, + ), (SERVICE_NAME_REMOVE_PIN, async_remove_pin, SERVICE_REMOVE_PIN_SCHEMA), (SERVICE_NAME_SET_PIN, async_set_pin, SERVICE_SET_PIN_SCHEMA), ( From bd239bcbed005361cd7e8718386834b84d954b81 Mon Sep 17 00:00:00 2001 From: Alexander Pitkin Date: Mon, 6 Dec 2021 11:49:31 +0300 Subject: [PATCH 044/366] Fix yandex transport for Belarus (#61080) --- homeassistant/components/yandex_transport/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 680336fe47b..22872259a6f 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "yandex_transport", "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", - "requirements": ["aioymaps==1.2.1"], + "requirements": ["aioymaps==1.2.2"], "codeowners": ["@rishatik92", "@devbis"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 63585a3c37b..384ab0dccdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.yandex_transport -aioymaps==1.2.1 +aioymaps==1.2.2 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a37f93b1f87..bc1d226d1dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,7 +197,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.yandex_transport -aioymaps==1.2.1 +aioymaps==1.2.2 # homeassistant.components.airly airly==1.1.0 From fa33464217b56cb4385a69649ff3c4cbca652451 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 09:11:44 -0800 Subject: [PATCH 045/366] Remove unnecessary explicit use of OrderedDict in nest media source (#61054) Address follow up PR comments from #60073 --- homeassistant/components/nest/media_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 140489bd63a..4f6cd8147d9 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -18,7 +18,6 @@ https://developers.google.com/nest/device-access/api/camera#handle_camera_events from __future__ import annotations -from collections import OrderedDict from collections.abc import Mapping from dataclasses import dataclass import logging @@ -204,7 +203,7 @@ class NestMediaSource(MediaSource): async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" events = await device.event_media_manager.async_events() - return OrderedDict({e.event_id: e for e in events}) + return {e.event_id: e for e in events} def _browse_root() -> BrowseMediaSource: From f4a38c01902f6cafcc971323cb018981cf080114 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 23:59:24 -0800 Subject: [PATCH 046/366] Coalesce nest media source preview clips by session and bump google-nest-sdm (#61081) --- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/test_events.py | 12 +-- tests/components/nest/test_media_source.py | 74 +++++++++++-------- 7 files changed, 56 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index fb39188710c..0151fb6a6a5 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -197,7 +197,7 @@ class SignalUpdateCallback: "device_id": device_entry.id, "type": event_type, "timestamp": event_message.timestamp, - "nest_event_id": image_event.event_id, + "nest_event_id": image_event.event_session_id, } self._hass.bus.async_fire(NEST_EVENT, message) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index a82f8395733..b9f20e92670 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.3"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.4"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 4f6cd8147d9..8fd7d384e36 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -181,7 +181,7 @@ class NestMediaSource(MediaSource): browse_device.children = [] events = await _get_events(device) for child_event in events.values(): - event_id = MediaId(media_id.device_id, child_event.event_id) + event_id = MediaId(media_id.device_id, child_event.event_session_id) browse_device.children.append( _browse_event(event_id, device, child_event) ) @@ -203,7 +203,7 @@ class NestMediaSource(MediaSource): async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" events = await device.event_media_manager.async_events() - return {e.event_id: e for e in events} + return {e.event_session_id: e for e in events} def _browse_root() -> BrowseMediaSource: diff --git a/requirements_all.txt b/requirements_all.txt index 384ab0dccdb..53b1db5a9e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.3 +google-nest-sdm==0.4.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc1d226d1dc..cf411b32d26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.3 +google-nest-sdm==0.4.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 6e9dd7dd40d..a2f5c21fdac 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -117,7 +117,7 @@ async def test_doorbell_chime_event(hass): "device_id": entry.device_id, "type": "doorbell_chime", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -145,7 +145,7 @@ async def test_camera_motion_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -173,7 +173,7 @@ async def test_camera_sound_event(hass): "device_id": entry.device_id, "type": "camera_sound", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -201,7 +201,7 @@ async def test_camera_person_event(hass): "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -238,13 +238,13 @@ async def test_camera_multiple_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } assert events[1].data == { "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 82c87579525..22ed0721eb2 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -27,6 +27,7 @@ DEVICE_ID = "example/api/device/id" DEVICE_NAME = "Front" PLATFORM = "camera" NEST_EVENT = "nest_event" +EVENT_ID = "1aXEvi9ajKVTdDsXdJda8fzfCa..." EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." CAMERA_DEVICE_TYPE = "sdm.devices.types.CAMERA" CAMERA_TRAITS = { @@ -81,26 +82,28 @@ async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): return subscriber -def create_event(event_id, event_type, timestamp=None, device_id=None): +def create_event( + event_session_id, event_id, event_type, timestamp=None, device_id=None +): """Create an EventMessage for a single event type.""" if not timestamp: timestamp = dt_util.now() event_data = { event_type: { - "eventSessionId": EVENT_SESSION_ID, + "eventSessionId": event_session_id, "eventId": event_id, }, } - return create_event_message(event_id, event_data, timestamp, device_id=device_id) + return create_event_message(event_data, timestamp, device_id=device_id) -def create_event_message(event_id, event_data, timestamp, device_id=None): +def create_event_message(event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" if device_id is None: device_id = DEVICE_ID return EventMessage( { - "eventId": f"{event_id}-{timestamp}", + "eventId": f"{EVENT_ID}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { "name": device_id, @@ -163,7 +166,6 @@ async def test_supported_device(hass, auth): async def test_camera_event(hass, auth, hass_client): """Test a media source and image created for an event.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -172,7 +174,8 @@ async def test_camera_event(hass, auth, hass_client): CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -213,7 +216,7 @@ async def test_camera_event(hass, auth, hass_client): # The device expands recent events assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN - assert browse.children[0].identifier == f"{device.id}/{event_id}" + assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}" event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Person @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -221,19 +224,19 @@ async def test_camera_event(hass, auth, hass_client): # Browse to the event browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) assert browse.domain == DOMAIN - assert browse.identifier == f"{device.id}/{event_id}" + assert browse.identifier == f"{device.id}/{EVENT_SESSION_ID}" assert "Person" in browse.title assert not browse.can_expand assert not browse.children # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) - assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" assert media.mime_type == "image/jpeg" auth.responses = [ @@ -250,9 +253,9 @@ async def test_camera_event(hass, auth, hass_client): async def test_event_order(hass, auth): """Test multiple events are in descending timestamp order.""" - event_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_session_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp1 = dt_util.now() - event_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." + event_session_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) await async_setup_devices( hass, @@ -261,12 +264,14 @@ async def test_event_order(hass, auth): CAMERA_TRAITS, events=[ create_event( - event_id1, + event_session_id1, + EVENT_ID + "1", PERSON_EVENT, timestamp=event_timestamp1, ), create_event( - event_id2, + event_session_id2, + EVENT_ID + "2", MOTION_EVENT, timestamp=event_timestamp2, ), @@ -293,7 +298,7 @@ async def test_event_order(hass, auth): # Motion event is most recent assert len(browse.children) == 2 assert browse.children[0].domain == DOMAIN - assert browse.children[0].identifier == f"{device.id}/{event_id2}" + assert browse.children[0].identifier == f"{device.id}/{event_session_id2}" event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -301,7 +306,7 @@ async def test_event_order(hass, auth): # Person event is next assert browse.children[1].domain == DOMAIN - assert browse.children[1].identifier == f"{device.id}/{event_id1}" + assert browse.children[1].identifier == f"{device.id}/{event_session_id1}" event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand @@ -395,9 +400,12 @@ async def test_resolve_invalid_event_id(hass, auth): async def test_camera_event_clip_preview(hass, auth, hass_client): """Test an event for a battery camera video clip.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() event_data = { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:2", + }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, "previewUrl": "https://127.0.0.1/example", @@ -410,7 +418,6 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): BATTERY_CAMERA_TRAITS, events=[ create_event_message( - event_id, event_data, timestamp=event_timestamp, ), @@ -439,7 +446,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert browse.children[0].domain == DOMAIN actual_event_id = browse.children[0].identifier event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) - assert browse.children[0].title == f"Event @ {event_timestamp_string}" + assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 @@ -490,7 +497,6 @@ async def test_event_media_render_invalid_event_id(hass, auth, hass_client): async def test_event_media_failure(hass, auth, hass_client): """Test event media fetch sees a failure from the server.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -499,7 +505,8 @@ async def test_event_media_failure(hass, auth, hass_client): CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -517,9 +524,9 @@ async def test_event_media_failure(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) - assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" assert media.mime_type == "image/jpeg" auth.responses = [ @@ -535,7 +542,6 @@ async def test_event_media_failure(hass, auth, hass_client): async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin_user): """Test case where user does not have permissions to view media.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -544,7 +550,8 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -560,7 +567,7 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin assert device assert device.name == DEVICE_NAME - media_url = f"/api/nest/event_media/{device.id}/{event_id}" + media_url = f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" # Empty policy with no access to the entity hass_admin_user.mock_policy({}) @@ -616,7 +623,12 @@ async def test_multiple_devices(hass, auth, hass_client): # Send events for device #1 for i in range(0, 5): await subscriber.async_receive_event( - create_event(f"event-id-{i}", PERSON_EVENT, device_id=device_id1) + create_event( + f"event-session-id-{i}", + f"event-id-{i}", + PERSON_EVENT, + device_id=device_id1, + ) ) browse = await media_source.async_browse_media( @@ -631,7 +643,9 @@ async def test_multiple_devices(hass, auth, hass_client): # Send events for device #2 for i in range(0, 3): await subscriber.async_receive_event( - create_event(f"other-id-{i}", PERSON_EVENT, device_id=device_id2) + create_event( + f"other-id-{i}", f"event-id{i}", PERSON_EVENT, device_id=device_id2 + ) ) browse = await media_source.async_browse_media( From 20fb06484cbd05d0d55cd35f31ef92070ed7dffc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:48:17 -1000 Subject: [PATCH 047/366] Bump enphase_envoy to 0.20.1 (#61082) --- homeassistant/components/enphase_envoy/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9e948eaf842..d7ad10ca062 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -3,7 +3,7 @@ "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": [ - "envoy_reader==0.20.0" + "envoy_reader==0.20.1" ], "codeowners": [ "@gtdiehl" @@ -15,4 +15,4 @@ } ], "iot_class": "local_polling" -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 53b1db5a9e4..3f7fa4cbe75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -606,7 +606,7 @@ env_canada==0.5.18 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.20.0 +envoy_reader==0.20.1 # homeassistant.components.season ephem==3.7.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf411b32d26..615d41b663b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -378,7 +378,7 @@ enocean==0.50 env_canada==0.5.18 # homeassistant.components.enphase_envoy -envoy_reader==0.20.0 +envoy_reader==0.20.1 # homeassistant.components.season ephem==3.7.7.0 From e09245eb14dd4a4b0ab7aa2284c757d927f5708b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:53:53 -1000 Subject: [PATCH 048/366] Fix missing unique id in enphase_envoy (#61083) --- .../components/enphase_envoy/__init__.py | 8 ++ .../components/enphase_envoy/config_flow.py | 53 ++++++++--- .../enphase_envoy/test_config_flow.py | 88 +++++++++++++++++++ 3 files changed, 136 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 69c488169a6..7b3765bd25c 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -75,6 +75,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: envoy_reader.get_inverters = False await coordinator.async_config_entry_first_refresh() + if not entry.unique_id: + try: + serial = await envoy_reader.get_full_serial_number() + except httpx.HTTPError: + pass + else: + hass.config_entries.async_update_entry(entry, unique_id=serial) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, NAME: name, diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 0b163e331d6..d1e0febe2e6 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Enphase Envoy integration.""" from __future__ import annotations +import contextlib import logging from typing import Any @@ -31,7 +32,7 @@ ENVOY = "Envoy" CONF_SERIAL = "serial" -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> EnvoyReader: """Validate the user input allows us to connect.""" envoy_reader = EnvoyReader( data[CONF_HOST], @@ -48,6 +49,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, except (RuntimeError, httpx.HTTPError) as err: raise CannotConnect from err + return envoy_reader + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Enphase Envoy.""" @@ -59,7 +62,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ip_address = None self.name = None self.username = None - self.serial = None self._reauth_entry = None @callback @@ -104,8 +106,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - self.serial = discovery_info.properties["serialnum"] - await self.async_set_unique_id(self.serial) + serial = discovery_info.properties["serialnum"] + await self.async_set_unique_id(serial) self.ip_address = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) for entry in self._async_current_entries(include_ignore=False): @@ -114,9 +116,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): and CONF_HOST in entry.data and entry.data[CONF_HOST] == self.ip_address ): - title = f"{ENVOY} {self.serial}" if entry.title == ENVOY else ENVOY + title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY self.hass.config_entries.async_update_entry( - entry, title=title, unique_id=self.serial + entry, title=title, unique_id=serial ) self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) @@ -132,6 +134,24 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_user() + def _async_envoy_name(self) -> str: + """Return the name of the envoy.""" + if self.name: + return self.name + if self.unique_id: + return f"{ENVOY} {self.unique_id}" + return ENVOY + + async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyReader) -> bool: + """Set the unique id by fetching it from the envoy.""" + serial = None + with contextlib.suppress(httpx.HTTPError): + serial = await envoy_reader.get_full_serial_number() + if serial: + await self.async_set_unique_id(serial) + return True + return False + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -145,7 +165,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="already_configured") try: - await validate_input(self.hass, user_input) + envoy_reader = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -155,21 +175,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: data = user_input.copy() - if self.serial: - data[CONF_NAME] = f"{ENVOY} {self.serial}" - else: - data[CONF_NAME] = self.name or ENVOY + data[CONF_NAME] = self._async_envoy_name() + if self._reauth_entry: self.hass.config_entries.async_update_entry( self._reauth_entry, data=data, ) return self.async_abort(reason="reauth_successful") + + if not self.unique_id and await self._async_set_unique_id_from_envoy( + envoy_reader + ): + data[CONF_NAME] = self._async_envoy_name() + + if self.unique_id: + self._abort_if_unique_id_configured({CONF_HOST: data[CONF_HOST]}) + return self.async_create_entry(title=data[CONF_NAME], data=data) - if self.serial: + if self.unique_id: self.context["title_placeholders"] = { - CONF_SERIAL: self.serial, + CONF_SERIAL: self.unique_id, CONF_HOST: self.ip_address, } return self.async_show_form( diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index fc9a7de188e..41a49a7b245 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -23,6 +23,91 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value="1234", + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy 1234" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy 1234", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_no_serial_number(hass: HomeAssistant) -> None: + """Test user setup without a serial number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value=None, + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_fetching_serial_fails(hass: HomeAssistant) -> None: + """Test user setup without a serial number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + side_effect=httpx.HTTPStatusError( + "any", request=MagicMock(), response=MagicMock() + ), ), patch( "homeassistant.components.enphase_envoy.async_setup_entry", return_value=True, @@ -125,6 +210,9 @@ async def test_import(hass: HomeAssistant) -> None: with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value="1234", ), patch( "homeassistant.components.enphase_envoy.async_setup_entry", return_value=True, From 878700e26f02c70e2b0b2a24e085dcbac28669b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:50:45 -1000 Subject: [PATCH 049/366] Provide a hint on which username to use for enphase_envoy (#61084) --- homeassistant/components/enphase_envoy/strings.json | 1 + homeassistant/components/enphase_envoy/translations/en.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index b42f6bfb50f..822ee14fc9e 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -3,6 +3,7 @@ "flow_title": "{serial} ({host})", "step": { "user": { + "description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password.", "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index 2cdb75a6b53..5d4617ed9fa 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -16,7 +16,8 @@ "host": "Host", "password": "Password", "username": "Username" - } + }, + "description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password." } } } From cb371ef27ccb76ad9059770761bca1c39c20716c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 7 Dec 2021 00:26:31 +0100 Subject: [PATCH 050/366] Prevent log flooding in frame helper (#61085) Co-authored-by: epenet --- homeassistant/helpers/frame.py | 9 +++++++++ tests/helpers/test_frame.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 3995f24102d..13ffea48f81 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -12,6 +12,9 @@ from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) +# Keep track of integrations already reported to prevent flooding +_REPORTED_INTEGRATIONS: set[str] = set() + CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name @@ -85,6 +88,12 @@ def report_integration( """ found_frame, integration, path = integration_frame + # Keep track of integrations already reported to prevent flooding + key = f"{found_frame.filename}:{found_frame.lineno}" + if key in _REPORTED_INTEGRATIONS: + return + _REPORTED_INTEGRATIONS.add(key) + index = found_frame.filename.index(path) if path == "custom_components/": extra = " to the custom component author" diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 5e48b2aec5f..37f3e7ec95f 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -1,4 +1,5 @@ """Test the frame helper.""" +# pylint: disable=protected-access from unittest.mock import Mock, patch import pytest @@ -70,3 +71,24 @@ async def test_extract_frame_no_integration(caplog): ], ), pytest.raises(frame.MissingIntegrationFrame): frame.get_integration_frame() + + +@pytest.mark.usefixtures("mock_integration_frame") +@patch.object(frame, "_REPORTED_INTEGRATIONS", set()) +async def test_prevent_flooding(caplog): + """Test to ensure a report is only written once to the log.""" + + what = "accessed hi instead of hello" + key = "/home/paulus/homeassistant/components/hue/light.py:23" + + frame.report(what, error_if_core=False) + assert what in caplog.text + assert key in frame._REPORTED_INTEGRATIONS + assert len(frame._REPORTED_INTEGRATIONS) == 1 + + caplog.clear() + + frame.report(what, error_if_core=False) + assert what not in caplog.text + assert key in frame._REPORTED_INTEGRATIONS + assert len(frame._REPORTED_INTEGRATIONS) == 1 From ef0f3f7ce9f59c78eea93e0c7f75f9a02cff2868 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 6 Dec 2021 17:24:59 +0100 Subject: [PATCH 051/366] Fix migration of entities of Hue integration (#61095) * fix device name in log * Fix Hue migration for all id versions * fix tests * typo * change to bit more universal approach * fix test again * formatting --- homeassistant/components/hue/migration.py | 138 +++++++++++++--------- tests/components/hue/test_migration.py | 54 +++++++-- 2 files changed, 125 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 408ba3fc8e0..9891cc65b0c 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -4,6 +4,7 @@ import logging from aiohue import HueBridgeV2 from aiohue.discovery import is_v2_bridge +from aiohue.v2.models.device import DeviceArchetypes from aiohue.v2.models.resource import ResourceTypes from homeassistant import core @@ -18,7 +19,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ) from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry as devices_for_config_entries, + async_get as async_get_device_registry, +) from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry as entities_for_config_entry, async_entries_for_device, @@ -82,6 +86,18 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N dev_reg = async_get_device_registry(hass) ent_reg = async_get_entity_registry(hass) LOGGER.info("Start of migration of devices and entities to support API schema 2") + + # Create mapping of mac address to HA device id's. + # Identifier in dev reg should be mac-address, + # but in some cases it has a postfix like `-0b` or `-01`. + dev_ids = {} + for hass_dev in devices_for_config_entries(dev_reg, entry.entry_id): + for domain, mac in hass_dev.identifiers: + if domain != DOMAIN: + continue + normalized_mac = mac.split("-")[0] + dev_ids[normalized_mac] = hass_dev.id + # initialize bridge connection just for the migration async with HueBridgeV2(host, api_key, websession) as api: @@ -92,83 +108,93 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE, } - # handle entities attached to device + # migrate entities attached to a device for hue_dev in api.devices: zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) if not zigbee or not zigbee.mac_address: # not a zigbee device or invalid mac continue - # get/update existing device by V1 identifier (mac address) - # the device will now have both the old and the new identifier - identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, zigbee.mac_address)} - hass_dev = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, identifiers=identifiers - ) - LOGGER.info("Migrated device %s (%s)", hass_dev.name, hass_dev.id) - # loop through al entities for device and find match - for ent in async_entries_for_device(ent_reg, hass_dev.id, True): - # migrate light - if ent.entity_id.startswith("light"): - # should always return one lightid here - new_unique_id = next(iter(hue_dev.lights)) - if ent.unique_id == new_unique_id: - continue # just in case - LOGGER.info( - "Migrating %s from unique id %s to %s", - ent.entity_id, - ent.unique_id, - new_unique_id, - ) - ent_reg.async_update_entity( - ent.entity_id, new_unique_id=new_unique_id - ) - continue - # migrate sensors - matched_dev_class = sensor_class_mapping.get( - ent.original_device_class or "unknown" + + # get existing device by V1 identifier (mac address) + if hue_dev.product_data.product_archetype == DeviceArchetypes.BRIDGE_V2: + hass_dev_id = dev_ids.get(api.config.bridge_id.upper()) + else: + hass_dev_id = dev_ids.get(zigbee.mac_address) + if hass_dev_id is None: + # can be safely ignored, this device does not exist in current config + LOGGER.debug( + "Ignoring device %s (%s) as it does not (yet) exist in the device registry", + hue_dev.metadata.name, + hue_dev.id, ) - if matched_dev_class is None: + continue + dev_reg.async_update_device( + hass_dev_id, new_identifiers={(DOMAIN, hue_dev.id)} + ) + LOGGER.info("Migrated device %s (%s)", hue_dev.metadata.name, hass_dev_id) + + # loop through all entities for device and find match + for ent in async_entries_for_device(ent_reg, hass_dev_id, True): + + if ent.entity_id.startswith("light"): + # migrate light + # should always return one lightid here + new_unique_id = next(iter(hue_dev.lights), None) + else: + # migrate sensors + matched_dev_class = sensor_class_mapping.get( + ent.original_device_class or "unknown" + ) + new_unique_id = next( + ( + sensor.id + for sensor in api.devices.get_sensors(hue_dev.id) + if sensor.type == matched_dev_class + ), + None, + ) + + if new_unique_id is None: # this may happen if we're looking at orphaned or unsupported entity LOGGER.warning( "Skip migration of %s because it no longer exists on the bridge", ent.entity_id, ) continue - for sensor in api.devices.get_sensors(hue_dev.id): - if sensor.type != matched_dev_class: - continue - new_unique_id = sensor.id - if ent.unique_id == new_unique_id: - break # just in case + + try: + ent_reg.async_update_entity( + ent.entity_id, new_unique_id=new_unique_id + ) + except ValueError: + # assume edge case where the entity was already migrated in a previous run + # which got aborted somehow and we do not want + # to crash the entire integration init + LOGGER.warning( + "Skip migration of %s because it already exists", + ent.entity_id, + ) + else: LOGGER.info( - "Migrating %s from unique id %s to %s", + "Migrated entity %s from unique id %s to %s", ent.entity_id, ent.unique_id, new_unique_id, ) - try: - ent_reg.async_update_entity( - ent.entity_id, new_unique_id=sensor.id - ) - except ValueError: - # assume edge case where the entity was already migrated in a previous run - # which got aborted somehow and we do not want - # to crash the entire integration init - LOGGER.warning( - "Skip migration of %s because it already exists", - ent.entity_id, - ) - break # migrate entities that are not connected to a device (groups) for ent in entities_for_config_entry(ent_reg, entry.entry_id): if ent.device_id is not None: continue - v1_id = f"/groups/{ent.unique_id}" - hue_group = api.groups.room.get_by_v1_id(v1_id) - if hue_group is None or hue_group.grouped_light is None: - # try again with zone - hue_group = api.groups.zone.get_by_v1_id(v1_id) + if "-" in ent.unique_id: + # handle case where unique id is v2-id of group/zone + hue_group = api.groups.get(ent.unique_id) + else: + # handle case where the unique id is just the v1 id + v1_id = f"/groups/{ent.unique_id}" + hue_group = api.groups.room.get_by_v1_id( + v1_id + ) or api.groups.zone.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # this may happen if we're looking at some orphaned entity LOGGER.warning( diff --git a/tests/components/hue/test_migration.py b/tests/components/hue/test_migration.py index 8457ed04170..2dc1636d485 100644 --- a/tests/components/hue/test_migration.py +++ b/tests/components/hue/test_migration.py @@ -54,12 +54,12 @@ async def test_light_entity_migration( # create device/entity with V1 schema in registry device = dev_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65")}, + identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65-0b")}, ) ent_reg.async_get_or_create( "light", hue.DOMAIN, - "00:17:88:01:09:aa:bb:65", + "00:17:88:01:09:aa:bb:65-0b", suggested_object_id="migrated_light_1", device_id=device.id, ) @@ -74,14 +74,13 @@ async def test_light_entity_migration( ): await hue.migration.handle_v2_migration(hass, config_entry) - # migrated device should have new identifier (guid) and old style (mac) + # migrated device should now have the new identifier (guid) instead of old style (mac) migrated_device = dev_reg.async_get(device.id) assert migrated_device is not None assert migrated_device.identifiers == { - (hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50"), - (hue.DOMAIN, "00:17:88:01:09:aa:bb:65"), + (hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50") } - # the entity should have the new identifier (guid) + # the entity should have the new unique_id (guid) migrated_entity = ent_reg.async_get("light.migrated_light_1") assert migrated_entity is not None assert migrated_entity.unique_id == "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1" @@ -131,14 +130,13 @@ async def test_sensor_entity_migration( ): await hue.migration.handle_v2_migration(hass, config_entry) - # migrated device should have new identifier (guid) and old style (mac) + # migrated device should now have the new identifier (guid) instead of old style (mac) migrated_device = dev_reg.async_get(device.id) assert migrated_device is not None assert migrated_device.identifiers == { - (hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6"), - (hue.DOMAIN, device_mac), + (hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6") } - # the entities should have the correct V2 identifier (guid) + # the entities should have the correct V2 unique_id (guid) for dev_class, platform, new_id in sensor_mappings: migrated_entity = ent_reg.async_get( f"{platform}.hue_migrated_{dev_class}_sensor" @@ -147,7 +145,7 @@ async def test_sensor_entity_migration( assert migrated_entity.unique_id == new_id -async def test_group_entity_migration( +async def test_group_entity_migration_with_v1_id( hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data ): """Test if entity schema for grouped_lights migrates from v1 to v2.""" @@ -156,6 +154,7 @@ async def test_group_entity_migration( ent_reg = er.async_get(hass) # create (deviceless) entity with V1 schema in registry + # using the legacy style group id as unique id ent_reg.async_get_or_create( "light", hue.DOMAIN, @@ -177,3 +176,36 @@ async def test_group_entity_migration( migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light") assert migrated_entity is not None assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34" + + +async def test_group_entity_migration_with_v2_group_id( + hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data +): + """Test if entity schema for grouped_lights migrates from v1 to v2.""" + config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + + ent_reg = er.async_get(hass) + + # create (deviceless) entity with V1 schema in registry + # using the V2 group id as unique id + ent_reg.async_get_or_create( + "light", + hue.DOMAIN, + "6ddc9066-7e7d-4a03-a773-c73937968296", + suggested_object_id="hue_migrated_grouped_light", + config_entry=config_entry, + ) + + # now run the migration and check results + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await hass.async_block_till_done() + with patch( + "homeassistant.components.hue.migration.HueBridgeV2", + return_value=mock_bridge_v2.api, + ): + await hue.migration.handle_v2_migration(hass, config_entry) + + # the entity should have the new identifier (guid) + migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light") + assert migrated_entity is not None + assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34" From 21463121a747ea4e21fbd0675f38e0232d87cdeb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 6 Dec 2021 14:46:53 +0100 Subject: [PATCH 052/366] Improve zwave_js add-on config flow description (#61099) --- homeassistant/components/zwave_js/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 1446c1fc7aa..13f65921cdb 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -22,6 +22,7 @@ }, "configure_addon": { "title": "Enter the Z-Wave JS add-on configuration", + "description": "The add-on will generate security keys if those fields are left empty.", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "s0_legacy_key": "S0 Key (Legacy)", @@ -79,6 +80,7 @@ }, "configure_addon": { "title": "Enter the Z-Wave JS add-on configuration", + "description": "The add-on will generate security keys if those fields are left empty.", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "s0_legacy_key": "S0 Key (Legacy)", From 3ba07ce395f5a8cb8dc28de930d170e1e5fd69ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 6 Dec 2021 18:55:28 +0100 Subject: [PATCH 053/366] Fix CO2 calculation when data is missing (#61106) --- .../components/energy/websocket_api.py | 2 + tests/components/energy/test_websocket_api.py | 193 ++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index e15713ff8ad..cdc7599b55b 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -303,6 +303,8 @@ async def ws_get_fossil_energy_consumption( """Reduce hourly deltas to daily or monthly deltas.""" result: list[dict[str, Any]] = [] deltas: list[float] = [] + if not stat_list: + return result prev_stat: dict[str, Any] = stat_list[0] # Loop over the hourly deltas + a fake entry to end the period diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index f86e43dd1b2..46c6a5c0fa6 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -472,8 +472,10 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client): period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) external_energy_statistics_1 = ( { @@ -575,6 +577,197 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client): period4.isoformat(): pytest.approx(88.0 - 55.0), } + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx(3.0 - 20.0), + period3.isoformat(): pytest.approx(55.0 - 3.0), + period4_day_start.isoformat(): pytest.approx(88.0 - 55.0), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx(3.0 - 20.0), + period3.isoformat(): pytest.approx((55.0 - 3.0) + (88.0 - 55.0)), + } + + +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_no_data(hass, hass_ws_client): + """Test fossil_energy_consumption when there is no data.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": None, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 8, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": None, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 80, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + await async_wait_recording_done_without_instance(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") async def test_fossil_energy_consumption(hass, hass_ws_client): From 325aa66b8ca10ddda36d98a630d43bde84c85b2c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 09:46:17 -0800 Subject: [PATCH 054/366] Bump aiohue to 3.0.2 (#61115) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 02734df1481..c789755c9a3 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.1"], + "requirements": ["aiohue==3.0.2"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3f7fa4cbe75..8022ad374b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.1 +aiohue==3.0.2 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 615d41b663b..88045ab4f19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.1 +aiohue==3.0.2 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 725e3046db81a6285f56c700f1da80d5a0f8e4f5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 10:08:52 -0800 Subject: [PATCH 055/366] Return native timestamps for home connect (#61116) --- homeassistant/components/home_connect/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 373ad6be295..910bec3e6ab 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -63,16 +63,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): elif ( self._state is not None and self._sign == 1 - and dt_util.parse_datetime(self._state) < dt_util.utcnow() + and self._state < dt_util.utcnow() ): # if the date is supposed to be in the future but we're # already past it, set state to None. self._state = None else: seconds = self._sign * float(status[self._key][ATTR_VALUE]) - self._state = ( - dt_util.utcnow() + timedelta(seconds=seconds) - ).isoformat() + self._state = dt_util.utcnow() + timedelta(seconds=seconds) else: self._state = status[self._key].get(ATTR_VALUE) if self._key == BSH_OPERATION_STATE: From d1672a1e9aaa4ddc1eed83d181bf1e6692eeb581 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 7 Dec 2021 00:17:17 +0100 Subject: [PATCH 056/366] Remove colon from default entity name in Hue integration (#61118) --- homeassistant/components/hue/v2/entity.py | 2 +- tests/components/hue/test_binary_sensor.py | 2 +- tests/components/hue/test_sensor_v2.py | 6 +++--- tests/components/hue/test_switch.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 368a6cfe9d0..6dbc959fd9c 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -64,7 +64,7 @@ class HueBaseEntity(Entity): type_title = RESOURCE_TYPE_NAMES.get( self.resource.type, self.resource.type.value.replace("_", " ").title() ) - return f"{dev_name}: {type_title}" + return f"{dev_name} {type_title}" async def async_added_to_hass(self) -> None: """Call when entity is added.""" diff --git a/tests/components/hue/test_binary_sensor.py b/tests/components/hue/test_binary_sensor.py index f1d6a1a8087..ba5a58b4be0 100644 --- a/tests/components/hue/test_binary_sensor.py +++ b/tests/components/hue/test_binary_sensor.py @@ -19,7 +19,7 @@ async def test_binary_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("binary_sensor.hue_motion_sensor_motion") assert sensor is not None assert sensor.state == "off" - assert sensor.name == "Hue motion sensor: Motion" + assert sensor.name == "Hue motion sensor Motion" assert sensor.attributes["device_class"] == "motion" assert sensor.attributes["motion_valid"] is True diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index 2668922590f..256c323ccce 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -22,7 +22,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.hue_motion_sensor_temperature") assert sensor is not None assert sensor.state == "18.1" - assert sensor.attributes["friendly_name"] == "Hue motion sensor: Temperature" + assert sensor.attributes["friendly_name"] == "Hue motion sensor Temperature" assert sensor.attributes["device_class"] == "temperature" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "°C" @@ -32,7 +32,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.hue_motion_sensor_illuminance") assert sensor is not None assert sensor.state == "63" - assert sensor.attributes["friendly_name"] == "Hue motion sensor: Illuminance" + assert sensor.attributes["friendly_name"] == "Hue motion sensor Illuminance" assert sensor.attributes["device_class"] == "illuminance" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "lx" @@ -43,7 +43,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.wall_switch_with_2_controls_battery") assert sensor is not None assert sensor.state == "100" - assert sensor.attributes["friendly_name"] == "Wall switch with 2 controls: Battery" + assert sensor.attributes["friendly_name"] == "Wall switch with 2 controls Battery" assert sensor.attributes["device_class"] == "battery" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "%" diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index 30f4d3634b4..257f1a253c3 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -17,7 +17,7 @@ async def test_switch(hass, mock_bridge_v2, v2_resources_test_data): # test config switch to enable/disable motion sensor test_entity = hass.states.get("switch.hue_motion_sensor_motion") assert test_entity is not None - assert test_entity.name == "Hue motion sensor: Motion" + assert test_entity.name == "Hue motion sensor Motion" assert test_entity.state == "on" assert test_entity.attributes["device_class"] == "switch" From 0532c2206923671a1fb3f90d688010a6c0495cde Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 16:20:59 -0700 Subject: [PATCH 057/366] Bump simplisafe-python to 2021.12.0 (#61121) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 81cb5b7febc..954c39efce1 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2021.11.2"], + "requirements": ["simplisafe-python==2021.12.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8022ad374b4..ac45e00c77c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2149,7 +2149,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.11.2 +simplisafe-python==2021.12.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88045ab4f19..4ef88e0196f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1273,7 +1273,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.11.2 +simplisafe-python==2021.12.0 # homeassistant.components.slack slackclient==2.5.0 From b6d012222a9e7d898fe0a1c1d31e40355bddfdac Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Dec 2021 17:21:28 -0600 Subject: [PATCH 058/366] Improve Sonos activity debug logging (#61122) --- homeassistant/components/sonos/helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index e80d16a491b..74897a618ea 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -40,7 +40,7 @@ def soco_error( return None except (OSError, SoCoException, SoCoUPnPException) as err: error_code = getattr(err, "error_code", None) - function = funct.__name__ + function = funct.__qualname__ if errorcodes and error_code in errorcodes: _LOGGER.debug( "Error code %s ignored in call to %s", error_code, function @@ -59,7 +59,9 @@ def soco_error( return None dispatcher_send( - self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", funct.__name__ + self.hass, + f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", + funct.__qualname__, ) return result From 86f5165e4c6a896d0230515afab7afdad3f43f09 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 16:23:03 -0700 Subject: [PATCH 059/366] Deprecate `entity_id` parameter in Guardian service calls (#61129) --- homeassistant/components/guardian/__init__.py | 75 ++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 892080b9afe..fc7e2dd7be3 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable -from typing import cast +from typing import TYPE_CHECKING, cast from aioguardian import Client from aioguardian.errors import GuardianError @@ -11,6 +11,8 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, CONF_DEVICE_ID, CONF_FILENAME, CONF_IP_ADDRESS, @@ -18,7 +20,11 @@ from homeassistant.const import ( CONF_URL, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -63,20 +69,41 @@ SERVICES = ( SERVICE_NAME_UPGRADE_FIRMWARE, ) -SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_UID): cv.string, - } +SERVICE_BASE_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), ) -SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_URL): cv.url, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_FILENAME): cv.string, - }, +SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(CONF_UID): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), +) + +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_URL): cv.url, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_FILENAME): cv.string, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), ) @@ -86,6 +113,14 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] @callback def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: """Get the entry ID related to a service call (by device ID).""" + if ATTR_ENTITY_ID in call.data: + entity_registry = er.async_get(hass) + entity_registry_entry = entity_registry.async_get(call.data[ATTR_ENTITY_ID]) + if TYPE_CHECKING: + assert entity_registry_entry + assert entity_registry_entry.config_entry_id + return entity_registry_entry.config_entry_id + device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) @@ -221,15 +256,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for service_name, schema, method in ( - (SERVICE_NAME_DISABLE_AP, None, async_disable_ap), - (SERVICE_NAME_ENABLE_AP, None, async_enable_ap), + (SERVICE_NAME_DISABLE_AP, SERVICE_BASE_SCHEMA, async_disable_ap), + (SERVICE_NAME_ENABLE_AP, SERVICE_BASE_SCHEMA, async_enable_ap), ( SERVICE_NAME_PAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, async_pair_sensor, ), - (SERVICE_NAME_REBOOT, None, async_reboot), - (SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, None, async_reset_valve_diagnostics), + (SERVICE_NAME_REBOOT, SERVICE_BASE_SCHEMA, async_reboot), + ( + SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, + SERVICE_BASE_SCHEMA, + async_reset_valve_diagnostics, + ), ( SERVICE_NAME_UNPAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, From 348079f069534711315f7a867a7021714d031694 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 15:51:03 -0800 Subject: [PATCH 060/366] Bump frontend to 20211206.0 (#61133) --- 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 1ca97e4cdd8..6d419276029 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==20211203.0" + "home-assistant-frontend==20211206.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 029f889fd85..5082fe5559d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index ac45e00c77c..1ac6768455d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ef88e0196f..6daf9a62177 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From d105e9f99e35f143844fb79241dd7531073c84c8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 15:54:09 -0800 Subject: [PATCH 061/366] Bumped version to 2021.12.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f9c4b09944f..de6fbd815e4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __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) From 01adc6a042050a8a0ae532a64fa9dbb9bc634831 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 6 Dec 2021 18:01:46 +0100 Subject: [PATCH 062/366] Improve code quality trafikverket_weatherstation (#61044) * Code quality trafikverket_weatherstation * Updates from review * Fix extra attributes settings * Fix for additional review comments --- .../trafikverket_weatherstation/sensor.py | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 46fa3d9a5bd..01b70d5d3c7 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -5,13 +5,15 @@ import asyncio from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any import aiohttp -from pytrafikverket.trafikverket_weather import TrafikverketWeather +from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) @@ -70,6 +72,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="road_temp", @@ -78,12 +81,14 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation", api_key="precipitationtype", name="Precipitation type", icon="mdi:weather-snowy-rainy", + entity_registry_enabled_default=False, ), TrafikverketSensorEntityDescription( key="wind_direction", @@ -91,6 +96,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind direction", native_unit_of_measurement=DEGREE, icon="mdi:flag-triangle", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_direction_text", @@ -104,6 +110,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind speed", native_unit_of_measurement=SPEED_METERS_PER_SECOND, icon="mdi:weather-windy", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_speed_max", @@ -111,6 +118,8 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind speed max", native_unit_of_measurement=SPEED_METERS_PER_SECOND, icon="mdi:weather-windy-variant", + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="humidity", @@ -119,6 +128,8 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", device_class=DEVICE_CLASS_HUMIDITY, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amount", @@ -126,18 +137,20 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Precipitation amount", native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:cup-water", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amountname", api_key="precipitation_amountname", name="Precipitation name", icon="mdi:weather-pouring", + entity_registry_enabled_default=False, ), ) SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string, @@ -172,17 +185,12 @@ async def async_setup_entry( ) -> None: """Set up the Trafikverket sensor entry.""" - sensor_name = entry.data[CONF_STATION] - sensor_api = entry.data[CONF_API_KEY] - sensor_station = entry.data[CONF_STATION] - web_session = async_get_clientsession(hass) - - weather_api = TrafikverketWeather(web_session, sensor_api) + weather_api = TrafikverketWeather(web_session, entry.data[CONF_API_KEY]) entities = [ TrafikverketWeatherStation( - weather_api, sensor_name, sensor_station, description + weather_api, entry.entry_id, entry.data[CONF_STATION], description ) for description in SENSOR_TYPES ] @@ -197,29 +205,36 @@ class TrafikverketWeatherStation(SensorEntity): def __init__( self, - weather_api, - name, - sensor_station, + weather_api: TrafikverketWeather, + entry_id: str, + sensor_station: str, description: TrafikverketSensorEntityDescription, - ): + ) -> None: """Initialize the sensor.""" self.entity_description = description - self._attr_name = f"{name} {description.name}" + self._attr_name = f"{sensor_station} {description.name}" + self._attr_unique_id = f"{entry_id}_{description.key}" self._station = sensor_station self._weather_api = weather_api - self._weather = None + self._weather: WeatherStationInfo | None = None + self._active: bool | None = None + self._measure_time: str | None = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of Trafikverket Weatherstation.""" - return { + _additional_attributes: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_ACTIVE: self._weather.active, - ATTR_MEASURE_TIME: self._weather.measure_time, } + if self._active: + _additional_attributes[ATTR_ACTIVE] = self._active + if self._measure_time: + _additional_attributes[ATTR_MEASURE_TIME] = self._measure_time + + return _additional_attributes @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Trafikverket and updates the states.""" try: self._weather = await self._weather_api.async_get_weather(self._station) @@ -228,3 +243,6 @@ class TrafikverketWeatherStation(SensorEntity): ) except (asyncio.TimeoutError, aiohttp.ClientError, ValueError) as error: _LOGGER.error("Could not fetch weather data: %s", error) + return + self._active = self._weather.active + self._measure_time = self._weather.measure_time From 8da375660296a28479c471d1ebaa219bbdfe99fc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Dec 2021 08:07:31 +0100 Subject: [PATCH 063/366] Bump hatasmota to 0.3.1 (#61120) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_cover.py | 30 +++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 9ea06bea545..bd30231396f 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.3.0"], + "requirements": ["hatasmota==0.3.1"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 1ac6768455d..7a74a9a7575 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -792,7 +792,7 @@ hass-nabucasa==0.50.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.3.0 +hatasmota==0.3.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6daf9a62177..0c881de605e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -497,7 +497,7 @@ hangups==0.4.14 hass-nabucasa==0.50.0 # homeassistant.components.tasmota -hatasmota==0.3.0 +hatasmota==0.3.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 54ac192f7c1..c036f490f6d 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -9,6 +9,7 @@ from hatasmota.utils import ( get_topic_tele_sensor, get_topic_tele_will, ) +import pytest from homeassistant.components import cover from homeassistant.components.tasmota.const import DEFAULT_PREFIX @@ -34,6 +35,35 @@ async def test_missing_relay(hass, mqtt_mock, setup_tasmota): """Test no cover is discovered if relays are missing.""" +@pytest.mark.parametrize( + "relay_config, num_covers", + [ + ([3, 3, 3, 3, 3, 3, 1, 1, 3, 3], 4), + ([3, 3, 3, 3, 0, 0, 0, 0], 2), + ([3, 3, 1, 1, 0, 0, 0, 0], 1), + ([3, 3, 3, 1, 0, 0, 0, 0], 0), + ], +) +async def test_multiple_covers( + hass, mqtt_mock, setup_tasmota, relay_config, num_covers +): + """Test discovery of multiple covers.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"] = relay_config + mac = config["mac"] + + assert len(hass.states.async_all("cover")) == 0 + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all("cover")) == num_covers + + async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) From fa447332c61b9600f503046be563ca8fab630225 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Tue, 7 Dec 2021 09:00:30 +0100 Subject: [PATCH 064/366] Fix point availability (#61144) --- homeassistant/components/point/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index dee0ae1a492..5cbab59eabf 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -185,7 +185,7 @@ class MinutPointClient: async def _sync(self): """Update local list of devices.""" - if not await self._client.update() and self._is_available: + if not await self._client.update(): self._is_available = False _LOGGER.warning("Device is unavailable") async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) From 4ad904f3b7a140d106f8fd17a301ea8186cd8e83 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 7 Dec 2021 21:50:34 +0100 Subject: [PATCH 065/366] Change check for existence of options flow (#61147) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/config/config_entries.py | 10 ++++------ homeassistant/components/hue/config_flow.py | 15 ++++++++++----- homeassistant/config_entries.py | 6 ++++++ tests/components/config/test_config_entries.py | 8 +++++++- tests/components/hue/test_config_flow.py | 5 ++--- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b1f686e23a4..61df9dc190d 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -366,13 +366,11 @@ async def ignore_config_flow(hass, connection, msg): def entry_json(entry: config_entries.ConfigEntry) -> dict: """Return JSON value of a config entry.""" handler = config_entries.HANDLERS.get(entry.domain) - supports_options = ( - # Guard in case handler is no longer registered (custom component etc) - handler is not None - # pylint: disable=comparison-with-callable - and handler.async_get_options_flow - != config_entries.ConfigFlow.async_get_options_flow + # work out if handler has support for options flow + supports_options = handler is not None and handler.async_supports_options_flow( + entry ) + return { "entry_id": entry.entry_id, "domain": entry.domain, diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index ceb5a9a1a8e..49fca2158d5 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -12,7 +12,7 @@ import async_timeout import slugify as unicode_slug import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback @@ -48,10 +48,15 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): config_entry: config_entries.ConfigEntry, ) -> HueOptionsFlowHandler: """Get the options flow for this handler.""" - if config_entry.data.get(CONF_API_VERSION, 1) == 1: - # Options for Hue are only applicable to V1 bridges. - return HueOptionsFlowHandler(config_entry) - raise data_entry_flow.UnknownHandler + return HueOptionsFlowHandler(config_entry) + + @classmethod + @callback + def async_supports_options_flow( + cls, config_entry: config_entries.ConfigEntry + ) -> bool: + """Return options flow support for this handler.""" + return config_entry.data.get(CONF_API_VERSION, 1) == 1 def __init__(self) -> None: """Initialize the Hue flow.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 45bc10f5774..cdea9da2540 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1163,6 +1163,12 @@ class ConfigFlow(data_entry_flow.FlowHandler): """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler + @classmethod + @callback + def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: + """Return options flow support for this handler.""" + return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow + @callback def _async_abort_entries_match( self, match_dict: dict[str, Any] | None = None diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 8b890148d51..20a19495597 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -47,10 +47,16 @@ async def test_get_entries(hass, client): @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): """Get options flow.""" pass + @classmethod + @callback + def async_supports_options_flow(cls, config_entry): + """Return options flow support for this handler.""" + return True + hass.helpers.config_entry_flow.register_discovery_flow( "comp2", "Comp 2", lambda: None ) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 65d3dd696d6..6ce8ff3e1c4 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -7,7 +7,7 @@ from aiohue.errors import LinkButtonNotPressed import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect @@ -706,8 +706,7 @@ async def test_options_flow_v2(hass): ) entry.add_to_hass(hass) - with pytest.raises(data_entry_flow.UnknownHandler): - await hass.config_entries.options.async_init(entry.entry_id) + assert config_flow.HueFlowHandler.async_supports_options_flow(entry) is False async def test_bridge_zeroconf(hass, aioclient_mock): From 78ada630c002c031bf5647a21f2dbea0e904f194 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Dec 2021 12:44:50 +0100 Subject: [PATCH 066/366] Guard against missing states in Alexa state updates (#61152) --- homeassistant/components/alexa/state_report.py | 9 +++++---- tests/components/alexa/test_state_report.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 1ab12041e32..767bfa18224 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -182,12 +182,13 @@ async def async_send_add_or_update_message(hass, config, entity_ids): endpoints = [] for entity_id in entity_ids: - domain = entity_id.split(".", 1)[0] - - if domain not in ENTITY_ADAPTERS: + if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS: continue - alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) + if (state := hass.states.get(entity_id)) is None: + continue + + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state) endpoints.append(alexa_entity.serialize_discovery()) payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index bd91dc8f846..29624e7d1ff 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -117,10 +117,18 @@ async def test_send_add_or_update_message(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_send_add_or_update_message( - hass, DEFAULT_CONFIG, ["binary_sensor.test_contact", "zwave.bla"] + hass.states.async_set( + "zwave.bla", + "wow_such_unsupported", ) + entities = [ + "binary_sensor.test_contact", + "binary_sensor.non_existing", # Supported, but does not exist + "zwave.bla", # Unsupported + ] + await state_report.async_send_add_or_update_message(hass, DEFAULT_CONFIG, entities) + assert len(aioclient_mock.mock_calls) == 1 call = aioclient_mock.mock_calls From 816b5af883a2566fc502134c2c54987d463edfb8 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 7 Dec 2021 13:56:31 +0100 Subject: [PATCH 067/366] Fix Netatmo climate issue (#61154) Signed-off-by: cgtobi --- homeassistant/components/netatmo/climate.py | 11 +- .../components/netatmo/data_handler.py | 6 +- homeassistant/components/netatmo/select.py | 8 ++ .../netatmo/fixtures/homesdata.json | 102 ++++++++++++++++-- .../homestatus_111111111111111111111401.json | 4 + 5 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 49e02f566a0..1ead9d7cbdb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -135,9 +135,14 @@ async def async_setup_entry( entities = [] for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) + + try: + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id + ) + except KeyError: + continue + climate_state = data_handler.data[signal_name] climate_topology.register_handler(home_id, climate_state.process_topology) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index c62522a931a..7a97ec3748f 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -194,7 +194,11 @@ class NetatmoDataHandler: self._auth, **kwargs ) - await self.async_fetch_data(data_class_entry) + try: + await self.async_fetch_data(data_class_entry) + except KeyError: + self.data_classes.pop(data_class_entry) + raise self._queue.append(self.data_classes[data_class_entry]) _LOGGER.debug("Data class %s added", data_class_entry) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 9902155be73..98576497f3e 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -48,6 +48,14 @@ async def async_setup_entry( entities = [] for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" + + try: + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id + ) + except KeyError: + continue + await data_handler.register_data_class( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index fd63a0c200f..8c6587ca973 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -5,7 +5,10 @@ "id": "91763b24c43d3e344f424e8b", "name": "MYHOME", "altitude": 112, - "coordinates": [52.516263, 13.377726], + "coordinates": [ + 52.516263, + 13.377726 + ], "country": "DE", "timezone": "Europe/Berlin", "rooms": [ @@ -13,25 +16,33 @@ "id": "2746182631", "name": "Livingroom", "type": "livingroom", - "module_ids": ["12:34:56:00:01:ae"] + "module_ids": [ + "12:34:56:00:01:ae" + ] }, { "id": "3688132631", "name": "Hall", "type": "custom", - "module_ids": ["12:34:56:00:f1:62"] + "module_ids": [ + "12:34:56:00:f1:62" + ] }, { "id": "2833524037", "name": "Entrada", "type": "lobby", - "module_ids": ["12:34:56:03:a5:54"] + "module_ids": [ + "12:34:56:03:a5:54" + ] }, { "id": "2940411577", "name": "Cocina", "type": "kitchen", - "module_ids": ["12:34:56:03:a0:ac"] + "module_ids": [ + "12:34:56:03:a0:ac" + ] } ], "modules": [ @@ -388,6 +399,85 @@ } ], "therm_mode": "schedule" + }, + { + "id": "111111111111111111111401", + "name": "Home with no modules", + "altitude": 9, + "coordinates": [ + 1.23456789, + 50.0987654 + ], + "country": "BE", + "timezone": "Europe/Brussels", + "rooms": [ + { + "id": "1111111401", + "name": "Livingroom", + "type": "livingroom" + } + ], + "temperature_control_mode": "heating", + "therm_mode": "away", + "therm_setpoint_default_duration": 120, + "cooling_mode": "schedule", + "schedules": [ + { + "away_temp": 14, + "hg_temp": 7, + "name": "Week", + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 6, + "m_offset": 420 + } + ], + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [], + "id": 0, + "rooms": [] + }, + { + "type": 1, + "name": "Nacht", + "rooms_temp": [], + "id": 1, + "rooms": [] + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [], + "id": 4, + "rooms": [] + }, + { + "type": 4, + "name": "Tussenin", + "rooms_temp": [], + "id": 5, + "rooms": [] + }, + { + "type": 4, + "name": "Ochtend", + "rooms_temp": [], + "id": 6, + "rooms": [] + } + ], + "id": "700000000000000000000401", + "selected": true, + "type": "therm" + } + ] } ], "user": { @@ -404,4 +494,4 @@ "status": "ok", "time_exec": 0.056135892868042, "time_server": 1559171003 -} +} \ No newline at end of file diff --git a/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json b/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json new file mode 100644 index 00000000000..2ae65dc0d21 --- /dev/null +++ b/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json @@ -0,0 +1,4 @@ +{ + "status": "ok", + "time_server": 1638873670 +} \ No newline at end of file From 13ce6edc6870f60190e89e30f9643a9aa2cb87d8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 7 Dec 2021 12:29:54 -0700 Subject: [PATCH 068/366] Bump py17track to 2021.12.2 (#61166) --- homeassistant/components/seventeentrack/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 05f240043a9..01fdb22395c 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -2,7 +2,7 @@ "domain": "seventeentrack", "name": "17TRACK", "documentation": "https://www.home-assistant.io/integrations/seventeentrack", - "requirements": ["py17track==2021.12.1"], + "requirements": ["py17track==2021.12.2"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7a74a9a7575..cf6b78a83b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1305,7 +1305,7 @@ py-synologydsm-api==1.0.4 py-zabbix==1.1.7 # homeassistant.components.seventeentrack -py17track==2021.12.1 +py17track==2021.12.2 # homeassistant.components.hdmi_cec pyCEC==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c881de605e..38e8fcf29f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -792,7 +792,7 @@ py-nightscout==1.2.2 py-synologydsm-api==1.0.4 # homeassistant.components.seventeentrack -py17track==2021.12.1 +py17track==2021.12.2 # homeassistant.components.control4 pyControl4==0.0.6 From fecfbba44242d93ac37f0a679a062318b12b441f Mon Sep 17 00:00:00 2001 From: einarhauks Date: Tue, 7 Dec 2021 19:33:24 +0000 Subject: [PATCH 069/366] Display energy in wh instead of kWh (#61169) --- .../components/tesla_wall_connector/sensor.py | 10 +++++----- tests/components/tesla_wall_connector/test_sensor.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index b2353681291..8219d121ae3 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, TEMP_CELSIUS, @@ -120,10 +120,10 @@ WALL_CONNECTOR_SENSORS = [ entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), WallConnectorSensorDescription( - key="total_energy_kWh", - name=prefix_entity_name("Total Energy"), - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh / 1000.0, + key="energy_kWh", + name=prefix_entity_name("Energy"), + native_unit_of_measurement=ENERGY_WATT_HOUR, + value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh, state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_ENERGY, ), diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index 6763f685441..0cafc15c6f1 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -24,7 +24,7 @@ async def test_sensors(hass: HomeAssistant) -> None: "sensor.tesla_wall_connector_grid_frequency", "50.021", "49.981" ), EntityAndExpectedValues( - "sensor.tesla_wall_connector_total_energy", "988.022", "989.0" + "sensor.tesla_wall_connector_energy", "988022", "989000" ), EntityAndExpectedValues( "sensor.tesla_wall_connector_phase_a_current", "10", "7" From e09c85c5912d10fa7f2eea44dda3372f7d147653 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 7 Dec 2021 11:30:23 -0800 Subject: [PATCH 070/366] Bump nest to 0.4.5 to fix media player event expiration (#61174) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index b9f20e92670..11a464dbaf1 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.4"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.5"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index cf6b78a83b8..11392cc4ac7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.4 +google-nest-sdm==0.4.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38e8fcf29f0..e8c46b255f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.4 +google-nest-sdm==0.4.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 61545edd963b5116f97bec5ab44d5f0d327ab85f Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 7 Dec 2021 13:47:44 -0500 Subject: [PATCH 071/366] Remove loopenergy integration (#61175) * Remove loopenergy integration * Fix requirements_all.txt * Fix requirements_test_all.txt --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/loopenergy/__init__.py | 1 - .../components/loopenergy/manifest.json | 8 - homeassistant/components/loopenergy/sensor.py | 149 ------------------ requirements_all.txt | 3 - 6 files changed, 163 deletions(-) delete mode 100644 homeassistant/components/loopenergy/__init__.py delete mode 100644 homeassistant/components/loopenergy/manifest.json delete mode 100644 homeassistant/components/loopenergy/sensor.py diff --git a/.coveragerc b/.coveragerc index fc033c77369..05bb880c2f2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -597,7 +597,6 @@ omit = homeassistant/components/lookin/models.py homeassistant/components/lookin/sensor.py homeassistant/components/lookin/climate.py - homeassistant/components/loopenergy/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/__init__.py homeassistant/components/luftdaten/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 513115a9953..f7333867069 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -293,7 +293,6 @@ homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lookin/* @ANMalko -homeassistant/components/loopenergy/* @pavoni homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @mzdrale homeassistant/components/luftdaten/* @fabaff diff --git a/homeassistant/components/loopenergy/__init__.py b/homeassistant/components/loopenergy/__init__.py deleted file mode 100644 index 4e963f2828a..00000000000 --- a/homeassistant/components/loopenergy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The loopenergy component.""" diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json deleted file mode 100644 index 01a18dc01db..00000000000 --- a/homeassistant/components/loopenergy/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "loopenergy", - "name": "Loop Energy", - "documentation": "https://www.home-assistant.io/integrations/loopenergy", - "requirements": ["pyloopenergy==0.2.1"], - "codeowners": ["@pavoni"], - "iot_class": "cloud_push" -} diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py deleted file mode 100644 index 05d7f79ebfd..00000000000 --- a/homeassistant/components/loopenergy/sensor.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Support for Loop Energy sensors.""" -import logging - -import pyloopenergy -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, - EVENT_HOMEASSISTANT_STOP, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_ELEC = "electricity" -CONF_GAS = "gas" - -CONF_ELEC_SERIAL = "electricity_serial" -CONF_ELEC_SECRET = "electricity_secret" - -CONF_GAS_SERIAL = "gas_serial" -CONF_GAS_SECRET = "gas_secret" -CONF_GAS_CALORIFIC = "gas_calorific" - -CONF_GAS_TYPE = "gas_type" - -DEFAULT_CALORIFIC = 39.11 -DEFAULT_UNIT = "kW" - -ELEC_SCHEMA = vol.Schema( - { - vol.Required(CONF_ELEC_SERIAL): cv.string, - vol.Required(CONF_ELEC_SECRET): cv.string, - } -) - -GAS_TYPE_SCHEMA = vol.In([CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]) - -GAS_SCHEMA = vol.Schema( - { - vol.Required(CONF_GAS_SERIAL): cv.string, - vol.Required(CONF_GAS_SECRET): cv.string, - vol.Optional(CONF_GAS_TYPE, default=CONF_UNIT_SYSTEM_METRIC): GAS_TYPE_SCHEMA, - vol.Optional(CONF_GAS_CALORIFIC, default=DEFAULT_CALORIFIC): vol.Coerce(float), - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_ELEC): ELEC_SCHEMA, vol.Optional(CONF_GAS): GAS_SCHEMA} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Loop Energy sensors.""" - elec_config = config.get(CONF_ELEC) - gas_config = config.get(CONF_GAS, {}) - - controller = pyloopenergy.LoopEnergy( - elec_config.get(CONF_ELEC_SERIAL), - elec_config.get(CONF_ELEC_SECRET), - gas_config.get(CONF_GAS_SERIAL), - gas_config.get(CONF_GAS_SECRET), - gas_config.get(CONF_GAS_TYPE), - gas_config.get(CONF_GAS_CALORIFIC), - ) - - def stop_loopenergy(event): - """Shutdown loopenergy thread on exit.""" - _LOGGER.info("Shutting down loopenergy") - controller.terminate() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_loopenergy) - - sensors = [LoopEnergyElec(controller)] - - if gas_config.get(CONF_GAS_SERIAL): - sensors.append(LoopEnergyGas(controller)) - - add_entities(sensors) - - -class LoopEnergySensor(SensorEntity): - """Implementation of an Loop Energy base sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - self._state = None - self._unit_of_measurement = DEFAULT_UNIT - self._controller = controller - self._name = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - def _callback(self): - self.schedule_update_ha_state(True) - - -class LoopEnergyElec(LoopEnergySensor): - """Implementation of an Loop Energy Electricity sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - super().__init__(controller) - self._name = "Power Usage" - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self._controller.subscribe_elecricity(self._callback) - - def update(self): - """Get the cached Loop energy reading.""" - self._state = round(self._controller.electricity_useage, 2) - - -class LoopEnergyGas(LoopEnergySensor): - """Implementation of an Loop Energy Gas sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - super().__init__(controller) - self._name = "Gas Usage" - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self._controller.subscribe_gas(self._callback) - - def update(self): - """Get the cached Loop gas reading.""" - self._state = round(self._controller.gas_useage, 2) diff --git a/requirements_all.txt b/requirements_all.txt index 11392cc4ac7..a827ea36164 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1612,9 +1612,6 @@ pylitejet==0.3.0 # homeassistant.components.litterrobot pylitterbot==2021.11.0 -# homeassistant.components.loopenergy -pyloopenergy==0.2.1 - # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 From 9f1701f55734fe790aa545f6782a8a978e20c560 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 12:54:28 -0800 Subject: [PATCH 072/366] Bumped version to 2021.12.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index de6fbd815e4..31c44f04051 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __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) From 707e50151105ace7c9e8951bf40f520028487898 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Dec 2021 19:59:26 +0100 Subject: [PATCH 073/366] Skip duplicated data when calculating fossil energy consumption (#60599) --- .../components/energy/websocket_api.py | 4 +- tests/components/energy/test_websocket_api.py | 217 +++++++++++++++++- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index cdc7599b55b..d243faae89f 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -274,14 +274,16 @@ async def ws_get_fossil_energy_consumption( ) -> dict[datetime, float]: """Combine multiple statistics, returns a dict indexed by start time.""" result: defaultdict[datetime, float] = defaultdict(float) + seen: defaultdict[datetime, set[str]] = defaultdict(set) for statistics_id, stat in stats.items(): if statistics_id not in statistic_ids: continue for period in stat: - if period["sum"] is None: + if period["sum"] is None or statistics_id in seen[period["start"]]: continue result[period["start"]] += period["sum"] + seen[period["start"]].add(statistics_id) return {key: result[key] for key in sorted(result)} diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 46c6a5c0fa6..c1dc195a63e 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -1,9 +1,10 @@ """Test the Energy websocket API.""" -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.energy import data, is_configured +from homeassistant.components.recorder import statistics from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -963,6 +964,220 @@ async def test_fossil_energy_consumption(hass, hass_ws_client): } +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_duplicate(hass, hass_ws_client): + """Test fossil_energy_consumption with co2 sensor data.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + with patch.object( + statistics, "_statistics_exists", return_value=False + ), patch.object( + statistics, "_insert_statistics", wraps=statistics._insert_statistics + ) as insert_statistics_mock: + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + async_add_external_statistics( + hass, external_co2_metadata, external_co2_statistics + ) + await async_wait_recording_done_without_instance(hass) + assert insert_statistics_mock.call_count == 14 + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4_day_start.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx( + ((44.0 - 33.0) * 0.6) + ((55.0 - 44.0) * 0.9) + ), + } + + async def test_fossil_energy_consumption_checks(hass, hass_ws_client): """Test fossil_energy_consumption parameter validation.""" client = await hass_ws_client(hass) From a581095bd070fb3e9f2b34bd66d1fea14c25adbc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 8 Dec 2021 19:32:25 +0100 Subject: [PATCH 074/366] Fix pvoutput template use and REST integer parsing (#61171) * Fix pvoutput template use and REST integer parsing * revert accepting templates as input --- homeassistant/components/pvoutput/sensor.py | 8 ++++++-- homeassistant/components/rest/utils.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 8126e00d8e5..512fb75067b 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -25,9 +25,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template _LOGGER = logging.getLogger(__name__) -_ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp" +_ENDPOINT = "https://pvoutput.org/service/r2/getstatus.jsp" ATTR_ENERGY_GENERATION = "energy_generation" ATTR_POWER_GENERATION = "power_generation" @@ -59,7 +60,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= method = "GET" payload = auth = None verify_ssl = DEFAULT_VERIFY_SSL - headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} + headers = { + "X-Pvoutput-Apikey": Template(api_key, hass), + "X-Pvoutput-SystemId": Template(system_id, hass), + } rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py index 24c58d294e1..35b3c22db31 100644 --- a/homeassistant/components/rest/utils.py +++ b/homeassistant/components/rest/utils.py @@ -23,5 +23,5 @@ def render_templates(tpl_dict: dict[str, Template] | None): rendered_items = {} for item_name, template_header in tpl_dict.items(): if (value := template_header.async_render()) is not None: - rendered_items[item_name] = value + rendered_items[item_name] = str(value) return rendered_items From bdc37e9353ebe903db96be950f99051535b5d848 Mon Sep 17 00:00:00 2001 From: Robert Blomqvist Date: Wed, 8 Dec 2021 02:19:23 +0100 Subject: [PATCH 075/366] Rephrase upgrade notification message to avoid installing Python 3.10 (#61181) --- homeassistant/bootstrap.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f111ff6a079..64a6e98aa87 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -252,8 +252,7 @@ async def async_from_config_dict( f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " f"be removed in Home Assistant {REQUIRED_NEXT_PYTHON_HA_RELEASE}. " "Please upgrade Python to " - f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " - "higher." + f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])}." ) _LOGGER.warning(msg) hass.components.persistent_notification.async_create( From 04a2e1fd7b7c1c5304774796ff182c65a33a75ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Dec 2021 15:20:38 -1000 Subject: [PATCH 076/366] Fix uncaught exception in bond config flow (#61184) --- homeassistant/components/bond/config_flow.py | 5 ++- tests/components/bond/test_config_flow.py | 41 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 5fce8477a28..d3a7b4adf72 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -87,7 +87,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self._discovered[CONF_ACCESS_TOKEN] = token - _, hub_name = await _validate_input(self.hass, self._discovered) + try: + _, hub_name = await _validate_input(self.hass, self._discovered) + except InputValidationError: + return self._discovered[CONF_NAME] = hub_name async def async_step_zeroconf( diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 4a6efa8f89b..b36637897d8 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Bond config flow.""" from __future__ import annotations +from http import HTTPStatus from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -304,6 +305,46 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_form_with_token_available_name_unavailable( + hass: core.HomeAssistant, +): + """Test we get the discovery form when we can get the token but the name is unavailable.""" + + with patch_bond_version( + side_effect=ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST) + ), patch_bond_token(return_value={"token": "discovered-token"}): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="test-host", + hostname="mock_hostname", + name="test-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["errors"] == {} + + with _patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-bond-id" + assert result2["data"] == { + CONF_HOST: "test-host", + CONF_ACCESS_TOKEN: "discovered-token", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_already_configured(hass: core.HomeAssistant): """Test starting a flow from discovery when already configured.""" From 64c52aecef9bdd9300f1c7310dff43c981fca987 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 7 Dec 2021 18:38:34 -0500 Subject: [PATCH 077/366] Bump ZHA dependency zigpy-znp from 0.6.3 to 0.6.4 (#61194) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index aa167fc2df4..daeb90be801 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -12,7 +12,7 @@ "zigpy==0.42.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.6.3" + "zigpy-znp==0.6.4" ], "usb": [ {"vid":"10C4","pid":"EA60","description":"*2652*","known_devices":["slae.sh cc2652rb stick"]}, diff --git a/requirements_all.txt b/requirements_all.txt index a827ea36164..a4cd61798e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2504,7 +2504,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.6.3 +zigpy-znp==0.6.4 # homeassistant.components.zha zigpy==0.42.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8c46b255f6..cc44099b4e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1488,7 +1488,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.6.3 +zigpy-znp==0.6.4 # homeassistant.components.zha zigpy==0.42.0 From 428129cad7c5c08d47a91b4d2a4b136ffc5227c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Dec 2021 15:20:55 -1000 Subject: [PATCH 078/366] Fix log spam from flux_led 0x08 devices when in music mode (#61196) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 71d8fd350b7..defaa348262 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.17"], + "requirements": ["flux_led==0.26.2"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index a4cd61798e0..cdf9fb48011 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.17 +flux_led==0.26.2 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc44099b4e6..c5d218a01b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.17 +flux_led==0.26.2 # homeassistant.components.homekit fnvhash==0.1.0 From 8735395144a5a6cc182caf02c26eb5e74a47e830 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:55:43 -0800 Subject: [PATCH 079/366] Fix Rova using strings as timestamp (#61201) --- homeassistant/components/rova/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 54e2c315a4e..ca9f201b302 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -116,7 +116,7 @@ class RovaSensor(SensorEntity): self.data_service.update() pickup_date = self.data_service.data.get(self.entity_description.key) if pickup_date is not None: - self._attr_native_value = pickup_date.isoformat() + self._attr_native_value = pickup_date class RovaData: From 2fa2a2e6d4515bcc4c500e1239ce538a15a38118 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:47:38 -0800 Subject: [PATCH 080/366] Fix bbox timestamp (#61202) --- homeassistant/components/bbox/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 53a7e8720b1..129803ad1e1 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -134,7 +134,7 @@ class BboxUptimeSensor(SensorEntity): uptime = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._attr_native_value = uptime.replace(microsecond=0).isoformat() + self._attr_native_value = uptime.replace(microsecond=0) class BboxSensor(SensorEntity): From 67c808bde9c09483b35c759cac951a62e7c7457c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:48:04 -0800 Subject: [PATCH 081/366] Fix flipr timestamp sensor (#61203) --- homeassistant/components/flipr/sensor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index e79ba131618..527742539c5 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -1,8 +1,6 @@ """Sensor platform for the Flipr's pool_sensor.""" from __future__ import annotations -from datetime import datetime - from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, @@ -60,7 +58,4 @@ class FliprSensor(FliprEntity, SensorEntity): @property def native_value(self): """State of the sensor.""" - state = self.coordinator.data[self.entity_description.key] - if isinstance(state, datetime): - return state.isoformat() - return state + return self.coordinator.data[self.entity_description.key] From 2c0e406c1b53cfa7fe0b0a50e524867e969c7c1f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:49:43 -0800 Subject: [PATCH 082/366] Fix gtfs timestamp sensor (#61204) --- homeassistant/components/gtfs/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 9450c717148..9a622e417ad 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -544,7 +544,7 @@ class GTFSDepartureSensor(SensorEntity): self._available = False self._icon = ICON self._name = "" - self._state: str | None = None + self._state: datetime.datetime | None = None self._attributes: dict[str, Any] = {} self._agency = None @@ -563,7 +563,7 @@ class GTFSDepartureSensor(SensorEntity): return self._name @property - def native_value(self) -> str | None: + def native_value(self) -> datetime.datetime | None: """Return the state of the sensor.""" return self._state @@ -619,9 +619,7 @@ class GTFSDepartureSensor(SensorEntity): if not self._departure: self._state = None else: - self._state = dt_util.as_utc( - self._departure["departure_time"] - ).isoformat() + self._state = dt_util.as_utc(self._departure["departure_time"]) # Fetch trip and route details once, unless updated if not self._departure: From dc3ece447b12212df271814e1315cbb87542b5c0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:56:07 -0800 Subject: [PATCH 083/366] Fix hvv_departures timestamp sensor (#61205) --- homeassistant/components/hvv_departures/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index da52fd878d8..d82a15cebe9 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -116,7 +116,7 @@ class HVVDepartureSensor(SensorEntity): departure_time + timedelta(minutes=departure["timeOffset"]) + timedelta(seconds=delay) - ).isoformat() + ) self.attr.update( { From 7583d9a4091641fff8cea5e5ef92e6b126444586 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:56:22 -0800 Subject: [PATCH 084/366] Fix hydrawise timestamp sensor (#61206) --- homeassistant/components/hydrawise/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index f8c02309569..ee9e931a351 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -83,4 +83,4 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): _LOGGER.debug("New cycle time: %s", next_cycle) self._attr_native_value = dt.utc_from_timestamp( dt.as_timestamp(dt.now()) + next_cycle - ).isoformat() + ) From 700eaf8794486fd56e28bd906425f1d9cd380326 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:39:27 -0800 Subject: [PATCH 085/366] Fix islamic prayer times timestamp sensor (#61207) --- homeassistant/components/islamic_prayer_times/sensor.py | 6 ++---- tests/components/islamic_prayer_times/test_config_flow.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 99cc65bb548..38a95fb803b 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -45,10 +45,8 @@ class IslamicPrayerTimeSensor(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return ( - self.client.prayer_times_info.get(self.sensor_type) - .astimezone(dt_util.UTC) - .isoformat() + return self.client.prayer_times_info.get(self.sensor_type).astimezone( + dt_util.UTC ) async def async_added_to_hass(self): diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 842a877e292..18d64842c65 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -5,6 +5,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import islamic_prayer_times +from homeassistant.components.islamic_prayer_times import config_flow # noqa: F401 from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN from tests.common import MockConfigEntry From d533aba4f90a377aa6f3efa1ec08d568870d51e1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:38:35 -0800 Subject: [PATCH 086/366] Fix litterrobot timestamp sensor (#61208) * Fix litterrobot timestamp sensor * Update type --- homeassistant/components/litterrobot/sensor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 1fab6983249..6b4dc1b3300 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,9 +1,11 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations +from datetime import datetime + from pylitterbot.robot import Robot -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, StateType from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE from homeassistant.core import HomeAssistant @@ -36,7 +38,7 @@ class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): self.sensor_attribute = sensor_attribute @property - def native_value(self) -> str: + def native_value(self) -> StateType | datetime: """Return the state.""" return getattr(self.robot, self.sensor_attribute) @@ -59,10 +61,10 @@ class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): """Litter-Robot sleep time sensor.""" @property - def native_value(self) -> str | None: + def native_value(self) -> StateType | datetime: """Return the state.""" if self.robot.sleep_mode_enabled: - return super().native_value.isoformat() + return super().native_value return None @property From 66fa6dff93498eccc6fc4db8419dab04676744e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:46:45 -0800 Subject: [PATCH 087/366] Fix lyric timestamp sensor (#61209) * Fix lyric timestamp sensor * Update type --- homeassistant/components/lyric/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 6f550813ad8..be156594524 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -47,7 +47,7 @@ LYRIC_SETPOINT_STATUS_NAMES = { class LyricSensorEntityDescription(SensorEntityDescription): """Class describing Honeywell Lyric sensor entities.""" - value: Callable[[LyricDevice], StateType] = round + value: Callable[[LyricDevice], StateType | datetime] = round def get_datetime_from_future_time(time: str) -> datetime: @@ -133,7 +133,7 @@ async def async_setup_entry( device_class=DEVICE_CLASS_TIMESTAMP, value=lambda device: get_datetime_from_future_time( device.changeableValues.nextPeriodTime - ).isoformat(), + ), ), location, device, From e68dcff3f364731782c07035fdaebb66fd61b254 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:25:42 -0800 Subject: [PATCH 088/366] Fix meteo_france timestamp sensor (#61210) --- homeassistant/components/meteo_france/sensor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index ac1ccc13009..7f4e3e0a77b 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -142,11 +142,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): (cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1), None, ) - return ( - dt_util.utc_from_timestamp(next_rain["dt"]).isoformat() - if next_rain - else None - ) + return dt_util.utc_from_timestamp(next_rain["dt"]) if next_rain else None @property def extra_state_attributes(self): From d080c31583a2567d1caa294becd0aeb10ccecdd6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:35:38 -0800 Subject: [PATCH 089/366] Fix modern_forms timestmap sensors (#61211) --- homeassistant/components/modern_forms/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py index 1e51ec9a1ae..5c9e0a18575 100644 --- a/homeassistant/components/modern_forms/sensor.py +++ b/homeassistant/components/modern_forms/sensor.py @@ -73,7 +73,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.light_sleep_timer @@ -83,7 +83,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): or (sleep_time - dt_util.utcnow()).total_seconds() < 0 ): return None - return sleep_time.isoformat() + return sleep_time class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): @@ -103,7 +103,7 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.fan_sleep_timer @@ -115,4 +115,4 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): ): return None - return sleep_time.isoformat() + return sleep_time From e6b784e4f2c99c8e84b4f7b71a2d64c08ce5116e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:23:14 -0800 Subject: [PATCH 090/366] Fix nextbus timestamp sensor (#61212) --- homeassistant/components/nextbus/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index f9df0d60412..3756c1853b7 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -218,6 +218,4 @@ class NextBusDepartureSensor(SensorEntity): ) latest_prediction = maybe_first(predictions) - self._state = utc_from_timestamp( - int(latest_prediction["epochTime"]) / 1000 - ).isoformat() + self._state = utc_from_timestamp(int(latest_prediction["epochTime"]) / 1000) From 2513347e2746105d1aecfcec57790b76d8b253d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:14:52 -0800 Subject: [PATCH 091/366] Fix oasa_telematics timestamp sensor (#61213) --- homeassistant/components/oasa_telematics/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 1a51738cb77..a5a4a98c3d4 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -120,7 +120,7 @@ class OASATelematicsSensor(SensorEntity): self._name_data = self.data.name_data next_arrival_data = self._times[0] if ATTR_NEXT_ARRIVAL in next_arrival_data: - self._state = next_arrival_data[ATTR_NEXT_ARRIVAL].isoformat() + self._state = next_arrival_data[ATTR_NEXT_ARRIVAL] class OASATelematicsData: From 7940aab4c5262451dc8aac4f0637b152133d0e16 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:12:26 -0800 Subject: [PATCH 092/366] Fix repetier timestamp sensors (#61214) --- homeassistant/components/repetier/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 393d8a16ae3..25c70cc2960 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -160,7 +160,7 @@ class RepetierJobEndSensor(RepetierSensor): print_time = data["print_time"] from_start = data["from_start"] time_end = start + round(print_time, 0) - self._state = datetime.utcfromtimestamp(time_end).isoformat() + self._state = datetime.utcfromtimestamp(time_end) remaining = print_time - from_start remaining_secs = int(round(remaining, 0)) _LOGGER.debug( @@ -182,7 +182,7 @@ class RepetierJobStartSensor(RepetierSensor): job_name = data["job_name"] start = data["start"] from_start = data["from_start"] - self._state = datetime.utcfromtimestamp(start).isoformat() + self._state = datetime.utcfromtimestamp(start) elapsed_secs = int(round(from_start, 0)) _LOGGER.debug( "Job %s elapsed %s", From b5b2c3cc0d315b0331e697831cffdf577edf068f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:35:13 -0800 Subject: [PATCH 093/366] Fix vallox timestamp sensor (#61216) * Fix vallox timestamp sensor * Change old state type --- homeassistant/components/vallox/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 17bcf0e4499..6eee46be737 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -56,7 +56,7 @@ class ValloxSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{uuid}-{description.key}" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" if (metric_key := self.entity_description.metric_key) is None: return None @@ -84,7 +84,7 @@ class ValloxFanSpeedSensor(ValloxSensor): """Child class for fan speed reporting.""" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" fan_is_on = self.coordinator.data.get_metric(METRIC_KEY_MODE) == MODE_ON return super().native_value if fan_is_on else 0 @@ -94,7 +94,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): """Child class for filter remaining time reporting.""" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" super_native_value = super().native_value @@ -107,7 +107,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): days_remaining_delta = timedelta(days=days_remaining) now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0) - return (now + days_remaining_delta).isoformat() + return now + days_remaining_delta class ValloxCellStateSensor(ValloxSensor): From 030ac3d762e4f1e0b0dbe240b738ca68309d6743 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:14:28 -0800 Subject: [PATCH 094/366] Fix yandex_transport timestamp sensor (#61217) --- homeassistant/components/yandex_transport/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index bd5d85d3ffe..724fca14725 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -129,9 +129,7 @@ class DiscoverYandexTransport(SensorEntity): if closer_time is None: self._state = None else: - self._state = dt_util.utc_from_timestamp(closer_time).isoformat( - timespec="seconds" - ) + self._state = dt_util.utc_from_timestamp(closer_time).replace(microsecond=0) self._attrs = attrs @property From 0b470bb8fb0670da5f788e82a62b2bf9c117de05 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:14:21 -0800 Subject: [PATCH 095/366] Fix follow-up review comment for bbox (#61219) --- homeassistant/components/bbox/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 129803ad1e1..fc9c8982733 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -131,10 +131,9 @@ class BboxUptimeSensor(SensorEntity): def update(self): """Get the latest data from Bbox and update the state.""" self.bbox_data.update() - uptime = utcnow() - timedelta( + self._attr_native_value = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._attr_native_value = uptime.replace(microsecond=0) class BboxSensor(SensorEntity): From 36eca38be2d1d1e1e3263773ee67565885991630 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:26:45 -0800 Subject: [PATCH 096/366] don't convert GTFS timestamp to UTC in timestamp sensor (#61221) --- homeassistant/components/gtfs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 9a622e417ad..367a45aa073 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -619,7 +619,7 @@ class GTFSDepartureSensor(SensorEntity): if not self._departure: self._state = None else: - self._state = dt_util.as_utc(self._departure["departure_time"]) + self._state = self._departure["departure_time"] # Fetch trip and route details once, unless updated if not self._departure: From 0cb0136b2fbe3eb300e7c812d1094aa66933a9ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 11:02:14 -0800 Subject: [PATCH 097/366] Bumped version to 2021.12.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 31c44f04051..e50660a841f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __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) From 89eed9c31ee166ea28bec554def7794a0651baa8 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 7 Dec 2021 13:16:24 +0100 Subject: [PATCH 098/366] Allow to lock SQLite database during backup (#60874) * Allow to set CONF_DB_URL This is useful for test which need a custom DB path. * Introduce write_lock_db helper to lock SQLite database * Introduce Websocket API which allows to lock database during backup * Fix isort * Avoid mutable default arguments * Address pylint issues * Avoid holding executor thread * Set unlock event in case timeout occures This makes sure the database is left unlocked even in case of a race condition. * Add more unit tests * Address new pylint errors * Lower timeout to speedup tests * Introduce queue overflow test * Unlock database if necessary This makes sure that the test runs through in case locking actually succeeds (and the test fails). * Make DB_LOCK_TIMEOUT a global There is no good reason for this to be an argument. The recorder needs to pick a sensible value. * Add Websocket Timeout test * Test lock_database() return * Update homeassistant/components/recorder/__init__.py Co-authored-by: Erik Montnemery * Fix format Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/components/recorder/__init__.py | 100 ++++++++++++++++-- homeassistant/components/recorder/util.py | 19 ++++ .../components/recorder/websocket_api.py | 40 +++++++ tests/common.py | 5 +- tests/components/recorder/test_init.py | 79 ++++++++++++++ tests/components/recorder/test_util.py | 19 ++++ .../components/recorder/test_websocket_api.py | 59 +++++++++++ 7 files changed, 310 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index da3955cb9b8..8a907a8d9fa 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable import concurrent.futures +from dataclasses import dataclass from datetime import datetime, timedelta import logging import queue @@ -76,6 +77,7 @@ from .util import ( session_scope, setup_connection_for_dialect, validate_or_move_away_sqlite_database, + write_lock_db, ) _LOGGER = logging.getLogger(__name__) @@ -123,6 +125,9 @@ KEEPALIVE_TIME = 30 # States and Events objects EXPIRE_AFTER_COMMITS = 120 +DB_LOCK_TIMEOUT = 30 +DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 + CONF_AUTO_PURGE = "auto_purge" CONF_DB_URL = "db_url" CONF_DB_MAX_RETRIES = "db_max_retries" @@ -370,6 +375,15 @@ class WaitTask: """An object to insert into the recorder queue to tell it set the _queue_watch event.""" +@dataclass +class DatabaseLockTask: + """An object to insert into the recorder queue to prevent writes to the database.""" + + database_locked: asyncio.Event + database_unlock: threading.Event + queue_overflow: bool + + class Recorder(threading.Thread): """A threaded recorder class.""" @@ -419,6 +433,7 @@ class Recorder(threading.Thread): self.migration_in_progress = False self._queue_watcher = None self._db_supports_row_number = True + self._database_lock_task: DatabaseLockTask | None = None self.enabled = True @@ -687,6 +702,8 @@ class Recorder(threading.Thread): def _process_one_event_or_recover(self, event): """Process an event, reconnect, or recover a malformed database.""" try: + if self._process_one_task(event): + return self._process_one_event(event) return except exc.DatabaseError as err: @@ -788,34 +805,63 @@ class Recorder(threading.Thread): # Schedule a new statistics task if this one didn't finish self.queue.put(ExternalStatisticsTask(metadata, stats)) - def _process_one_event(self, event): + def _lock_database(self, task: DatabaseLockTask): + @callback + def _async_set_database_locked(task: DatabaseLockTask): + task.database_locked.set() + + with write_lock_db(self): + # Notify that lock is being held, wait until database can be used again. + self.hass.add_job(_async_set_database_locked, task) + while not task.database_unlock.wait(timeout=DB_LOCK_QUEUE_CHECK_TIMEOUT): + if self.queue.qsize() > MAX_QUEUE_BACKLOG * 0.9: + _LOGGER.warning( + "Database queue backlog reached more than 90% of maximum queue " + "length while waiting for backup to finish; recorder will now " + "resume writing to database. The backup can not be trusted and " + "must be restarted" + ) + task.queue_overflow = True + break + _LOGGER.info( + "Database queue backlog reached %d entries during backup", + self.queue.qsize(), + ) + + def _process_one_task(self, event) -> bool: """Process one event.""" if isinstance(event, PurgeTask): self._run_purge(event.purge_before, event.repack, event.apply_filter) - return + return True if isinstance(event, PurgeEntitiesTask): self._run_purge_entities(event.entity_filter) - return + return True if isinstance(event, PerodicCleanupTask): perodic_db_cleanups(self) - return + return True if isinstance(event, StatisticsTask): self._run_statistics(event.start) - return + return True if isinstance(event, ClearStatisticsTask): statistics.clear_statistics(self, event.statistic_ids) - return + return True if isinstance(event, UpdateStatisticsMetadataTask): statistics.update_statistics_metadata( self, event.statistic_id, event.unit_of_measurement ) - return + return True if isinstance(event, ExternalStatisticsTask): self._run_external_statistics(event.metadata, event.statistics) - return + return True if isinstance(event, WaitTask): self._queue_watch.set() - return + return True + if isinstance(event, DatabaseLockTask): + self._lock_database(event) + return True + return False + + def _process_one_event(self, event): if event.event_type == EVENT_TIME_CHANGED: self._keepalive_count += 1 if self._keepalive_count >= KEEPALIVE_TIME: @@ -982,6 +1028,42 @@ class Recorder(threading.Thread): self.queue.put(WaitTask()) self._queue_watch.wait() + async def lock_database(self) -> bool: + """Lock database so it can be backed up safely.""" + if self._database_lock_task: + _LOGGER.warning("Database already locked") + return False + + database_locked = asyncio.Event() + task = DatabaseLockTask(database_locked, threading.Event(), False) + self.queue.put(task) + try: + await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT) + except asyncio.TimeoutError as err: + task.database_unlock.set() + raise TimeoutError( + f"Could not lock database within {DB_LOCK_TIMEOUT} seconds." + ) from err + self._database_lock_task = task + return True + + @callback + def unlock_database(self) -> bool: + """Unlock database. + + Returns true if database lock has been held throughout the process. + """ + if not self._database_lock_task: + _LOGGER.warning("Database currently not locked") + return False + + self._database_lock_task.database_unlock.set() + success = not self._database_lock_task.queue_overflow + + self._database_lock_task = None + + return success + def _setup_connection(self): """Ensure database is ready to fly.""" kwargs = {} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c63f6abee3a..3900641db63 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -457,6 +457,25 @@ def perodic_db_cleanups(instance: Recorder): connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE);")) +@contextmanager +def write_lock_db(instance: Recorder): + """Lock database for writes.""" + + if instance.engine.dialect.name == "sqlite": + with instance.engine.connect() as connection: + # Execute sqlite to create a wal checkpoint + # This is optional but makes sure the backup is going to be minimal + connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")) + # Create write lock + _LOGGER.debug("Lock database") + connection.execute(text("BEGIN IMMEDIATE;")) + try: + yield + finally: + _LOGGER.debug("Unlock database") + connection.execute(text("END;")) + + def async_migration_in_progress(hass: HomeAssistant) -> bool: """Determine is a migration is in progress. diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 5a4f0425919..f6d4d57a7e5 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,6 +1,7 @@ """The Energy websocket API.""" from __future__ import annotations +import logging from typing import TYPE_CHECKING import voluptuous as vol @@ -15,6 +16,8 @@ from .util import async_migration_in_progress if TYPE_CHECKING: from . import Recorder +_LOGGER: logging.Logger = logging.getLogger(__package__) + @callback def async_setup(hass: HomeAssistant) -> None: @@ -23,6 +26,8 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_clear_statistics) websocket_api.async_register_command(hass, ws_update_statistics_metadata) websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_backup_start) + websocket_api.async_register_command(hass, ws_backup_end) @websocket_api.websocket_command( @@ -106,3 +111,38 @@ def ws_info( "thread_running": thread_alive, } connection.send_result(msg["id"], recorder_info) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/start"}) +@websocket_api.async_response +async def ws_backup_start( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Backup start notification.""" + + _LOGGER.info("Backup start notification, locking database for writes") + instance: Recorder = hass.data[DATA_INSTANCE] + try: + await instance.lock_database() + except TimeoutError as err: + connection.send_error(msg["id"], "timeout_error", str(err)) + return + connection.send_result(msg["id"]) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/end"}) +@websocket_api.async_response +async def ws_backup_end( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Backup end notification.""" + + instance: Recorder = hass.data[DATA_INSTANCE] + _LOGGER.info("Backup end notification, releasing write lock") + if not instance.unlock_database(): + connection.send_error( + msg["id"], "database_unlock_failed", "Failed to unlock database." + ) + connection.send_result(msg["id"]) diff --git a/tests/common.py b/tests/common.py index 55c76e953cd..9d4a9cfe366 100644 --- a/tests/common.py +++ b/tests/common.py @@ -902,8 +902,9 @@ def init_recorder_component(hass, add_config=None): async def async_init_recorder_component(hass, add_config=None): """Initialize the recorder asynchronously.""" - config = dict(add_config) if add_config else {} - config[recorder.CONF_DB_URL] = "sqlite://" + config = add_config or {} + if recorder.CONF_DB_URL not in config: + config[recorder.CONF_DB_URL] = "sqlite://" with patch("homeassistant.components.recorder.migration.migrate_schema"): assert await async_setup_component( diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index e41a0da34ba..7d7c3f27fb6 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,5 +1,6 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access +import asyncio from datetime import datetime, timedelta import sqlite3 from unittest.mock import patch @@ -1134,3 +1135,81 @@ def test_entity_id_filter(hass_recorder): db_events = list(session.query(Events).filter_by(event_type="hello")) # Keep referring idx + 1, as no new events are being added assert len(db_events) == idx + 1, data + + +async def test_database_lock_and_unlock(hass: HomeAssistant, tmp_path): + """Test writing events during lock getting written after unlocking.""" + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + + assert await instance.lock_database() + + assert not await instance.lock_database() + + event_type = "EVENT_TEST" + event_data = {"test_attr": 5, "test_attr_10": "nice"} + hass.bus.fire(event_type, event_data) + task = asyncio.create_task(async_wait_recording_done(hass, instance)) + + # Recording can't be finished while lock is held + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(asyncio.shield(task), timeout=1) + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 0 + + assert instance.unlock_database() + + await task + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 1 + + +async def test_database_lock_and_overflow(hass: HomeAssistant, tmp_path): + """Test writing events during lock leading to overflow the queue causes the database to unlock.""" + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + + with patch.object(recorder, "MAX_QUEUE_BACKLOG", 1), patch.object( + recorder, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.1 + ): + await instance.lock_database() + + event_type = "EVENT_TEST" + event_data = {"test_attr": 5, "test_attr_10": "nice"} + hass.bus.fire(event_type, event_data) + + # Check that this causes the queue to overflow and write succeeds + # even before unlocking. + await async_wait_recording_done(hass, instance) + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 1 + + assert not instance.unlock_database() + + +async def test_database_lock_timeout(hass): + """Test locking database timeout when recorder stopped.""" + await async_init_recorder_component(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + with patch.object(recorder, "DB_LOCK_TIMEOUT", 0.1): + try: + with pytest.raises(TimeoutError): + await instance.lock_database() + finally: + instance.unlock_database() diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 940925c48ca..fa449aefefc 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -8,6 +8,7 @@ import pytest from sqlalchemy import text from sqlalchemy.sql.elements import TextClause +from homeassistant.components import recorder from homeassistant.components.recorder import run_information_with_session, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import RecorderRuns @@ -556,3 +557,21 @@ def test_perodic_db_cleanups(hass_recorder): ][0] assert isinstance(text_obj, TextClause) assert str(text_obj) == "PRAGMA wal_checkpoint(TRUNCATE);" + + +async def test_write_lock_db(hass, tmp_path): + """Test database write lock.""" + from sqlalchemy.exc import OperationalError + + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance = hass.data[DATA_INSTANCE] + + with util.write_lock_db(instance): + # Database should be locked now, try writing SQL command + with instance.engine.connect() as connection: + with pytest.raises(OperationalError): + connection.execute(text("DROP TABLE events;")) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 7a45dea0379..994d1c677af 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -358,3 +358,62 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): assert response["result"]["migration_in_progress"] is False assert response["result"]["recording"] is True assert response["result"]["thread_running"] is True + + +async def test_backup_start_no_recorder(hass, hass_ws_client): + """Test getting backup start when recorder is not present.""" + client = await hass_ws_client() + + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "unknown_command" + + +async def test_backup_start_timeout(hass, hass_ws_client): + """Test getting backup start when recorder is not present.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + with patch.object(recorder, "DB_LOCK_TIMEOUT", 0): + try: + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "timeout_error" + finally: + await client.send_json({"id": 2, "type": "backup/end"}) + + +async def test_backup_end(hass, hass_ws_client): + """Test backup start.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert response["success"] + + await client.send_json({"id": 2, "type": "backup/end"}) + response = await client.receive_json() + assert response["success"] + + +async def test_backup_end_without_start(hass, hass_ws_client): + """Test backup start.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + await client.send_json({"id": 1, "type": "backup/end"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "database_unlock_failed" From 1ddb0d255a6480b2c9cb19d577b705b62aa8b6c0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Dec 2021 11:43:48 +0100 Subject: [PATCH 099/366] Fix date/datetime support for templates (#61088) Co-authored-by: Paulus Schoutsen --- homeassistant/components/template/sensor.py | 65 +++++++- tests/components/template/test_sensor.py | 166 ++++++++++++++++++++ 2 files changed, 229 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a31e49db570..18ae8af8569 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,6 +1,8 @@ """Allows the creation of a sensor that breaks out state_attributes.""" from __future__ import annotations +from datetime import date, datetime +import logging from typing import Any import voluptuous as vol @@ -12,6 +14,7 @@ from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, + SensorDeviceClass, SensorEntity, ) from homeassistant.const import ( @@ -32,6 +35,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.util import dt as dt_util from .const import ( CONF_ATTRIBUTE_TEMPLATES, @@ -85,6 +89,7 @@ LEGACY_SENSOR_SCHEMA = vol.All( } ), ) +_LOGGER = logging.getLogger(__name__) def extra_validation_checks(val): @@ -179,6 +184,32 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) +@callback +def _async_parse_date_datetime( + value: str, entity_id: str, device_class: SensorDeviceClass | str | None +) -> datetime | date | None: + """Parse datetime.""" + if device_class == SensorDeviceClass.TIMESTAMP: + if (parsed_timestamp := dt_util.parse_datetime(value)) is None: + _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) + return None + + if parsed_timestamp.tzinfo is None: + _LOGGER.warning( + "%s rendered timestamp without timezone: %s", entity_id, value + ) + return None + + return parsed_timestamp + + # Date device class + if (parsed_date := dt_util.parse_date(value)) is not None: + return parsed_date + + _LOGGER.warning("%s rendered invalid date %s", entity_id, value) + return None + + class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" @@ -227,7 +258,20 @@ class SensorTemplate(TemplateEntity, SensorEntity): @callback def _update_state(self, result): super()._update_state(result) - self._attr_native_value = None if isinstance(result, TemplateError) else result + if isinstance(result, TemplateError): + self._attr_native_value = None + return + + if result is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + self._attr_native_value = result + return + + self._attr_native_value = _async_parse_date_datetime( + result, self.entity_id, self.device_class + ) class TriggerSensorEntity(TriggerEntity, SensorEntity): @@ -237,7 +281,7 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): extra_template_keys = (CONF_STATE,) @property - def native_value(self) -> str | None: + def native_value(self) -> str | datetime | date | None: """Return state of the sensor.""" return self._rendered.get(CONF_STATE) @@ -245,3 +289,20 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): def state_class(self) -> str | None: """Sensor state class.""" return self._config.get(CONF_STATE_CLASS) + + @callback + def _process_data(self) -> None: + """Process new data.""" + super()._process_data() + + if ( + state := self._rendered.get(CONF_STATE) + ) is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + return + + self._rendered[CONF_STATE] = _async_parse_date_datetime( + state, self.entity_id, self.device_class + ) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 189fe3653f2..0352080bed8 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1094,3 +1094,169 @@ async def test_trigger_entity_available(hass): state = hass.states.get("sensor.maybe_available") assert state.state == "unavailable" + + +async def test_trigger_entity_device_class_parsing_works(hass): + """Test trigger entity device class parsing works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "trigger": {"platform": "event", "event_type": "test_event"}, + "sensor": [ + { + "name": "Date entity", + "state": "{{ now().date() }}", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "{{ now() }}", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == now.date().isoformat() + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == now.isoformat(timespec="seconds") + + +async def test_trigger_entity_device_class_errors_works(hass): + """Test trigger entity device class errors works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "trigger": {"platform": "event", "event_type": "test_event"}, + "sensor": [ + { + "name": "Date entity", + "state": "invalid", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "invalid", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == STATE_UNKNOWN + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == STATE_UNKNOWN + + +async def test_entity_device_class_parsing_works(hass): + """Test entity device class parsing works.""" + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "sensor": [ + { + "name": "Date entity", + "state": "{{ now().date() }}", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "{{ now() }}", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == now.date().isoformat() + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == now.isoformat(timespec="seconds") + + +async def test_entity_device_class_errors_works(hass): + """Test entity device class errors works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "sensor": [ + { + "name": "Date entity", + "state": "invalid", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "invalid", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == STATE_UNKNOWN + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == STATE_UNKNOWN From 7ee148c65082a4dc74d3cfd8b8fbc20c885d8db2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 8 Dec 2021 21:49:40 -0800 Subject: [PATCH 100/366] Display nest media events using local time (#61143) --- homeassistant/components/nest/media_source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 8fd7d384e36..b02b9b1870e 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -46,6 +46,7 @@ from homeassistant.components.nest.device_info import NestDeviceInfo from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP from homeassistant.core import HomeAssistant from homeassistant.helpers.template import DATE_STR_FORMAT +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -250,7 +251,7 @@ def _browse_event( media_content_type=MEDIA_TYPE_IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), - event_time=event.timestamp.strftime(DATE_STR_FORMAT), + event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), ), can_play=True, can_expand=False, From 79501289f0afb4f3bb006389d3c71b551efbfff8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 9 Dec 2021 10:49:19 +0100 Subject: [PATCH 101/366] Correct state class for Tasmota sensors (#61236) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/tasmota/sensor.py | 146 +++++++++++++-------- 1 file changed, 94 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 678d3eaf4fa..45ff93b5945 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -10,25 +10,15 @@ from hatasmota.models import DiscoveryHashType from homeassistant.components import sensor from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO2, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -63,28 +53,54 @@ STATE_CLASS = "state_class" ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, not both -SENSOR_DEVICE_CLASS_ICON_MAP = { - hc.SENSOR_AMBIENT: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, - hc.SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_BATTERY: { - DEVICE_CLASS: DEVICE_CLASS_BATTERY, - STATE_CLASS: STATE_CLASS_MEASUREMENT, +SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { + hc.SENSOR_AMBIENT: { + DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_APPARENT_POWERUSAGE: { + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_BATTERY: { + DEVICE_CLASS: SensorDeviceClass.BATTERY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_CCT: { + ICON: "mdi:temperature-kelvin", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_CO2: { + DEVICE_CLASS: SensorDeviceClass.CO2, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_CCT: {ICON: "mdi:temperature-kelvin"}, - hc.SENSOR_CO2: {DEVICE_CLASS: DEVICE_CLASS_CO2}, hc.SENSOR_COLOR_BLUE: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"}, - hc.SENSOR_CURRENT: {ICON: "mdi:alpha-a-circle-outline"}, - hc.SENSOR_DEWPOINT: {ICON: "mdi:weather-rainy"}, - hc.SENSOR_DISTANCE: {ICON: "mdi:leak"}, - hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, - hc.SENSOR_FREQUENCY: {ICON: "mdi:current-ac"}, - hc.SENSOR_HUMIDITY: { - DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + hc.SENSOR_CURRENT: { + ICON: "mdi:alpha-a-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_DEWPOINT: { + ICON: "mdi:weather-rainy", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_DISTANCE: { + ICON: "mdi:leak", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, + hc.SENSOR_FREQUENCY: { + DEVICE_CLASS: SensorDeviceClass.FREQUENCY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_HUMIDITY: { + DEVICE_CLASS: SensorDeviceClass.HUMIDITY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_ILLUMINANCE: { + DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_ILLUMINANCE: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, hc.SENSOR_MOISTURE: {ICON: "mdi:cup-water"}, @@ -95,40 +111,66 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_PB1: {ICON: "mdi:flask"}, hc.SENSOR_PB2_5: {ICON: "mdi:flask"}, hc.SENSOR_PB5: {ICON: "mdi:flask"}, - hc.SENSOR_PM10: {ICON: "mdi:air-filter"}, - hc.SENSOR_PM1: {ICON: "mdi:air-filter"}, - hc.SENSOR_PM2_5: {ICON: "mdi:air-filter"}, - hc.SENSOR_POWERFACTOR: {ICON: "mdi:alpha-f-circle-outline"}, - hc.SENSOR_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_PM10: { + DEVICE_CLASS: SensorDeviceClass.PM10, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_PM1: { + DEVICE_CLASS: SensorDeviceClass.PM1, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_PM2_5: { + DEVICE_CLASS: SensorDeviceClass.PM25, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWERFACTOR: { + ICON: "mdi:alpha-f-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWERUSAGE: { + DEVICE_CLASS: SensorDeviceClass.POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_PRESSURE: { - DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.PRESSURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PRESSUREATSEALEVEL: { - DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.PRESSURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, - hc.SENSOR_REACTIVE_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + hc.SENSOR_REACTIVE_POWERUSAGE: { + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, hc.SENSOR_STATUS_RESTART_REASON: {ICON: "mdi:information-outline"}, - hc.SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH}, - hc.SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"}, + hc.SENSOR_STATUS_SIGNAL: { + DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_STATUS_RSSI: { + ICON: "mdi:access-point", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"}, hc.SENSOR_TEMPERATURE: { - DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, + hc.SENSOR_TODAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, hc.SENSOR_TOTAL: { - DEVICE_CLASS: DEVICE_CLASS_ENERGY, - STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, - hc.SENSOR_VOLTAGE: {ICON: "mdi:alpha-v-circle-outline"}, - hc.SENSOR_WEIGHT: {ICON: "mdi:scale"}, - hc.SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, + hc.SENSOR_VOLTAGE: { + ICON: "mdi:alpha-v-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_WEIGHT: {ICON: "mdi:scale", STATE_CLASS: SensorStateClass.MEASUREMENT}, + hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, } SENSOR_UNIT_MAP = { @@ -208,7 +250,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): @callback def sensor_state_updated(self, state: Any, **kwargs: Any) -> None: """Handle state updates.""" - if self.device_class == DEVICE_CLASS_TIMESTAMP: + if self.device_class == SensorDeviceClass.TIMESTAMP: self._state_timestamp = state else: self._state = state @@ -261,7 +303,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): @property def native_value(self) -> datetime | str | None: """Return the state of the entity.""" - if self._state_timestamp and self.device_class == DEVICE_CLASS_TIMESTAMP: + if self._state_timestamp and self.device_class == SensorDeviceClass.TIMESTAMP: return self._state_timestamp return self._state From 5c70ddb7cb05e2e544b636a66bb01d6b56f7bc0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 11:44:53 -0800 Subject: [PATCH 102/366] Fix smartthings timestamp sensor (#61254) --- homeassistant/components/smartthings/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index eab840bd629..fa749e07dfb 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -38,6 +38,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_METERS, ) +from homeassistant.util import dt as dt_util from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -656,7 +657,12 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self._device.status.attributes[self._attribute].value + value = self._device.status.attributes[self._attribute].value + + if self._device_class != DEVICE_CLASS_TIMESTAMP: + return value + + return dt_util.parse_datetime(value) @property def device_class(self): From e66f0a68e79f6dd4c9fbda8cd874d28badd29c97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 12:21:33 -0800 Subject: [PATCH 103/366] Guard cannot connect during Tuya init (#61267) --- homeassistant/components/tuya/__init__.py | 44 ++++++++++------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 4f34d3c31bf..fb5b4d759f3 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import NamedTuple +import requests from tuya_iot import ( AuthType, TuyaDevice, @@ -18,6 +19,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import dispatcher_send @@ -60,18 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data.pop(CONF_PROJECT_TYPE) hass.config_entries.async_update_entry(entry, data=data) - success = await _init_tuya_sdk(hass, entry) - - if not success: - hass.data[DOMAIN].pop(entry.entry_id) - - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - - return bool(success) - - -async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: auth_type = AuthType(entry.data[CONF_AUTH_TYPE]) api = TuyaOpenAPI( endpoint=entry.data[CONF_ENDPOINT], @@ -82,22 +72,24 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.set_dev_channel("hass") - if auth_type == AuthType.CUSTOM: - response = await hass.async_add_executor_job( - api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] - ) - else: - response = await hass.async_add_executor_job( - api.connect, - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - entry.data[CONF_COUNTRY_CODE], - entry.data[CONF_APP_TYPE], - ) + try: + if auth_type == AuthType.CUSTOM: + response = await hass.async_add_executor_job( + api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) + else: + response = await hass.async_add_executor_job( + api.connect, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_COUNTRY_CODE], + entry.data[CONF_APP_TYPE], + ) + except requests.exceptions.RequestException as err: + raise ConfigEntryNotReady(err) from err if response.get("success", False) is False: - _LOGGER.error("Tuya login error response: %s", response) - return False + raise ConfigEntryNotReady(response) tuya_mq = TuyaOpenMQ(api) tuya_mq.start() From 793bdebc1399102efdeee4e5e21037643529c9cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 12:19:22 -0800 Subject: [PATCH 104/366] Use correct template parameter in Rest template rendering (#61269) --- homeassistant/components/rest/data.py | 4 ++-- homeassistant/components/rest/switch.py | 8 ++++---- homeassistant/components/rest/utils.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 513f2393127..e9fbb5718a5 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -52,8 +52,8 @@ class RestData: self._hass, verify_ssl=self._verify_ssl ) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) _LOGGER.debug("Updating from %s", self._resource) try: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 3448b79979c..1fd04b66559 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -207,8 +207,8 @@ class RestSwitch(SwitchEntity): """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) async with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( @@ -233,8 +233,8 @@ class RestSwitch(SwitchEntity): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) async with async_timeout.timeout(self._timeout): req = await websession.get( diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py index 35b3c22db31..f3fdba651ac 100644 --- a/homeassistant/components/rest/utils.py +++ b/homeassistant/components/rest/utils.py @@ -15,13 +15,13 @@ def inject_hass_in_templates_list( tpl.hass = hass -def render_templates(tpl_dict: dict[str, Template] | None): +def render_templates(tpl_dict: dict[str, Template] | None, parse_result: bool): """Render a dict of templates.""" if tpl_dict is None: return None rendered_items = {} - for item_name, template_header in tpl_dict.items(): - if (value := template_header.async_render()) is not None: - rendered_items[item_name] = str(value) + for item_name, template in tpl_dict.items(): + if (value := template.async_render(parse_result=parse_result)) is not None: + rendered_items[item_name] = value return rendered_items From 9a4a09b2f2c7a5bdc3205a3a4884ba750a20b729 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 10:54:41 -1000 Subject: [PATCH 105/366] Bump flux_led to 0.26.3 (#61287) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index defaa348262..0c5e58027ce 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.2"], + "requirements": ["flux_led==0.26.3"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index cdf9fb48011..26461f9c3d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.2 +flux_led==0.26.3 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5d218a01b3..516cc0ce54f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.2 +flux_led==0.26.3 # homeassistant.components.homekit fnvhash==0.1.0 From aefd675737d9a3a08628c5ab60edfe2702b74b92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 11:03:07 -1000 Subject: [PATCH 106/366] Restore rest integration ability to follow http redirects (#61293) --- homeassistant/components/rest/data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index e9fbb5718a5..bc98b0caf68 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -65,6 +65,7 @@ class RestData: auth=self._auth, data=self._request_data, timeout=self._timeout, + follow_redirects=True, ) self.data = response.text self.headers = response.headers From 10a4037ed32f70e11367829869012f26792b70ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 14:35:50 -0800 Subject: [PATCH 107/366] Rest fixes (#61296) --- homeassistant/components/pvoutput/sensor.py | 6 +---- homeassistant/components/rest/__init__.py | 6 ++--- homeassistant/components/rest/data.py | 6 ++--- homeassistant/components/rest/switch.py | 16 ++++++------ homeassistant/components/rest/utils.py | 27 --------------------- homeassistant/helpers/template.py | 15 +++++++++--- 6 files changed, 26 insertions(+), 50 deletions(-) delete mode 100644 homeassistant/components/rest/utils.py diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 512fb75067b..1d8b3400d8b 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template import Template _LOGGER = logging.getLogger(__name__) _ENDPOINT = "https://pvoutput.org/service/r2/getstatus.jsp" @@ -60,10 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= method = "GET" payload = auth = None verify_ssl = DEFAULT_VERIFY_SSL - headers = { - "X-Pvoutput-Apikey": Template(api_key, hass), - "X-Pvoutput-SystemId": Template(system_id, hass), - } + headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index ba101624673..b55d9c6d844 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -25,7 +25,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, template from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, @@ -37,7 +37,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX from .data import RestData from .schema import CONFIG_SCHEMA # noqa: F401 -from .utils import inject_hass_in_templates_list _LOGGER = logging.getLogger(__name__) @@ -161,7 +160,8 @@ def create_rest_data_from_config(hass, config): resource_template.hass = hass resource = resource_template.async_render(parse_result=False) - inject_hass_in_templates_list(hass, [headers, params]) + template.attach(hass, headers) + template.attach(hass, params) if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index bc98b0caf68..7c8fd61e688 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -3,7 +3,7 @@ import logging import httpx -from homeassistant.components.rest.utils import render_templates +from homeassistant.helpers import template from homeassistant.helpers.httpx_client import get_async_client DEFAULT_TIMEOUT = 10 @@ -52,8 +52,8 @@ class RestData: self._hass, verify_ssl=self._verify_ssl ) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) _LOGGER.debug("Updating from %s", self._resource) try: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 1fd04b66559..3e5fd7e2c68 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -24,10 +24,8 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -from .utils import inject_hass_in_templates_list, render_templates _LOGGER = logging.getLogger(__name__) CONF_BODY_OFF = "body_off" @@ -92,7 +90,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= body_on.hass = hass if body_off is not None: body_off.hass = hass - inject_hass_in_templates_list(hass, [headers, params]) + + template.attach(hass, headers) + template.attach(hass, params) timeout = config.get(CONF_TIMEOUT) try: @@ -207,8 +207,8 @@ class RestSwitch(SwitchEntity): """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( @@ -233,8 +233,8 @@ class RestSwitch(SwitchEntity): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): req = await websession.get( diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py deleted file mode 100644 index f3fdba651ac..00000000000 --- a/homeassistant/components/rest/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Reusable utilities for the Rest component.""" -from __future__ import annotations - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.template import Template - - -def inject_hass_in_templates_list( - hass: HomeAssistant, tpl_dict_list: list[dict[str, Template] | None] -): - """Inject hass in a list of dict of templates.""" - for tpl_dict in tpl_dict_list: - if tpl_dict is not None: - for tpl in tpl_dict.values(): - tpl.hass = hass - - -def render_templates(tpl_dict: dict[str, Template] | None, parse_result: bool): - """Render a dict of templates.""" - if tpl_dict is None: - return None - - rendered_items = {} - for item_name, template in tpl_dict.items(): - if (value := template.async_render(parse_result=parse_result)) is not None: - rendered_items[item_name] = value - return rendered_items diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d460e7ab42b..0ba1d6bfa14 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -110,18 +110,25 @@ def attach(hass: HomeAssistant, obj: Any) -> None: def render_complex( - value: Any, variables: TemplateVarsType = None, limited: bool = False + value: Any, + variables: TemplateVarsType = None, + limited: bool = False, + parse_result: bool = True, ) -> Any: """Recursive template creator helper function.""" if isinstance(value, list): - return [render_complex(item, variables) for item in value] + return [ + render_complex(item, variables, limited, parse_result) for item in value + ] if isinstance(value, collections.abc.Mapping): return { - render_complex(key, variables): render_complex(item, variables) + render_complex(key, variables, limited, parse_result): render_complex( + item, variables, limited, parse_result + ) for key, item in value.items() } if isinstance(value, Template): - return value.async_render(variables, limited=limited) + return value.async_render(variables, limited=limited, parse_result=parse_result) return value From d7708d58ba44b5978c0fe23a797878404536e621 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 9 Dec 2021 01:49:35 +0100 Subject: [PATCH 108/366] Introduce only_supervisor for @websocket_api.ws_require_user() (#61298) --- homeassistant/components/hassio/__init__.py | 5 ++-- .../components/recorder/websocket_api.py | 4 ++-- .../components/websocket_api/decorators.py | 6 +++++ homeassistant/const.py | 3 +++ .../components/recorder/test_websocket_api.py | 20 +++++++++------- .../websocket_api/test_decorators.py | 23 +++++++++++++++++++ tests/conftest.py | 22 +++++++++++++++++- 7 files changed, 70 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 7991c50563c..2c3d61ef584 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_NAME, EVENT_CORE_CONFIG_UPDATE, + HASSIO_USER_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, ) @@ -439,11 +440,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # Migrate old name if user.name == "Hass.io": - await hass.auth.async_update_user(user, name="Supervisor") + await hass.auth.async_update_user(user, name=HASSIO_USER_NAME) if refresh_token is None: user = await hass.auth.async_create_system_user( - "Supervisor", group_ids=[GROUP_ID_ADMIN] + HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN] ) refresh_token = await hass.auth.async_create_refresh_token(user) data["hassio_user"] = user.id diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index f6d4d57a7e5..aec7905615f 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -113,7 +113,7 @@ def ws_info( connection.send_result(msg["id"], recorder_info) -@websocket_api.require_admin +@websocket_api.ws_require_user(only_supervisor=True) @websocket_api.websocket_command({vol.Required("type"): "backup/start"}) @websocket_api.async_response async def ws_backup_start( @@ -131,7 +131,7 @@ async def ws_backup_start( connection.send_result(msg["id"]) -@websocket_api.require_admin +@websocket_api.ws_require_user(only_supervisor=True) @websocket_api.websocket_command({vol.Required("type"): "backup/end"}) @websocket_api.async_response async def ws_backup_end( diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index eff82a8c71d..296271c7cfd 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -8,6 +8,7 @@ from typing import Any import voluptuous as vol +from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized @@ -70,6 +71,7 @@ def ws_require_user( allow_system_user: bool = True, only_active_user: bool = True, only_inactive_user: bool = False, + only_supervisor: bool = False, ) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Decorate function validating login user exist in current WS connection. @@ -111,6 +113,10 @@ def ws_require_user( output_error("only_inactive_user", "Not allowed as active user") return + if only_supervisor and connection.user.name != HASSIO_USER_NAME: + output_error("only_supervisor", "Only allowed as Supervisor") + return + return func(hass, connection, msg) return check_current_user diff --git a/homeassistant/const.py b/homeassistant/const.py index e50660a841f..1eae844f7cd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -756,3 +756,6 @@ ENTITY_CATEGORIES: Final[list[str]] = [ CAST_APP_ID_HOMEASSISTANT_MEDIA: Final = "B45F4572" # The ID of the Home Assistant Lovelace Cast App CAST_APP_ID_HOMEASSISTANT_LOVELACE: Final = "A078F6B0" + +# User used by Supervisor +HASSIO_USER_NAME = "Supervisor" diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 994d1c677af..2a9f737e9a5 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -360,9 +360,11 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): assert response["result"]["thread_running"] is True -async def test_backup_start_no_recorder(hass, hass_ws_client): +async def test_backup_start_no_recorder( + hass, hass_ws_client, hass_supervisor_access_token +): """Test getting backup start when recorder is not present.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await client.send_json({"id": 1, "type": "backup/start"}) response = await client.receive_json() @@ -370,9 +372,9 @@ async def test_backup_start_no_recorder(hass, hass_ws_client): assert response["error"]["code"] == "unknown_command" -async def test_backup_start_timeout(hass, hass_ws_client): +async def test_backup_start_timeout(hass, hass_ws_client, hass_supervisor_access_token): """Test getting backup start when recorder is not present.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events @@ -388,9 +390,9 @@ async def test_backup_start_timeout(hass, hass_ws_client): await client.send_json({"id": 2, "type": "backup/end"}) -async def test_backup_end(hass, hass_ws_client): +async def test_backup_end(hass, hass_ws_client, hass_supervisor_access_token): """Test backup start.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events @@ -405,9 +407,11 @@ async def test_backup_end(hass, hass_ws_client): assert response["success"] -async def test_backup_end_without_start(hass, hass_ws_client): +async def test_backup_end_without_start( + hass, hass_ws_client, hass_supervisor_access_token +): """Test backup start.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py index 45d761f6fed..4fbc1ae1a21 100644 --- a/tests/components/websocket_api/test_decorators.py +++ b/tests/components/websocket_api/test_decorators.py @@ -66,3 +66,26 @@ async def test_async_response_request_context(hass, websocket_client): assert msg["id"] == 7 assert not msg["success"] assert msg["error"]["code"] == "not_found" + + +async def test_supervisor_only(hass, websocket_client): + """Test that only the Supervisor can make requests.""" + + @websocket_api.ws_require_user(only_supervisor=True) + @websocket_api.websocket_command({"type": "test-require-supervisor-user"}) + def require_supervisor_request(hass, connection, msg): + connection.send_result(msg["id"]) + + websocket_api.async_register_command(hass, require_supervisor_request) + + await websocket_client.send_json( + { + "id": 5, + "type": "test-require-supervisor-user", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"]["code"] == "only_supervisor" diff --git a/tests/conftest.py b/tests/conftest.py index 10a9dd1627b..0107e218335 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.http import URL -from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED +from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED, HASSIO_USER_NAME from homeassistant.helpers import config_entry_oauth2_flow, event from homeassistant.setup import async_setup_component from homeassistant.util import location @@ -405,6 +405,26 @@ def hass_read_only_access_token(hass, hass_read_only_user, local_auth): return hass.auth.async_create_access_token(refresh_token) +@pytest.fixture +def hass_supervisor_user(hass, local_auth): + """Return the Home Assistant Supervisor user.""" + admin_group = hass.loop.run_until_complete( + hass.auth.async_get_group(GROUP_ID_ADMIN) + ) + return MockUser( + name=HASSIO_USER_NAME, groups=[admin_group], system_generated=True + ).add_to_hass(hass) + + +@pytest.fixture +def hass_supervisor_access_token(hass, hass_supervisor_user, local_auth): + """Return a Home Assistant Supervisor access token.""" + refresh_token = hass.loop.run_until_complete( + hass.auth.async_create_refresh_token(hass_supervisor_user) + ) + return hass.auth.async_create_access_token(refresh_token) + + @pytest.fixture def legacy_auth(hass): """Load legacy API password provider.""" From 7387640524974dde756f1d0c362f062fa8fdeac6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 22:36:41 -0800 Subject: [PATCH 109/366] Fix rova timezone (#61302) --- homeassistant/components/rova/sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index ca9f201b302..60d0fbe6df0 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +from homeassistant.util.dt import get_time_zone, now # Config for rova requests. CONF_ZIP_CODE = "zip_code" @@ -140,10 +141,12 @@ class RovaData: self.data = {} for item in items: - date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S") + date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S").replace( + tzinfo=get_time_zone("Europe/Amsterdam") + ) code = item["GarbageTypeCode"].lower() - if code not in self.data and date > datetime.now(): + if code not in self.data and date > now(): self.data[code] = date _LOGGER.debug("Updated Rova calendar: %s", self.data) From 24a6e9004272a0251ec00edfd4db9c8856a20ebf Mon Sep 17 00:00:00 2001 From: Yehuda Davis Date: Thu, 9 Dec 2021 02:40:45 -0500 Subject: [PATCH 110/366] Fix regression in Tuya cover is_closed logic (#61303) --- homeassistant/components/tuya/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index fd1f2aae972..275d28bae17 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -297,7 +297,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): is not None ): return self.entity_description.current_state_inverse is not ( - current_state in (False, "fully_close") + current_state in (True, "fully_close") ) if (position := self.current_cover_position) is not None: From fe7521b503e19cc13cdf6df1fbe7abcc43090337 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 20:21:11 -1000 Subject: [PATCH 111/366] Fix lookin failing to setup during firmware updates (#61305) --- homeassistant/components/lookin/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index f749621beaf..5e603027a50 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -1,6 +1,7 @@ """The lookin integration.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging @@ -37,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: lookin_device = await lookin_protocol.get_info() devices = await lookin_protocol.get_devices() - except aiohttp.ClientError as ex: + except (asyncio.TimeoutError, aiohttp.ClientError) as ex: raise ConfigEntryNotReady from ex meteo_coordinator: DataUpdateCoordinator = DataUpdateCoordinator( From 0203228a11426d8e88e4477ca6d41f8356806401 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 23:58:23 -0800 Subject: [PATCH 112/366] Fix hue groups inheritance (#61308) --- homeassistant/components/hue/v2/group.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index ae77ab38539..312fef6629f 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -7,7 +7,6 @@ from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.groups import GroupedLight, Room, Zone -from homeassistant.components.group.light import LightGroup from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -18,6 +17,7 @@ from homeassistant.components.light import ( COLOR_MODE_ONOFF, COLOR_MODE_XY, SUPPORT_TRANSITION, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -73,11 +73,12 @@ async def async_setup_entry( ) -class GroupedHueLight(HueBaseEntity, LightGroup): +class GroupedHueLight(HueBaseEntity, LightEntity): """Representation of a Grouped Hue light.""" # Entities for Hue groups are disabled by default _attr_entity_registry_enabled_default = False + _attr_icon = "mdi:lightbulb-group" def __init__( self, bridge: HueBridge, resource: GroupedLight, group: Room | Zone From 07438c07c948b68cb8217c81cd2b8c2c68f0b281 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 22:50:30 -0800 Subject: [PATCH 113/366] Fix CO2signal error handling (#61311) --- homeassistant/components/co2signal/__init__.py | 3 --- homeassistant/components/co2signal/config_flow.py | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index a87aed64564..56be5bca57b 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -134,9 +134,6 @@ def get_data(hass: HomeAssistant, config: dict) -> CO2SignalResponse: _LOGGER.exception("Unexpected exception") raise UnknownError from err - except Exception as err: - _LOGGER.exception("Unexpected exception") - raise UnknownError from err else: if "error" in data: diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index e7f94e4d603..fb4e48c66e8 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from . import APIRatelimitExceeded, CO2Error, InvalidAuth, UnknownError, get_data +from . import APIRatelimitExceeded, CO2Error, InvalidAuth, get_data from .const import CONF_COUNTRY_CODE, DOMAIN from .util import get_extra_name @@ -172,7 +172,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except APIRatelimitExceeded: errors["base"] = "api_ratelimit" - except UnknownError: + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" else: return self.async_create_entry( From 3b2b116c1046ed865c01261c5e1adac7d1c4ca54 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Dec 2021 10:17:34 +0100 Subject: [PATCH 114/366] Upgrade tailscale to 0.1.4 (#61338) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index e1b2435b989..4d47e397b76 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.3"], + "requirements": ["tailscale==0.1.4"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 26461f9c3d8..95818018796 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2272,7 +2272,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.3 +tailscale==0.1.4 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 516cc0ce54f..fbb236aff66 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.3 +tailscale==0.1.4 # homeassistant.components.tellduslive tellduslive==0.10.11 From 29aab7ad7acfd80b492a579cfb9b5e8870c9634d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Dec 2021 12:09:53 +0100 Subject: [PATCH 115/366] Bumped version to 2021.12.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1eae844f7cd..474d2125689 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __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) From 7cc2af2a468fe07c057e1afaeebb331afba65330 Mon Sep 17 00:00:00 2001 From: einarhauks Date: Thu, 9 Dec 2021 22:08:29 +0000 Subject: [PATCH 116/366] Update tesla-wall-connector to v1.0.1 (#61392) --- homeassistant/components/tesla_wall_connector/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json index 08d52b3016b..8e86fa3d2f8 100644 --- a/homeassistant/components/tesla_wall_connector/manifest.json +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Wall Connector", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla_wall_connector", - "requirements": ["tesla-wall-connector==1.0.0"], + "requirements": ["tesla-wall-connector==1.0.1"], "dhcp": [ { "hostname": "teslawallconnector_*", diff --git a/requirements_all.txt b/requirements_all.txt index 95818018796..2f06a548d13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2302,7 +2302,7 @@ temperusb==1.5.3 tesla-powerwall==0.3.12 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.0.0 +tesla-wall-connector==1.0.1 # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbb236aff66..ae4395dc699 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1361,7 +1361,7 @@ tellduslive==0.10.11 tesla-powerwall==0.3.12 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.0.0 +tesla-wall-connector==1.0.1 # homeassistant.components.tolo tololib==0.1.0b3 From f8f381afa3c446b2764e4ab42f439767206b655d Mon Sep 17 00:00:00 2001 From: bigbadblunt Date: Thu, 9 Dec 2021 21:12:40 +0000 Subject: [PATCH 117/366] Add default value for signal_repetitions in cover (#61393) --- homeassistant/components/rfxtrx/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 0244e3aa8a0..4dc89577542 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -65,7 +65,7 @@ async def async_setup_entry( entity = RfxtrxCover( event.device, device_id, - signal_repetitions=entity_info[CONF_SIGNAL_REPETITIONS], + signal_repetitions=entity_info.get(CONF_SIGNAL_REPETITIONS, 1), venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), ) entities.append(entity) From ae26e60740072fa9e6fb2cf10fc186d3d17e5de9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 9 Dec 2021 16:12:19 -0600 Subject: [PATCH 118/366] Fix Sonos radio handling during polling (#61401) --- homeassistant/components/sonos/speaker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 05787a354f0..e12166d7795 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -994,9 +994,9 @@ class SonosSpeaker: @soco_error() def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" - variables = event and event.variables + variables = event.variables if event else {} - if variables and "transport_state" in variables: + if "transport_state" in variables: # If the transport has an error then transport_state will # not be set new_status = variables["transport_state"] @@ -1012,7 +1012,7 @@ class SonosSpeaker: update_position = new_status != self.media.playback_status self.media.playback_status = new_status - if variables and "transport_state" in variables: + if "transport_state" in variables: self.media.play_mode = variables["current_play_mode"] track_uri = ( variables["enqueued_transport_uri"] or variables["current_track_uri"] @@ -1060,7 +1060,7 @@ class SonosSpeaker: self.media.title = source self.media.source_name = source - def update_media_radio(self, variables: dict | None) -> None: + def update_media_radio(self, variables: dict) -> None: """Update state when streaming radio.""" self.media.clear_position() radio_title = None From abe6f1ab5b45e1bdc475e89739690deab3496405 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 9 Dec 2021 15:11:41 -0700 Subject: [PATCH 119/366] Consolidate SimpliSafe config flow forms into one (#61402) --- .../components/simplisafe/config_flow.py | 59 +++++++++---------- .../components/simplisafe/strings.json | 8 +-- .../simplisafe/translations/en.json | 25 +------- .../components/simplisafe/test_config_flow.py | 21 ------- 4 files changed, 31 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 8f8ec6cdc16..53fcb92f71a 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -24,7 +24,7 @@ from .const import CONF_USER_ID, DOMAIN, LOGGER CONF_AUTH_CODE = "auth_code" -STEP_INPUT_AUTH_CODE_SCHEMA = vol.Schema( +STEP_USER_SCHEMA = vol.Schema( { vol.Required(CONF_AUTH_CODE): cv.string, } @@ -54,8 +54,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._errors: dict[str, Any] = {} - self._oauth_values: SimpliSafeOAuthValues = async_get_simplisafe_oauth_values() + self._oauth_values: SimpliSafeOAuthValues | None = None self._reauth: bool = False self._username: str | None = None @@ -67,19 +66,34 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_input_auth_code( + def _async_show_form(self, *, errors: dict[str, Any] | None = None) -> FlowResult: + """Show the form.""" + self._oauth_values = async_get_simplisafe_oauth_values() + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors=errors or {}, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + + async def async_step_reauth(self, config: ConfigType) -> FlowResult: + """Handle configuration by re-auth.""" + self._username = config.get(CONF_USERNAME) + self._reauth = True + return await self.async_step_user() + + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the input of a SimpliSafe OAuth authorization code.""" + """Handle the start of the config flow.""" if user_input is None: - return self.async_show_form( - step_id="input_auth_code", data_schema=STEP_INPUT_AUTH_CODE_SCHEMA - ) + return self._async_show_form() if TYPE_CHECKING: assert self._oauth_values - self._errors = {} + errors = {} session = aiohttp_client.async_get_clientsession(self.hass) try: @@ -89,13 +103,13 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session=session, ) except InvalidCredentialsError: - self._errors = {"base": "invalid_auth"} + errors = {"base": "invalid_auth"} except SimplipyError as err: LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) - self._errors = {"base": "unknown"} + errors = {"base": "unknown"} - if self._errors: - return await self.async_step_user() + if errors: + return self._async_show_form(errors=errors) data = {CONF_USER_ID: simplisafe.user_id, CONF_TOKEN: simplisafe.refresh_token} unique_id = str(simplisafe.user_id) @@ -122,25 +136,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=unique_id, data=data) - async def async_step_reauth(self, config: ConfigType) -> FlowResult: - """Handle configuration by re-auth.""" - self._username = config.get(CONF_USERNAME) - self._reauth = True - return await self.async_step_user() - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the start of the config flow.""" - if user_input is None: - return self.async_show_form( - step_id="user", - errors=self._errors, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, - ) - - return await self.async_step_input_auth_code() - class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): """Handle a SimpliSafe options flow.""" diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 55a916bfe6b..08632cd754a 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -1,15 +1,11 @@ { "config": { "step": { - "input_auth_code": { - "title": "Finish Authorization", - "description": "Input the authorization code from the SimpliSafe web app URL:", + "user": { + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", "data": { "auth_code": "Authorization Code" } - }, - "user": { - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and click Submit." } }, "error": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 66843f86d27..3c5dd9261bc 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -12,32 +12,11 @@ "unknown": "Unexpected error" }, "step": { - "input_auth_code": { + "user": { "data": { "auth_code": "Authorization Code" }, - "description": "Input the authorization code from the SimpliSafe web app URL:", - "title": "Finish Authorization" - }, - "mfa": { - "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration.", - "title": "SimpliSafe Multi-Factor Authentication" - }, - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", - "title": "Reauthenticate Integration" - }, - "user": { - "data": { - "code": "Code (used in Home Assistant UI)", - "password": "Password", - "username": "Email" - }, - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and click Submit.", - "title": "Fill in your information." + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." } } }, diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 99943497556..0597ad377cf 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -53,9 +53,6 @@ async def test_duplicate_error(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -73,9 +70,6 @@ async def test_invalid_credentials(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -131,9 +125,6 @@ async def test_step_reauth_old_format(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -166,9 +157,6 @@ async def test_step_reauth_new_format(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -205,9 +193,6 @@ async def test_step_reauth_wrong_account(hass, api, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -229,9 +214,6 @@ async def test_step_user(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -252,9 +234,6 @@ async def test_unknown_error(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) From 509ebbc743e1166fdbb906263dc02ae4f4f184ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Dec 2021 14:40:05 -0800 Subject: [PATCH 120/366] Bump frontend to 20211209.0 (#61406) --- 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 6d419276029..c9739cd0302 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==20211206.0" + "home-assistant-frontend==20211209.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5082fe5559d..77275b0e6d8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2f06a548d13..6775cdc4f00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae4395dc699..d5092d08477 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5d65db5168e8383c30ffbb130f80213efb5b8ffd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 9 Dec 2021 15:41:13 -0700 Subject: [PATCH 121/366] Assign docs URL to a placeholder in SimpliSafe config flow (#61410) --- homeassistant/components/simplisafe/config_flow.py | 10 +++++++++- homeassistant/components/simplisafe/strings.json | 2 +- .../components/simplisafe/translations/en.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 53fcb92f71a..3a3d1963e0e 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -23,6 +23,11 @@ from homeassistant.helpers.typing import ConfigType from .const import CONF_USER_ID, DOMAIN, LOGGER CONF_AUTH_CODE = "auth_code" +CONF_DOCS_URL = "docs_url" + +AUTH_DOCS_URL = ( + "http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code" +) STEP_USER_SCHEMA = vol.Schema( { @@ -74,7 +79,10 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors or {}, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, + description_placeholders={ + CONF_URL: self._oauth_values.auth_url, + CONF_DOCS_URL: AUTH_DOCS_URL, + }, ) async def async_step_reauth(self, config: ConfigType) -> FlowResult: diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 08632cd754a..a0ff28fd689 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", + "description": "SimpliSafe authenticates with Home Assistant via the SimpliSafe web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({docs_url}) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", "data": { "auth_code": "Authorization Code" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 3c5dd9261bc..5829c68301f 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -16,7 +16,7 @@ "data": { "auth_code": "Authorization Code" }, - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." + "description": "SimpliSafe authenticates with Home Assistant via the SimpliSafe web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({docs_url}) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." } } }, From 5476b23d8b0b014beecfa2aaf686df506c899b2e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Dec 2021 14:42:14 -0800 Subject: [PATCH 122/366] Bumped version to 2021.12.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 474d2125689..bf14b073cb7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __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) From 7208cb49f1ae0c0ec354ba9ba57d3bee4523c5c5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 21:24:18 -0800 Subject: [PATCH 123/366] Disable lupusec (#61142) --- homeassistant/components/lupusec/__init__.py | 1 + homeassistant/components/lupusec/binary_sensor.py | 1 + homeassistant/components/lupusec/manifest.json | 1 + homeassistant/components/lupusec/switch.py | 1 + requirements_all.txt | 3 --- 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index 3ae07bd8105..734c7affe90 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,4 +1,5 @@ """Support for Lupusec Home Security system.""" +# pylint: disable=import-error import logging import lupupy diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index 963c82da5fa..9668b06b0ef 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Lupusec Security System binary sensors.""" +# pylint: disable=import-error from datetime import timedelta import lupupy.constants as CONST diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 6541925a5e4..ce200fe196a 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,4 +1,5 @@ { + "disabled": "Library has incompatible requirements.", "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index f35322eb773..5321d1b4f25 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -1,4 +1,5 @@ """Support for Lupusec Security System switches.""" +# pylint: disable=import-error from datetime import timedelta import lupupy.constants as CONST diff --git a/requirements_all.txt b/requirements_all.txt index 6775cdc4f00..6a7c60023a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,9 +968,6 @@ london-tube-status==0.2 # homeassistant.components.luftdaten luftdaten==0.7.1 -# homeassistant.components.lupusec -lupupy==0.0.21 - # homeassistant.components.lw12wifi lw12==0.9.2 From 6785e32683302883c84f6b6f55dc21bd4d61d611 Mon Sep 17 00:00:00 2001 From: MattWestb <49618193+MattWestb@users.noreply.github.com> Date: Fri, 10 Dec 2021 20:07:53 +0100 Subject: [PATCH 124/366] Add 2 new CN-Hysen TRVs (#61002) Adding CN-Hysen "_TZE200_pvvbommb" and "_TZE200_4eeyebrt" TRVs --- homeassistant/components/zha/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 9ef7e8fdebc..f7a1d1815db 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -614,6 +614,8 @@ class CentralitePearl(ZenWithinThermostat): "_TZE200_cwnjrr72", "_TZE200_b6wax7g0", "_TZE200_2atgpdho", + "_TZE200_pvvbommb", + "_TZE200_4eeyebrt", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", "_TYST11_cwnjrr72", From da9fbde83aac1f5140b3e9cb8a91cdc8b1515e07 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Thu, 9 Dec 2021 20:00:23 +0100 Subject: [PATCH 125/366] add missing unit of measurement in Smappee (#61365) --- homeassistant/components/smappee/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 276248fd6ae..ccae097e53f 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -250,6 +250,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): description=SmappeeSensorEntityDescription( key="load", name=measurement.name, + native_unit_of_measurement=POWER_WATT, sensor_id=measurement_id, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, From 87b50fff54609574f994caf2f3de1c1064411124 Mon Sep 17 00:00:00 2001 From: Yehuda Davis Date: Fri, 10 Dec 2021 12:19:33 -0500 Subject: [PATCH 126/366] Fix Tuya cover open/close commands (#61369) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/cover.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 275d28bae17..572d440f937 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -315,11 +315,18 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): {"code": self.entity_description.key, "value": value} ] - if (self.entity_description.set_position) is not None: + if ( + self.entity_description.set_position is not None + and self._set_position_type is not None + ): commands.append( { "code": self.entity_description.set_position, - "value": 0, + "value": round( + self._set_position_type.remap_value_from( + 100, 0, 100, reverse=True + ), + ), } ) @@ -327,7 +334,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): def close_cover(self, **kwargs: Any) -> None: """Close cover.""" - value: bool | str = True + value: bool | str = False if self.device.function[self.entity_description.key].type == "Enum": value = "close" @@ -335,11 +342,18 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): {"code": self.entity_description.key, "value": value} ] - if (self.entity_description.set_position) is not None: + if ( + self.entity_description.set_position is not None + and self._set_position_type is not None + ): commands.append( { "code": self.entity_description.set_position, - "value": 100, + "value": round( + self._set_position_type.remap_value_from( + 0, 0, 100, reverse=True + ), + ), } ) From e483c16d59abb7927a55bbf3462e796adb64f100 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:42:33 +0100 Subject: [PATCH 127/366] Remove energy entity again in Smappee local integration (#61373) --- homeassistant/components/smappee/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index ccae097e53f..595cc4da02d 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -320,7 +320,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ), ) for actuator_id, actuator in service_location.actuators.items() - if actuator.type == "SWITCH" + if actuator.type == "SWITCH" and not service_location.local_polling ] ) From c51c18781db60625b0daf2eaac64a440b730bf34 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 10 Dec 2021 00:35:20 +0100 Subject: [PATCH 128/366] Fix unique_id of S0 meters connected to Fronius inverters (#61408) --- homeassistant/components/fronius/sensor.py | 11 +- tests/components/fronius/__init__.py | 28 ++-- .../fixtures/primo_s0/GetAPIVersion.json | 5 + .../fixtures/primo_s0/GetInverterInfo.json | 33 +++++ .../GetInverterRealtimeData_Device_1.json | 64 +++++++++ .../GetInverterRealtimeData_Device_2.json | 64 +++++++++ .../fixtures/primo_s0/GetLoggerInfo.json | 29 ++++ .../primo_s0/GetMeterRealtimeData.json | 31 ++++ .../primo_s0/GetOhmPilotRealtimeData.json | 17 +++ .../primo_s0/GetPowerFlowRealtimeData.json | 45 ++++++ .../primo_s0/GetStorageRealtimeData.json | 14 ++ tests/components/fronius/test_sensor.py | 136 +++++++++++++++++- 12 files changed, 462 insertions(+), 15 deletions(-) create mode 100644 tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index b2c6ecbb820..8a1348bed14 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -784,15 +784,22 @@ class MeterSensor(_FroniusSensorEntity): self._entity_id_prefix = f"meter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) meter_data = self._device_data() + # S0 meters connected directly to inverters respond "n.a." as serial number + # `model` contains the inverter id: "S0 Meter at inverter 1" + if (meter_uid := meter_data["serial"]["value"]) == "n.a.": + meter_uid = ( + f"{coordinator.solar_net.solar_net_device_id}:" + f'{meter_data["model"]["value"]}' + ) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, meter_data["serial"]["value"])}, + identifiers={(DOMAIN, meter_uid)}, manufacturer=meter_data["manufacturer"]["value"], model=meter_data["model"]["value"], name=meter_data["model"]["value"], via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), ) - self._attr_unique_id = f'{meter_data["serial"]["value"]}-{key}' + self._attr_unique_id = f"{meter_uid}-{key}" class OhmpilotSensor(_FroniusSensorEntity): diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 683575a11c8..7930f6c01f4 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -1,4 +1,6 @@ """Tests for the Fronius integration.""" +from __future__ import annotations + from homeassistant.components.fronius.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -10,16 +12,16 @@ from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker MOCK_HOST = "http://fronius" -MOCK_UID = "123.4567890" # has to match mocked logger unique_id +MOCK_UID = "123.4567890" async def setup_fronius_integration( - hass: HomeAssistant, is_logger: bool = True + hass: HomeAssistant, is_logger: bool = True, unique_id: str = MOCK_UID ) -> ConfigEntry: """Create the Fronius integration.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=MOCK_UID, + unique_id=unique_id, # has to match mocked logger unique_id data={ CONF_HOST: MOCK_HOST, "is_logger": is_logger, @@ -35,9 +37,10 @@ def mock_responses( aioclient_mock: AiohttpClientMocker, host: str = MOCK_HOST, fixture_set: str = "symo", + inverter_ids: list[str | int] = [1], night: bool = False, ) -> None: - """Mock responses for Fronius Symo inverter with meter.""" + """Mock responses for Fronius devices.""" aioclient_mock.clear_requests() _night = "_night" if night else "" @@ -45,14 +48,15 @@ def mock_responses( f"{host}/solar_api/GetAPIVersion.cgi", text=load_fixture(f"{fixture_set}/GetAPIVersion.json", "fronius"), ) - aioclient_mock.get( - f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" - "DeviceId=1&DataCollection=CommonInverterData", - text=load_fixture( - f"{fixture_set}/GetInverterRealtimeData_Device_1{_night}.json", - "fronius", - ), - ) + for inverter_id in inverter_ids: + aioclient_mock.get( + f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" + f"DeviceId={inverter_id}&DataCollection=CommonInverterData", + text=load_fixture( + f"{fixture_set}/GetInverterRealtimeData_Device_{inverter_id}{_night}.json", + "fronius", + ), + ) aioclient_mock.get( f"{host}/solar_api/v1/GetInverterInfo.cgi", text=load_fixture(f"{fixture_set}/GetInverterInfo.json", "fronius"), diff --git a/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json new file mode 100644 index 00000000000..2051b4d58e3 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json @@ -0,0 +1,5 @@ +{ + "APIVersion" : 1, + "BaseURL" : "/solar_api/v1/", + "CompatibilityRange" : "1.6-3" +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json new file mode 100644 index 00000000000..5ac293653c0 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json @@ -0,0 +1,33 @@ +{ + "Body" : { + "Data" : { + "1" : { + "CustomName" : "Primo 5.0-1", + "DT" : 76, + "ErrorCode" : 0, + "PVPower" : 5160, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "123456" + }, + "2" : { + "CustomName" : "Primo 3.0-1", + "DT" : 81, + "ErrorCode" : 0, + "PVPower" : 3240, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "234567" + } + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:06-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json new file mode 100644 index 00000000000..e54366a5008 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json @@ -0,0 +1,64 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : 22504 + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "LEDColor" : 2, + "LEDState" : 0, + "MgmtTimerRemainingTime" : -1, + "StateToReset" : false, + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 60 + }, + "IAC" : { + "Unit" : "A", + "Value" : 3.8500000000000001 + }, + "IDC" : { + "Unit" : "A", + "Value" : 4.2300000000000004 + }, + "PAC" : { + "Unit" : "W", + "Value" : 862 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 17114940 + }, + "UAC" : { + "Unit" : "V", + "Value" : 223.90000000000001 + }, + "UDC" : { + "Unit" : "V", + "Value" : 452.30000000000001 + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : 7532755.5 + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceClass" : "Inverter", + "DeviceId" : "1", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:08-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json new file mode 100644 index 00000000000..dd1e22c0a7a --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json @@ -0,0 +1,64 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : 14237 + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "LEDColor" : 2, + "LEDState" : 0, + "MgmtTimerRemainingTime" : -1, + "StateToReset" : false, + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 60.009999999999998 + }, + "IAC" : { + "Unit" : "A", + "Value" : 1.3200000000000001 + }, + "IDC" : { + "Unit" : "A", + "Value" : 0.96999999999999997 + }, + "PAC" : { + "Unit" : "W", + "Value" : 296 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 5796010 + }, + "UAC" : { + "Unit" : "V", + "Value" : 223.59999999999999 + }, + "UDC" : { + "Unit" : "V", + "Value" : 329.5 + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : 3596193.25 + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceClass" : "Inverter", + "DeviceId" : "2", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:36:15-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json new file mode 100644 index 00000000000..1fb0bbc8577 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json @@ -0,0 +1,29 @@ +{ + "Body" : { + "LoggerInfo" : { + "CO2Factor" : 0.52999997138977051, + "CO2Unit" : "kg", + "CashCurrency" : "BRL", + "CashFactor" : 1, + "DefaultLanguage" : "en", + "DeliveryFactor" : 1, + "HWVersion" : "2.4E", + "PlatformID" : "wilma", + "ProductID" : "fronius-datamanager-card", + "SWVersion" : "3.18.7-1", + "TimezoneLocation" : "Sao_Paulo", + "TimezoneName" : "-03", + "UTCOffset" : 4294956496, + "UniqueID" : "123.4567890" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:09-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json new file mode 100644 index 00000000000..aa308bb3b69 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json @@ -0,0 +1,31 @@ +{ + "Body" : { + "Data" : { + "0" : { + "Details" : { + "Manufacturer" : "Fronius", + "Model" : "S0 Meter at inverter 1", + "Serial" : "n.a." + }, + "Enable" : 1, + "EnergyReal_WAC_Minus_Relative" : 191.25, + "Meter_Location_Current" : 1, + "PowerReal_P_Sum" : -2216.7486858112229, + "TimeStamp" : 1639074843, + "Visible" : 1 + } + } + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "Meter", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:04-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json new file mode 100644 index 00000000000..4562b45efb0 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json @@ -0,0 +1,17 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "OhmPilot", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:05-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json new file mode 100644 index 00000000000..4bbee2aec28 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json @@ -0,0 +1,45 @@ +{ + "Body" : { + "Data" : { + "Inverters" : { + "1" : { + "DT" : 76, + "E_Day" : 22502, + "E_Total" : 17114930, + "E_Year" : 7532753.5, + "P" : 886 + }, + "2" : { + "DT" : 81, + "E_Day" : 14222, + "E_Total" : 5795989.5, + "E_Year" : 3596179.75, + "P" : 948 + } + }, + "Site" : { + "E_Day" : 36724, + "E_Total" : 22910919.5, + "E_Year" : 11128933.25, + "Meter_Location" : "load", + "Mode" : "vague-meter", + "P_Akku" : null, + "P_Grid" : 384.93491437299008, + "P_Load" : -2218.9349143729901, + "P_PV" : 1834, + "rel_Autonomy" : 82.652266550064084, + "rel_SelfConsumption" : 100 + }, + "Version" : "12" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:06-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json new file mode 100644 index 00000000000..8743a2c6d68 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json @@ -0,0 +1,14 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 255, + "Reason" : "GetStorageRealtimeData request is not supported by this device.", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:05-03:00" + } +} diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index cf371a47471..2e48faf606a 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Fronius sensor platform.""" +from homeassistant.components.fronius.const import DOMAIN from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, FroniusMeterUpdateCoordinator, @@ -6,6 +7,7 @@ from homeassistant.components.fronius.coordinator import ( ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers import device_registry as dr from homeassistant.util import dt from . import enable_all_entities, mock_responses, setup_fronius_integration @@ -371,7 +373,9 @@ async def test_gen24_storage(hass, aioclient_mock): assert state.state == str(expected_state) mock_responses(aioclient_mock, fixture_set="gen24_storage") - config_entry = await setup_fronius_integration(hass, is_logger=False) + config_entry = await setup_fronius_integration( + hass, is_logger=False, unique_id="12345678" + ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 36 await enable_all_entities( @@ -469,3 +473,133 @@ async def test_gen24_storage(hass, aioclient_mock): assert_state("sensor.temperature_cell_fronius_storage_0_http_fronius", 21.5) assert_state("sensor.capacity_designed_fronius_storage_0_http_fronius", 16588) assert_state("sensor.voltage_dc_fronius_storage_0_http_fronius", 0.0) + + # Devices + device_registry = dr.async_get(hass) + + solar_net = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_12345678")} + ) + assert solar_net.configuration_url == "http://fronius" + assert solar_net.manufacturer == "Fronius" + assert solar_net.name == "SolarNet" + + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "12345678")}) + assert inverter_1.manufacturer == "Fronius" + assert inverter_1.model == "Gen24" + assert inverter_1.name == "Gen24 Storage" + + meter = device_registry.async_get_device(identifiers={(DOMAIN, "1234567890")}) + assert meter.manufacturer == "Fronius" + assert meter.model == "Smart Meter TS 65A-3" + assert meter.name == "Smart Meter TS 65A-3" + + ohmpilot = device_registry.async_get_device(identifiers={(DOMAIN, "23456789")}) + assert ohmpilot.manufacturer == "Fronius" + assert ohmpilot.model == "Ohmpilot 6" + assert ohmpilot.name == "Ohmpilot" + assert ohmpilot.sw_version == "1.0.25-3" + + storage = device_registry.async_get_device( + identifiers={(DOMAIN, "P030T020Z2001234567 ")} + ) + assert storage.manufacturer == "BYD" + assert storage.model == "BYD Battery-Box Premium HV" + assert storage.name == "BYD Battery-Box Premium HV" + + +async def test_primo_s0(hass, aioclient_mock): + """Test Fronius Primo dual inverter with S0 meter entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses(aioclient_mock, fixture_set="primo_s0", inverter_ids=[1, 2]) + config_entry = await setup_fronius_integration(hass, is_logger=True) + + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 30 + await enable_all_entities( + hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + ) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 41 + # logger + assert_state("sensor.cash_factor_fronius_logger_info_0_http_fronius", 1) + assert_state("sensor.co2_factor_fronius_logger_info_0_http_fronius", 0.53) + assert_state("sensor.delivery_factor_fronius_logger_info_0_http_fronius", 1) + # inverter 1 + assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 17114940) + assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 22504) + assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 452.3) + assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 862) + assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 4.23) + assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) + assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 7532755.5) + assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 3.85) + assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 223.9) + assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 60) + assert_state("sensor.led_color_fronius_inverter_1_http_fronius", 2) + assert_state("sensor.led_state_fronius_inverter_1_http_fronius", 0) + # inverter 2 + assert_state("sensor.energy_total_fronius_inverter_2_http_fronius", 5796010) + assert_state("sensor.energy_day_fronius_inverter_2_http_fronius", 14237) + assert_state("sensor.voltage_dc_fronius_inverter_2_http_fronius", 329.5) + assert_state("sensor.power_ac_fronius_inverter_2_http_fronius", 296) + assert_state("sensor.error_code_fronius_inverter_2_http_fronius", 0) + assert_state("sensor.current_dc_fronius_inverter_2_http_fronius", 0.97) + assert_state("sensor.status_code_fronius_inverter_2_http_fronius", 7) + assert_state("sensor.energy_year_fronius_inverter_2_http_fronius", 3596193.25) + assert_state("sensor.current_ac_fronius_inverter_2_http_fronius", 1.32) + assert_state("sensor.voltage_ac_fronius_inverter_2_http_fronius", 223.6) + assert_state("sensor.frequency_ac_fronius_inverter_2_http_fronius", 60.01) + assert_state("sensor.led_color_fronius_inverter_2_http_fronius", 2) + assert_state("sensor.led_state_fronius_inverter_2_http_fronius", 0) + # meter + assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 1) + assert_state("sensor.power_real_fronius_meter_0_http_fronius", -2216.7487) + # power_flow + assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2218.9349) + assert_state( + "sensor.power_battery_fronius_power_flow_0_http_fronius", STATE_UNKNOWN + ) + assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "vague-meter") + assert_state("sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 1834) + assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 384.9349) + assert_state( + "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100 + ) + assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 82.6523) + assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 22910919.5) + assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", 36724) + assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", 11128933.25) + + # Devices + device_registry = dr.async_get(hass) + + solar_net = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_123.4567890")} + ) + assert solar_net.configuration_url == "http://fronius" + assert solar_net.manufacturer == "Fronius" + assert solar_net.model == "fronius-datamanager-card" + assert solar_net.name == "SolarNet" + assert solar_net.sw_version == "3.18.7-1" + + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "123456")}) + assert inverter_1.manufacturer == "Fronius" + assert inverter_1.model == "Primo 5.0-1" + assert inverter_1.name == "Primo 5.0-1" + + inverter_2 = device_registry.async_get_device(identifiers={(DOMAIN, "234567")}) + assert inverter_2.manufacturer == "Fronius" + assert inverter_2.model == "Primo 3.0-1" + assert inverter_2.name == "Primo 3.0-1" + + meter = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_123.4567890:S0 Meter at inverter 1")} + ) + assert meter.manufacturer == "Fronius" + assert meter.model == "S0 Meter at inverter 1" + assert meter.name == "S0 Meter at inverter 1" From d038db01ed9f24c14cc8755017901b1a06f1ed0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:35:07 -1000 Subject: [PATCH 129/366] Fix lookin set temperature when device is off (#61411) --- homeassistant/components/lookin/climate.py | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index cab4b0968eb..356b57453bc 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -10,6 +10,7 @@ from aiolookin import Climate, MeteoSensor, SensorID from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -151,6 +152,28 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._climate.temp_celsius = int(temperature) + lookin_index = LOOKIN_HVAC_MODE_IDX_TO_HASS + if hvac_mode := kwargs.get(ATTR_HVAC_MODE): + self._climate.hvac_mode = HASS_TO_LOOKIN_HVAC_MODE[hvac_mode] + elif self._climate.hvac_mode == lookin_index.index(HVAC_MODE_OFF): + # + # If the device is off, and the user didn't specify an HVAC mode + # (which is the default when using the HA UI), the device won't turn + # on without having an HVAC mode passed. + # + # We picked the hvac mode based on the current temp if its available + # since only some units support auto, but most support either heat + # or cool otherwise we set auto since we don't have a way to make + # an educated guess. + # + meteo_data: MeteoSensor = self._meteo_coordinator.data + current_temp = meteo_data.temperature + if not current_temp: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_AUTO) + elif current_temp >= self._climate.temp_celsius: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_COOL) + else: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_HEAT) await self._async_update_conditioner() async def async_set_fan_mode(self, fan_mode: str) -> None: From 18768ad8a043ddeb8313df6ba816d133bc03a9dc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 10 Dec 2021 00:30:15 -0700 Subject: [PATCH 130/366] Bump simplisafe-python to 2021.12.1 (#61412) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 954c39efce1..0b6cb385be6 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2021.12.0"], + "requirements": ["simplisafe-python==2021.12.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 6a7c60023a1..ba4f7167c10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2143,7 +2143,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.12.0 +simplisafe-python==2021.12.1 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5092d08477..89ee14dcc1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1273,7 +1273,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.12.0 +simplisafe-python==2021.12.1 # homeassistant.components.slack slackclient==2.5.0 From 81b1b042101e9835212975da46636fcd0387cc8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:29:27 -1000 Subject: [PATCH 131/366] Fix flux_led discovery with older models (#61413) --- .../components/flux_led/config_flow.py | 9 +++++- tests/components/flux_led/__init__.py | 18 ++++++++--- tests/components/flux_led/test_config_flow.py | 30 +++++++++++++++++++ tests/components/flux_led/test_light.py | 28 ++++++++--------- tests/components/flux_led/test_number.py | 8 ++--- tests/components/flux_led/test_switch.py | 2 +- 6 files changed, 71 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 87d07bba2b1..cefecf216db 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -238,7 +238,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FluxLEDDiscovery: """Try to connect.""" self._async_abort_entries_match({CONF_HOST: host}) - if device := await async_discover_device(self.hass, host): + if (device := await async_discover_device(self.hass, host)) and device[ + ATTR_MODEL_DESCRIPTION + ]: + # Older models do not return enough information + # to build the model description via UDP so we have + # to fallback to making a tcp connection to avoid + # identifying the device as the chip model number + # AKA `HF-LPB100-ZJ200` return device bulb = async_wifi_bulb_for_host(host) try: diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 764c33686b7..6bfe02990a2 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from contextlib import contextmanager import datetime from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch @@ -162,11 +163,20 @@ def _patch_discovery(device=None, no_device=False): async def _discovery(*args, **kwargs): if no_device: raise OSError - return [FLUX_DISCOVERY] + return [] if no_device else [device or FLUX_DISCOVERY] - return patch( - "homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery - ) + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.flux_led.AIOBulbScanner.async_scan", + new=_discovery, + ), patch( + "homeassistant.components.flux_led.AIOBulbScanner.getBulbInfo", + return_value=[] if no_device else [device or FLUX_DISCOVERY], + ): + yield + + return _patcher() def _patch_wifibulb(device=None, no_device=False): diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 4c956358818..a546120ae41 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -31,6 +31,7 @@ from . import ( DEFAULT_ENTRY_TITLE, DHCP_DISCOVERY, FLUX_DISCOVERY, + FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, MODULE, @@ -435,6 +436,35 @@ async def test_discovered_by_dhcp_no_udp_response(hass): assert mock_async_setup_entry.called +async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): + """Test we can setup when discovered from dhcp but part of the udp response is missing.""" + + with _patch_discovery(no_device=True), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(device=FLUX_DISCOVERY_PARTIAL), _patch_wifibulb(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_async_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + } + assert mock_async_setup.called + assert mock_async_setup_entry.called + + async def test_discovered_by_dhcp_no_udp_response_or_tcp_response(hass): """Test we can setup when discovered from dhcp but no udp response or tcp response.""" diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 6f08ae8a307..92ea0fd8d39 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -80,7 +80,7 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -100,7 +100,7 @@ async def test_light_goes_unavailable_and_recovers(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -294,7 +294,7 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -407,7 +407,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x35) # RGB & CCT model bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -526,7 +526,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGBW} bulb.color_mode = FLUX_COLOR_MODE_RGBW - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -628,7 +628,7 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = FLUX_COLOR_MODES_RGB_W bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -739,7 +739,7 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(warm_white=1, cool_white=2) bulb.color_modes = {FLUX_COLOR_MODE_RGBWW, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGBWW - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -879,7 +879,7 @@ async def test_white_light(hass: HomeAssistant) -> None: bulb.protocol = None bulb.color_modes = {FLUX_COLOR_MODE_DIM} bulb.color_mode = FLUX_COLOR_MODE_DIM - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -930,7 +930,7 @@ async def test_no_color_modes(hass: HomeAssistant) -> None: bulb.protocol = None bulb.color_modes = set() bulb.color_mode = None - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -974,7 +974,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1056,7 +1056,7 @@ async def test_rgb_light_custom_effects_invalid_colors( bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1085,7 +1085,7 @@ async def test_rgb_light_custom_effect_via_service( bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1230,7 +1230,7 @@ async def test_addressable_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_ADDRESSABLE} bulb.color_mode = FLUX_COLOR_MODE_ADDRESSABLE - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 11df6daae4a..325307f1f32 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -41,7 +41,7 @@ async def test_number_unique_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -64,7 +64,7 @@ async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -129,7 +129,7 @@ async def test_original_addressable_light_effect_speed(hass: HomeAssistant) -> N bulb.color_mode = FLUX_COLOR_MODE_RGB bulb.effect = "7 colors change gradually" bulb.speed = 50 - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -186,7 +186,7 @@ async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None: bulb.color_mode = FLUX_COLOR_MODE_RGB bulb.effect = "RBM 1" bulb.speed = 50 - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index 852e1efd49e..b569d51e13a 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -35,7 +35,7 @@ async def test_switch_on_off(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) switch = _mocked_switch() - with _patch_discovery(device=switch), _patch_wifibulb(device=switch): + with _patch_discovery(), _patch_wifibulb(device=switch): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() From c14269d09df31a351cc1d00662b06811f8011004 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:28:35 -1000 Subject: [PATCH 132/366] Fix older v1 dimmable flux_led bulbs not turning on (#61414) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0c5e58027ce..4e38a00a677 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.3"], + "requirements": ["flux_led==0.26.5"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ba4f7167c10..00877c44edf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.3 +flux_led==0.26.5 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89ee14dcc1b..614cd2e9801 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.3 +flux_led==0.26.5 # homeassistant.components.homekit fnvhash==0.1.0 From 519ec18a047981571cf2207cb59204b71d02152c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 09:12:24 +0100 Subject: [PATCH 133/366] Correct device class for Tasmota dewpoint sensor (#61420) --- homeassistant/components/tasmota/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 45ff93b5945..961a89cfb31 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -81,6 +81,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_DEWPOINT: { + DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ICON: "mdi:weather-rainy", STATE_CLASS: SensorStateClass.MEASUREMENT, }, From 4496aeb3276e5fa7f9c60df6d213e7ab8a79afbd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 19:09:29 +0100 Subject: [PATCH 134/366] Correct recorder.statistics.get_last_statistics (#61421) --- .../components/recorder/statistics.py | 43 +++++++++++++++---- homeassistant/components/sensor/recorder.py | 4 +- tests/components/recorder/test_statistics.py | 33 ++++++++++---- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2b60e6fbf00..02c00722e72 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -834,8 +834,12 @@ def statistics_during_period( return _reduce_statistics_per_month(result) -def get_last_statistics( - hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +def _get_last_statistics( + hass: HomeAssistant, + number_of_stats: int, + statistic_id: str, + convert_units: bool, + table: type[Statistics | StatisticsShortTerm], ) -> dict[str, list[dict]]: """Return the last number_of_stats statistics for a given statistic_id.""" statistic_ids = [statistic_id] @@ -845,16 +849,19 @@ def get_last_statistics( if not metadata: return {} - baked_query = hass.data[STATISTICS_SHORT_TERM_BAKERY]( - lambda session: session.query(*QUERY_STATISTICS_SHORT_TERM) - ) + if table == StatisticsShortTerm: + bakery = STATISTICS_SHORT_TERM_BAKERY + base_query = QUERY_STATISTICS_SHORT_TERM + else: + bakery = STATISTICS_BAKERY + base_query = QUERY_STATISTICS + + baked_query = hass.data[bakery](lambda session: session.query(*base_query)) baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) metadata_id = metadata[statistic_id][0] - baked_query += lambda q: q.order_by( - StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc() - ) + baked_query += lambda q: q.order_by(table.metadata_id, table.start.desc()) baked_query += lambda q: q.limit(bindparam("number_of_stats")) @@ -874,11 +881,29 @@ def get_last_statistics( statistic_ids, metadata, convert_units, - StatisticsShortTerm, + table, None, ) +def get_last_statistics( + hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +) -> dict[str, list[dict]]: + """Return the last number_of_stats statistics for a statistic_id.""" + return _get_last_statistics( + hass, number_of_stats, statistic_id, convert_units, Statistics + ) + + +def get_last_short_term_statistics( + hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +) -> dict[str, list[dict]]: + """Return the last number_of_stats short term statistics for a statistic_id.""" + return _get_last_statistics( + hass, number_of_stats, statistic_id, convert_units, StatisticsShortTerm + ) + + def _statistics_at_time( session: scoped_session, metadata_ids: set[int], diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 25cb81ded12..3cfe8d45b70 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -517,7 +517,9 @@ def _compile_statistics( # noqa: C901 last_reset = old_last_reset = None new_state = old_state = None _sum = 0.0 - last_stats = statistics.get_last_statistics(hass, 1, entity_id, False) + last_stats = statistics.get_last_short_term_statistics( + hass, 1, entity_id, False + ) if entity_id in last_stats: # We have compiled history for this sensor before, use that as a starting point last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index d510d6ef612..c4dd33ce840 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -14,6 +14,7 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.components.recorder.statistics import ( async_add_external_statistics, + get_last_short_term_statistics, get_last_statistics, get_metadata, list_statistic_ids, @@ -40,7 +41,7 @@ def test_compile_hourly_statistics(hass_recorder): for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} recorder.do_adhoc_statistics(start=zero) @@ -91,20 +92,20 @@ def test_compile_hourly_statistics(hass_recorder): ) assert stats == {} - # Test get_last_statistics - stats = get_last_statistics(hass, 0, "sensor.test1", True) + # Test get_last_short_term_statistics + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} - stats = get_last_statistics(hass, 1, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 1, "sensor.test1", True) assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]} - stats = get_last_statistics(hass, 2, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 2, "sensor.test1", True) assert stats == {"sensor.test1": expected_stats1[::-1]} - stats = get_last_statistics(hass, 3, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 3, "sensor.test1", True) assert stats == {"sensor.test1": expected_stats1[::-1]} - stats = get_last_statistics(hass, 1, "sensor.test3", True) + stats = get_last_short_term_statistics(hass, 1, "sensor.test3", True) assert stats == {} @@ -236,7 +237,7 @@ def test_rename_entity(hass_recorder): for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} recorder.do_adhoc_statistics(start=zero) @@ -392,6 +393,22 @@ def test_external_statistics(hass_recorder, caplog): }, ) } + last_stats = get_last_statistics(hass, 1, "test:total_energy_import", True) + assert last_stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } # Update the previously inserted statistics external_statistics = { From d20496a1bcb181933beef46d84e43276ebead333 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 18:59:27 +0100 Subject: [PATCH 135/366] Correct rest sensor configured to generate timestamps (#61429) --- homeassistant/components/rest/sensor.py | 13 ++++- homeassistant/components/sensor/helpers.py | 38 +++++++++++++ homeassistant/components/template/sensor.py | 34 +---------- tests/components/rest/test_sensor.py | 63 +++++++++++++++++++++ tests/components/sensor/test_helpers.py | 38 +++++++++++++ tests/components/sensor/test_init.py | 2 +- 6 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/sensor/helpers.py create mode 100644 tests/components/sensor/test_helpers.py diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 9f8c33ad6df..422ce84cc46 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -11,8 +11,10 @@ from homeassistant.components.sensor import ( CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, ) +from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, @@ -186,4 +188,13 @@ class RestSensor(RestEntity, SensorEntity): value, None ) - self._state = value + if value is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + self._state = value + return + + self._state = async_parse_date_datetime( + value, self.entity_id, self.device_class + ) diff --git a/homeassistant/components/sensor/helpers.py b/homeassistant/components/sensor/helpers.py new file mode 100644 index 00000000000..a3f5e3827bf --- /dev/null +++ b/homeassistant/components/sensor/helpers.py @@ -0,0 +1,38 @@ +"""Helpers for sensor entities.""" +from __future__ import annotations + +from datetime import date, datetime +import logging + +from homeassistant.core import callback +from homeassistant.util import dt as dt_util + +from . import SensorDeviceClass + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_parse_date_datetime( + value: str, entity_id: str, device_class: SensorDeviceClass | str | None +) -> datetime | date | None: + """Parse datetime string to a data or datetime.""" + if device_class == SensorDeviceClass.TIMESTAMP: + if (parsed_timestamp := dt_util.parse_datetime(value)) is None: + _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) + return None + + if parsed_timestamp.tzinfo is None: + _LOGGER.warning( + "%s rendered timestamp without timezone: %s", entity_id, value + ) + return None + + return parsed_timestamp + + # Date device class + if (parsed_date := dt_util.parse_date(value)) is not None: + return parsed_date + + _LOGGER.warning("%s rendered invalid date %s", entity_id, value) + return None diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 18ae8af8569..18d0be616d4 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import date, datetime -import logging from typing import Any import voluptuous as vol @@ -17,6 +16,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, ) +from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, @@ -35,7 +35,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.util import dt as dt_util from .const import ( CONF_ATTRIBUTE_TEMPLATES, @@ -89,7 +88,6 @@ LEGACY_SENSOR_SCHEMA = vol.All( } ), ) -_LOGGER = logging.getLogger(__name__) def extra_validation_checks(val): @@ -184,32 +182,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -@callback -def _async_parse_date_datetime( - value: str, entity_id: str, device_class: SensorDeviceClass | str | None -) -> datetime | date | None: - """Parse datetime.""" - if device_class == SensorDeviceClass.TIMESTAMP: - if (parsed_timestamp := dt_util.parse_datetime(value)) is None: - _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) - return None - - if parsed_timestamp.tzinfo is None: - _LOGGER.warning( - "%s rendered timestamp without timezone: %s", entity_id, value - ) - return None - - return parsed_timestamp - - # Date device class - if (parsed_date := dt_util.parse_date(value)) is not None: - return parsed_date - - _LOGGER.warning("%s rendered invalid date %s", entity_id, value) - return None - - class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" @@ -269,7 +241,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): self._attr_native_value = result return - self._attr_native_value = _async_parse_date_datetime( + self._attr_native_value = async_parse_date_datetime( result, self.entity_id, self.device_class ) @@ -303,6 +275,6 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): ): return - self._rendered[CONF_STATE] = _async_parse_date_datetime( + self._rendered[CONF_STATE] = async_parse_date_datetime( state, self.entity_id, self.device_class ) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index d37fb047f8f..fb826eefd78 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, DATA_MEGABYTES, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, SERVICE_RELOAD, STATE_UNKNOWN, TEMP_CELSIUS, @@ -218,6 +219,68 @@ async def test_setup_get(hass): assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT +@respx.mock +async def test_setup_timestamp(hass, caplog): + """Test setup with valid configuration.""" + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "2021-11-11 11:39Z"} + ) + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "method": "GET", + "value_template": "{{ value_json.key }}", + "device_class": DEVICE_CLASS_TIMESTAMP, + "state_class": sensor.STATE_CLASS_MEASUREMENT, + } + }, + ) + await async_setup_component(hass, "homeassistant", {}) + + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 1 + + state = hass.states.get("sensor.rest_sensor") + assert state.state == "2021-11-11T11:39:00+00:00" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered invalid timestamp" not in caplog.text + assert "sensor.rest_sensor rendered timestamp without timezone" not in caplog.text + + # Bad response: Not a timestamp + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "invalid time stamp"} + ) + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.rest_sensor"]}, + blocking=True, + ) + state = hass.states.get("sensor.rest_sensor") + assert state.state == "unknown" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered invalid timestamp" in caplog.text + + # Bad response: No timezone + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "2021-10-11 11:39"} + ) + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.rest_sensor"]}, + blocking=True, + ) + state = hass.states.get("sensor.rest_sensor") + assert state.state == "unknown" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered timestamp without timezone" in caplog.text + + @respx.mock async def test_setup_get_templated_headers_params(hass): """Test setup with valid configuration.""" diff --git a/tests/components/sensor/test_helpers.py b/tests/components/sensor/test_helpers.py new file mode 100644 index 00000000000..d43443b85ba --- /dev/null +++ b/tests/components/sensor/test_helpers.py @@ -0,0 +1,38 @@ +"""The test for sensor helpers.""" +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor.helpers import async_parse_date_datetime + + +def test_async_parse_datetime(caplog): + """Test async_parse_date_datetime.""" + entity_id = "sensor.timestamp" + device_class = SensorDeviceClass.TIMESTAMP + assert ( + async_parse_date_datetime( + "2021-12-12 12:12Z", entity_id, device_class + ).isoformat() + == "2021-12-12T12:12:00+00:00" + ) + assert not caplog.text + + # No timezone + assert ( + async_parse_date_datetime("2021-12-12 12:12", entity_id, device_class) is None + ) + assert "sensor.timestamp rendered timestamp without timezone" in caplog.text + + # Invalid timestamp + assert async_parse_date_datetime("12 past 12", entity_id, device_class) is None + assert "sensor.timestamp rendered invalid timestamp: 12 past 12" in caplog.text + + device_class = SensorDeviceClass.DATE + caplog.clear() + assert ( + async_parse_date_datetime("2021-12-12", entity_id, device_class).isoformat() + == "2021-12-12" + ) + assert not caplog.text + + # Invalid date + assert async_parse_date_datetime("December 12th", entity_id, device_class) is None + assert "sensor.timestamp rendered invalid date December 12th" in caplog.text diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 67f750ece96..d5deee41679 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,4 +1,4 @@ -"""The test for sensor device automation.""" +"""The test for sensor entity.""" from datetime import date, datetime, timezone import pytest From 23cb75fe200a35c7170d70e5ec5a90a738f63b68 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 10 Dec 2021 16:40:31 +0100 Subject: [PATCH 136/366] Interim fix (#61435) --- homeassistant/components/shelly/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 27f25211a96..4109130ab80 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -68,12 +68,13 @@ from .utils import ( BLOCK_PLATFORMS: Final = [ "binary_sensor", "button", + "climate", "cover", "light", "sensor", "switch", ] -BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"] +BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) From 46808b1fc1ccd32ad899bb4c57db3879221d63d1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 10 Dec 2021 14:29:46 -0500 Subject: [PATCH 137/366] Bump ZHA quirks to 0.0.65 (#61458) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index daeb90be801..960bb55e004 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.64", + "zha-quirks==0.0.65", "zigpy-deconz==0.14.0", "zigpy==0.42.0", "zigpy-xbee==0.14.0", diff --git a/requirements_all.txt b/requirements_all.txt index 00877c44edf..5ab89d07119 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ zengge==0.2 zeroconf==0.37.0 # homeassistant.components.zha -zha-quirks==0.0.64 +zha-quirks==0.0.65 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 614cd2e9801..b4b79ca7e80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1476,7 +1476,7 @@ youless-api==0.15 zeroconf==0.37.0 # homeassistant.components.zha -zha-quirks==0.0.64 +zha-quirks==0.0.65 # homeassistant.components.zha zigpy-deconz==0.14.0 From 7b64eabde109bd49fb68a4a7769d3a362e2ad3b5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 11 Dec 2021 00:11:34 +0100 Subject: [PATCH 138/366] Small fix for device triggers and events on Hue integration (#61462) --- .../components/hue/v2/device_trigger.py | 20 +++++++++++++++++-- .../components/hue/test_device_trigger_v2.py | 14 ++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index b33b7540cb8..7a194bef746 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -40,6 +40,19 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) +DEFAULT_BUTTON_EVENT_TYPES = ( + # all except `DOUBLE_SHORT_RELEASE` + ButtonEvent.INITIAL_PRESS, + ButtonEvent.REPEAT, + ButtonEvent.SHORT_RELEASE, + ButtonEvent.LONG_RELEASE, +) + +DEVICE_SPECIFIC_EVENT_TYPES = { + # device specific overrides of specific supported button events + "Hue tap switch": (ButtonEvent.INITIAL_PRESS,), +} + async def async_validate_trigger_config( bridge: "HueBridge", @@ -84,10 +97,13 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): hue_dev_id = get_hue_device_id(device_entry) # extract triggers from all button resources of this Hue device triggers = [] + model_id = api.devices[hue_dev_id].product_data.product_name for resource in api.devices.get_sensors(hue_dev_id): if resource.type != ResourceTypes.BUTTON: continue - for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN): + for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( + model_id, DEFAULT_BUTTON_EVENT_TYPES + ): triggers.append( { CONF_DEVICE_ID: device_entry.id, @@ -95,7 +111,7 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): CONF_PLATFORM: "device", CONF_TYPE: event_type, CONF_SUBTYPE: resource.metadata.control_id, - CONF_UNIQUE_ID: device_entry.id, + CONF_UNIQUE_ID: resource.id, } ) return triggers diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index bda963552c7..e155b0adb6d 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -70,12 +70,20 @@ async def test_get_triggers(hass, mock_bridge_v2, v2_resources_test_data, device "platform": "device", "domain": hue.DOMAIN, "device_id": hue_wall_switch_device.id, - "unique_id": hue_wall_switch_device.id, + "unique_id": resource_id, "type": event_type, "subtype": control_id, } - for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN) - for control_id in range(1, 3) + for event_type in ( + ButtonEvent.INITIAL_PRESS, + ButtonEvent.LONG_RELEASE, + ButtonEvent.REPEAT, + ButtonEvent.SHORT_RELEASE, + ) + for control_id, resource_id in ( + (1, "c658d3d8-a013-4b81-8ac6-78b248537e70"), + (2, "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d"), + ) ), ] From 08eabfd056ddcc0dafde164d04689d0e0261a1c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:20:58 -1000 Subject: [PATCH 139/366] Fix non-threadsafe call to async_fire in telegram_bot (#61465) Fixes https://github.com/home-assistant/core/issues/53255#issuecomment-888111478 --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 7fd83141b7d..c79b8c5a033 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -575,7 +575,7 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag - self.hass.bus.async_fire(EVENT_TELEGRAM_SENT, event_data) + self.hass.bus.fire(EVENT_TELEGRAM_SENT, event_data) elif not isinstance(out, bool): _LOGGER.warning( "Update last message: out_type:%s, out=%s", type(out), out From dd47f0b6986abb844d416c515c7a976a01117ef4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Dec 2021 20:19:54 -1000 Subject: [PATCH 140/366] Fix exception in color_rgb_to_rgbww (#61466) --- homeassistant/util/color.py | 2 +- tests/util/test_color.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 2daccf28915..3d4f7122ad0 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -450,7 +450,7 @@ def color_rgb_to_rgbww( w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) # Find the ratio of the midpoint white in the input rgb channels - white_level = min(r / w_r, g / w_g, b / w_b) + white_level = min(r / w_r, g / w_g, b / w_b if w_b else 0) # Subtract the white portion from the rgb channels. rgb = (r - w_r * white_level, g - w_g * white_level, b - w_b * white_level) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index db9ad988aee..d806a941965 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -366,3 +366,38 @@ def test_get_color_in_voluptuous(): schema("not a color") assert schema("red") == (255, 0, 0) + + +def test_color_rgb_to_rgbww(): + """Test color_rgb_to_rgbww conversions.""" + assert color_util.color_rgb_to_rgbww(255, 255, 255, 154, 370) == ( + 0, + 54, + 98, + 255, + 255, + ) + assert color_util.color_rgb_to_rgbww(255, 255, 255, 100, 1000) == ( + 255, + 255, + 255, + 0, + 0, + ) + assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 1000) == ( + 0, + 118, + 241, + 255, + 255, + ) + assert color_util.color_rgb_to_rgbww(128, 128, 128, 154, 370) == ( + 0, + 27, + 49, + 128, + 128, + ) + assert color_util.color_rgb_to_rgbww(64, 64, 64, 154, 370) == (0, 14, 25, 64, 64) + assert color_util.color_rgb_to_rgbww(32, 64, 16, 154, 370) == (9, 64, 0, 38, 38) + assert color_util.color_rgb_to_rgbww(0, 0, 0, 154, 370) == (0, 0, 0, 0, 0) From 1f57c8ed1a809ae91fddad8ff7bf9e01b05c689f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:39:32 -1000 Subject: [PATCH 141/366] Fix missing color modes for Magic Home Ceiling Light CCT (0xE1) (#61478) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4e38a00a677..191cdef7c38 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.5"], + "requirements": ["flux_led==0.26.7"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 5ab89d07119..81b773c9d91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.5 +flux_led==0.26.7 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4b79ca7e80..d099f500416 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.5 +flux_led==0.26.7 # homeassistant.components.homekit fnvhash==0.1.0 From f10bfc961da453bb328e9b811eba9fe6e7027675 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 11 Dec 2021 13:36:48 +0100 Subject: [PATCH 142/366] Bumped version to 2021.12.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bf14b073cb7..05541f1e497 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __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) From 2e989bdfcfe80730826b7890da1b7d3135030ecd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 11 Dec 2021 17:12:33 +0100 Subject: [PATCH 143/366] Fix typo in Hue device triggers - use enum value (#61498) --- homeassistant/components/hue/v2/device_trigger.py | 2 +- tests/components/hue/test_device_trigger_v2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 7a194bef746..74863a1897e 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -109,7 +109,7 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): CONF_DEVICE_ID: device_entry.id, CONF_DOMAIN: DOMAIN, CONF_PLATFORM: "device", - CONF_TYPE: event_type, + CONF_TYPE: event_type.value, CONF_SUBTYPE: resource.metadata.control_id, CONF_UNIQUE_ID: resource.id, } diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index e155b0adb6d..0641281b9fa 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -71,7 +71,7 @@ async def test_get_triggers(hass, mock_bridge_v2, v2_resources_test_data, device "domain": hue.DOMAIN, "device_id": hue_wall_switch_device.id, "unique_id": resource_id, - "type": event_type, + "type": event_type.value, "subtype": control_id, } for event_type in ( From 608ce2d5a0de07b3d30ec17d79c13f3eccb5f376 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 11 Dec 2021 18:11:42 +0100 Subject: [PATCH 144/366] Update frontend to 20211211.0 (#61499) --- homeassistant/components/frontend/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c9739cd0302..994ac596527 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20211209.0" - ], + "requirements": ["home-assistant-frontend==20211211.0"], "dependencies": [ "api", "auth", @@ -17,8 +15,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 77275b0e6d8..18c7c1befda 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 81b773c9d91..42e3237f874 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d099f500416..9c4def0e712 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1042f23a0af68a526b0c7cb5dda0d0e203472633 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 11 Dec 2021 18:15:19 +0100 Subject: [PATCH 145/366] Bumped version to 2021.12.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 05541f1e497..f0808c28aaf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __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) From ffcb107716cc16da9b3203f139a5ece239a0c545 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 11 Dec 2021 22:14:22 -0600 Subject: [PATCH 146/366] Fix Sonos sub & surround switch state reporting (#61531) * Fix sub/surround states, refactor volume param handling * Lint --- homeassistant/components/sonos/speaker.py | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e12166d7795..30b240c9fd7 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -193,7 +193,7 @@ class SonosSpeaker: self.volume: int | None = None self.muted: bool | None = None self.night_mode: bool | None = None - self.dialog_mode: bool | None = None + self.dialog_level: bool | None = None self.cross_fade: bool | None = None self.bass: int | None = None self.treble: int | None = None @@ -498,17 +498,18 @@ class SonosSpeaker: if "mute" in variables: self.muted = variables["mute"]["Master"] == "1" - if "night_mode" in variables: - self.night_mode = variables["night_mode"] == "1" + for bool_var in ( + "dialog_level", + "night_mode", + "sub_enabled", + "surround_enabled", + ): + if bool_var in variables: + setattr(self, bool_var, variables[bool_var] == "1") - if "dialog_level" in variables: - self.dialog_mode = variables["dialog_level"] == "1" - - if "bass" in variables: - self.bass = variables["bass"] - - if "treble" in variables: - self.treble = variables["treble"] + for int_var in ("bass", "treble"): + if int_var in variables: + setattr(self, int_var, variables[int_var]) self.async_write_entity_states() @@ -982,7 +983,7 @@ class SonosSpeaker: self.volume = self.soco.volume self.muted = self.soco.mute self.night_mode = self.soco.night_mode - self.dialog_mode = self.soco.dialog_mode + self.dialog_level = self.soco.dialog_mode self.bass = self.soco.bass self.treble = self.soco.treble From a2fc870266e152e79c4ce56e76f367ff17e3b8fa Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Dec 2021 14:10:20 -0800 Subject: [PATCH 147/366] Update logic for nest media source `can_play` for events (#61537) --- homeassistant/components/nest/media_source.py | 4 ++-- tests/components/nest/test_media_source.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index b02b9b1870e..7c33e004b2b 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -24,7 +24,7 @@ import logging from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device -from google_nest_sdm.event import ImageEventBase +from google_nest_sdm.event import EventImageType, ImageEventBase from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, @@ -253,7 +253,7 @@ def _browse_event( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), ), - can_play=True, + can_play=(event.event_image_type == EventImageType.CLIP_PREVIEW), can_expand=False, thumbnail=None, children=[], diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 22ed0721eb2..f52b89c4f4d 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -231,6 +231,7 @@ async def test_camera_event(hass, auth, hass_client): assert "Person" in browse.title assert not browse.can_expand assert not browse.children + assert not browse.can_play # Resolving the event links to the media media = await media_source.async_resolve_media( @@ -302,6 +303,7 @@ async def test_event_order(hass, auth): event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand + assert not browse.can_play # Person event is next assert browse.children[1].domain == DOMAIN @@ -310,6 +312,7 @@ async def test_event_order(hass, auth): event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand + assert not browse.can_play async def test_browse_invalid_device_id(hass, auth): @@ -449,6 +452,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 + assert browse.children[0].can_play # Resolving the event links to the media media = await media_source.async_resolve_media( From e7d06e3f6a5943dafe89c59592efb380627dd17c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Dec 2021 17:10:40 -0500 Subject: [PATCH 148/366] Fix HomeKit covers with device class window and no tilt (#61566) --- .../components/homekit/type_covers.py | 17 ++++++---- tests/components/homekit/test_type_covers.py | 33 +++++++++++++++++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 0c889d9aee4..b9153ef1372 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -249,14 +249,17 @@ class OpeningDeviceBase(HomeAccessory): def async_update_state(self, new_state): """Update cover position and tilt after state changed.""" # update tilt + if not self._supports_tilt: + return current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION) - if isinstance(current_tilt, (float, int)): - # HomeKit sends values between -90 and 90. - # We'll have to normalize to [0,100] - current_tilt = (current_tilt / 100.0 * 180.0) - 90.0 - current_tilt = int(current_tilt) - self.char_current_tilt.set_value(current_tilt) - self.char_target_tilt.set_value(current_tilt) + if not isinstance(current_tilt, (float, int)): + return + # HomeKit sends values between -90 and 90. + # We'll have to normalize to [0,100] + current_tilt = (current_tilt / 100.0 * 180.0) - 90.0 + current_tilt = int(current_tilt) + self.char_current_tilt.set_value(current_tilt) + self.char_target_tilt.set_value(current_tilt) class OpeningDevice(OpeningDeviceBase, HomeAccessory): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index c357598a3df..44c9365fc04 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -223,11 +223,15 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == 75 -async def test_window_instantiate(hass, hk_driver, events): - """Test if Window accessory is instantiated correctly.""" +async def test_window_instantiate_set_position(hass, hk_driver, events): + """Test if Window accessory is instantiated correctly and can set position.""" entity_id = "cover.window" - hass.states.async_set(entity_id, None) + hass.states.async_set( + entity_id, + STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 0}, + ) await hass.async_block_till_done() acc = Window(hass, hk_driver, "Window", entity_id, 2, None) await acc.run() @@ -239,6 +243,29 @@ async def test_window_instantiate(hass, hk_driver, events): assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 + hass.states.async_set( + entity_id, + STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + ) + await hass.async_block_till_done() + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 2 + + hass.states.async_set( + entity_id, + STATE_OPEN, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, + ATTR_CURRENT_POSITION: "GARBAGE", + }, + ) + await hass.async_block_till_done() + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 2 + async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): """Test if accessory and HA update slat tilt accordingly.""" From a3ff783bc1a94bd1ee61a65cfdbb9fa99064d8d1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 12 Dec 2021 19:24:32 +0100 Subject: [PATCH 149/366] Update frontend to 20211212.0 (#61577) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 994ac596527..4380440408f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20211211.0"], + "requirements": [ + "home-assistant-frontend==20211212.0" + ], "dependencies": [ "api", "auth", @@ -15,6 +17,8 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"], + "codeowners": [ + "@home-assistant/frontend" + ], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 18c7c1befda..6c86e5804d8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211211.0 +home-assistant-frontend==20211212.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 42e3237f874..9e30da8e406 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211211.0 +home-assistant-frontend==20211212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c4def0e712..42ee947adb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211211.0 +home-assistant-frontend==20211212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0924874d4bfbb3754d5b6453bd15ab602a0bd5da Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 12 Dec 2021 23:11:41 +0100 Subject: [PATCH 150/366] Fix Hue transition calculation (#61581) --- homeassistant/components/hue/scene.py | 4 ++-- homeassistant/components/hue/v2/group.py | 4 ++-- homeassistant/components/hue/v2/light.py | 8 ++++---- tests/components/hue/test_light_v2.py | 4 ++-- tests/components/hue/test_scene.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 7335d2a048e..d67a3b097c7 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -96,8 +96,8 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): """Activate Hue scene.""" transition = kwargs.get("transition") if transition is not None: - # hue transition duration is in steps of 100 ms - transition = int(transition * 100) + # hue transition duration is in milliseconds + transition = int(transition * 1000) dynamic = kwargs.get("dynamic", self.is_dynamic) await self.bridge.async_request_call( self.controller.recall, diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 312fef6629f..8fc7ca1ebfb 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -146,8 +146,8 @@ class GroupedHueLight(HueBaseEntity, LightEntity): # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) if transition is not None: - # hue transition duration is in steps of 100 ms - transition = int(transition * 100) + # hue transition duration is in milliseconds + transition = int(transition * 1000) # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 42972f2242c..de5388e1220 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -158,8 +158,8 @@ class HueLight(HueBaseEntity, LightEntity): # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) if transition is not None: - # hue transition duration is in steps of 100 ms - transition = int(transition * 100) + # hue transition duration is in milliseconds + transition = int(transition * 1000) await self.bridge.async_request_call( self.controller.set_state, @@ -176,8 +176,8 @@ class HueLight(HueBaseEntity, LightEntity): """Turn the light off.""" transition = kwargs.get(ATTR_TRANSITION) if transition is not None: - # hue transition duration is in steps of 100 ms - transition = int(transition * 100) + # hue transition duration is in milliseconds + transition = int(transition * 1000) await self.bridge.async_request_call( self.controller.set_state, id=self.resource.id, diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 7843cab1574..362b7076a92 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -119,7 +119,7 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True - assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 600 + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): @@ -164,7 +164,7 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False - assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 600 + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 async def test_light_added(hass, mock_bridge_v2): diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 0f3d6255e86..1d270706c99 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -83,7 +83,7 @@ async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["recall"] == { "action": "active", - "duration": 600, + "duration": 6000, } From a16bf358aaee10d66547e764282aeddd8ffae697 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 12 Dec 2021 19:12:49 +0100 Subject: [PATCH 151/366] enable grouped light if enabled in previous integration (#61582) --- homeassistant/components/hue/v2/group.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 8fc7ca1ebfb..08f1dc72325 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge -from ..const import DOMAIN +from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN from .entity import HueBaseEntity ALLOWED_ERRORS = [ @@ -76,8 +76,6 @@ async def async_setup_entry( class GroupedHueLight(HueBaseEntity, LightEntity): """Representation of a Grouped Hue light.""" - # Entities for Hue groups are disabled by default - _attr_entity_registry_enabled_default = False _attr_icon = "mdi:lightbulb-group" def __init__( @@ -92,6 +90,12 @@ class GroupedHueLight(HueBaseEntity, LightEntity): self.api: HueBridgeV2 = bridge.api self._attr_supported_features |= SUPPORT_TRANSITION + # Entities for Hue groups are disabled by default + # unless they were enabled in old version (legacy option) + self._attr_entity_registry_enabled_default = bridge.config_entry.data.get( + CONF_ALLOW_HUE_GROUPS, False + ) + self._update_values() async def async_added_to_hass(self) -> None: From 22530f72f3f055b7e0933e44c5d9da7b8e4abc05 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Dec 2021 14:12:05 -0800 Subject: [PATCH 152/366] Only publish nest camera event messages once per thread and bump nest version (#61587) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/common.py | 3 +- tests/components/nest/test_events.py | 73 +++++++++++++++++++++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 11a464dbaf1..507711c73ff 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.5"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.6"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9e30da8e406..3f868ed5f4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.5 +google-nest-sdm==0.4.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42ee947adb9..f131b731171 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.5 +google-nest-sdm==0.4.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 35183a441a5..0f1d47c687e 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -62,7 +62,7 @@ class FakeSubscriber(GoogleNestSubscriber): def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" - self._callback = callback + self._device_manager.set_update_callback(callback) async def create_subscription(self): """Create the subscription.""" @@ -93,7 +93,6 @@ class FakeSubscriber(GoogleNestSubscriber): """Simulate a received pubsub message, invoked by tests.""" # Update device state, then invoke HomeAssistant to refresh await self._device_manager.async_handle_event(event_message) - await self._callback(event_message) async def async_setup_sdm_platform( diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index a2f5c21fdac..4767fd815d2 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -4,6 +4,8 @@ These tests fake out the subscriber/devicemanager, and are not using a real pubsub subscriber. """ +import datetime + from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage @@ -298,3 +300,74 @@ async def test_event_message_without_device_event(hass): await hass.async_block_till_done() assert len(events) == 0 + + +async def test_doorbell_event_thread(hass): + """Test a series of pubsub messages in the same thread.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + traits={ + "sdm.devices.traits.Info": { + "customName": "Front", + }, + "sdm.devices.traits.CameraLiveStream": {}, + "sdm.devices.traits.CameraClipPreview": {}, + "sdm.devices.traits.CameraPerson": {}, + }, + ) + registry = er.async_get(hass) + entry = registry.async_get("camera.front") + assert entry is not None + + event_message_data = { + "eventId": "some-event-id-ignored", + "resourceUpdate": { + "name": DEVICE_ID, + "events": { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:1", + }, + "sdm.devices.events.CameraClipPreview.ClipPreview": { + "eventSessionId": EVENT_SESSION_ID, + "previewUrl": "image-url-1", + }, + }, + }, + "eventThreadId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...", + "resourcegroup": [DEVICE_ID], + } + + # Publish message #1 that starts the event thread + timestamp1 = utcnow() + message_data_1 = event_message_data.copy() + message_data_1.update( + { + "timestamp": timestamp1.isoformat(timespec="seconds"), + "eventThreadState": "STARTED", + } + ) + await subscriber.async_receive_event(EventMessage(message_data_1, auth=None)) + + # Publish message #1 that sends a no-op update to end the event thread + timestamp2 = timestamp1 + datetime.timedelta(seconds=1) + message_data_2 = event_message_data.copy() + message_data_2.update( + { + "timestamp": timestamp2.isoformat(timespec="seconds"), + "eventThreadState": "ENDED", + } + ) + await subscriber.async_receive_event(EventMessage(message_data_2, auth=None)) + await hass.async_block_till_done() + + # The event is only published once + assert len(events) == 1 + assert events[0].data == { + "device_id": entry.device_id, + "type": "camera_motion", + "timestamp": timestamp1.replace(microsecond=0), + "nest_event_id": EVENT_SESSION_ID, + } From b82ddb77bc2c07e8148e220f27cf8e825175ef65 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sun, 12 Dec 2021 23:09:15 +0100 Subject: [PATCH 153/366] Fix for failing Solarlog integration in HA 2021.12 (#61602) --- homeassistant/components/solarlog/const.py | 63 ++++++++++----------- homeassistant/components/solarlog/sensor.py | 18 ++++-- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 0e9e5e8e5e0..3159bc46218 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -4,16 +4,11 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_TIMESTAMP, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -38,35 +33,35 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="time", name="last update", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SolarLogSensorEntityDescription( key="power_ac", name="power AC", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="power_dc", name="power DC", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="voltage_ac", name="voltage AC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="voltage_dc", name="voltage DC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="yield_day", @@ -101,50 +96,50 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="yield total", icon="mdi:solar-power", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL, + state_class=SensorStateClass.TOTAL, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_ac", name="consumption AC", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="consumption_day", name="consumption day", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_yesterday", name="consumption yesterday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_month", name="consumption month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_year", name="consumption year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_total", name="consumption total", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, factor=0.001, ), SolarLogSensorEntityDescription( @@ -152,31 +147,31 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="installed peak power", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), SolarLogSensorEntityDescription( key="alternator_loss", name="alternator loss", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="capacity", name="capacity", icon="mdi:solar-power", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, factor=100, ), SolarLogSensorEntityDescription( key="efficiency", name="efficiency", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, factor=100, ), SolarLogSensorEntityDescription( @@ -184,15 +179,15 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="power available", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="usage", name="usage", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, factor=100, ), ) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 5d79efb94c9..5c4c2bfad28 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -1,7 +1,8 @@ """Platform for solarlog sensors.""" from homeassistant.components.sensor import SensorEntity from homeassistant.helpers import update_coordinator -from homeassistant.helpers.entity import DeviceInfo, StateType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.util.dt import as_local from . import SolarlogData from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription @@ -38,11 +39,16 @@ class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity): ) @property - def native_value(self) -> StateType: + def native_value(self): """Return the native sensor value.""" - result = getattr(self.coordinator.data, self.entity_description.key) - if self.entity_description.factor: - state = round(result * self.entity_description.factor, 3) + if self.entity_description.key == "time": + state = as_local( + getattr(self.coordinator.data, self.entity_description.key) + ) else: - state = result + result = getattr(self.coordinator.data, self.entity_description.key) + if self.entity_description.factor: + state = round(result * self.entity_description.factor, 3) + else: + state = result return state From 14401aa84054c758fd06ead577c9f547281cdc34 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 12 Dec 2021 23:12:35 +0100 Subject: [PATCH 154/366] Fix availability for 3th party Hue lights (#61603) --- homeassistant/components/hue/v2/entity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 6dbc959fd9c..68c427fd3a5 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -103,6 +103,9 @@ class HueBaseEntity(Entity): if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY: # the zigbee connectivity sensor itself should be always available return True + if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.": + # availability status for non-philips brand lights is unreliable + return True if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): # all device-attached entities get availability from the zigbee connectivity return zigbee.status == ConnectivityServiceStatus.CONNECTED From 973eb4f6d4c43bdefdb673cf09256ec0a2629ea3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 Dec 2021 14:15:15 -0800 Subject: [PATCH 155/366] Bumped version to 2021.12.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f0808c28aaf..00e2b23df4e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -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) From db6d176658d6a5db87a0b54f183fdffce0b02eea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Dec 2021 17:17:54 -0500 Subject: [PATCH 156/366] Bump aiopvapi to 1.6.19 to fix async_timeout passing loop (#61618) --- homeassistant/components/hunterdouglas_powerview/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index ad763a33bc8..ade3b25f31c 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==1.6.14"], + "requirements": ["aiopvapi==1.6.19"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 3f868ed5f4c..d0b868bc201 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aionotion==3.0.2 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==1.6.14 +aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing aiopvpc==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f131b731171..853e8bbbe3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aionotion==3.0.2 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==1.6.14 +aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing aiopvpc==2.2.4 From 77b06bc15800988d120723568c7af37952857f55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 Dec 2021 14:28:59 -0800 Subject: [PATCH 157/366] Bump aiohue to 3.0.3 (#61627) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c789755c9a3..ee337cd3d71 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.2"], + "requirements": ["aiohue==3.0.3"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index d0b868bc201..8e079958d13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.2 +aiohue==3.0.3 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 853e8bbbe3d..e1f9e1f7351 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.2 +aiohue==3.0.3 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 599c20c76ecdb53ecc87193145fa9de11bb1f4b0 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Tue, 7 Dec 2021 19:15:51 +0100 Subject: [PATCH 158/366] Bump pysmappee to 0.2.29 (#61160) --- homeassistant/components/smappee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 91192a13484..6a1edaf41ae 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.27" + "pysmappee==0.2.29" ], "codeowners": [ "@bsmappee" diff --git a/requirements_all.txt b/requirements_all.txt index 8e079958d13..24b1073952d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1802,7 +1802,7 @@ pyskyqhub==0.1.3 pysma==0.6.9 # homeassistant.components.smappee -pysmappee==0.2.27 +pysmappee==0.2.29 # homeassistant.components.smartthings pysmartapp==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1f9e1f7351..1c8e3e05459 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1109,7 +1109,7 @@ pysignalclirestapi==0.3.4 pysma==0.6.9 # homeassistant.components.smappee -pysmappee==0.2.27 +pysmappee==0.2.29 # homeassistant.components.smartthings pysmartapp==0.3.3 From ff2e2656b3e1d72ffbbcba73a447dc7062023d4a Mon Sep 17 00:00:00 2001 From: majuss Date: Mon, 13 Dec 2021 09:07:52 +0100 Subject: [PATCH 159/366] Upgrade lupupy to 0.0.24 (#61598) --- homeassistant/components/lupusec/manifest.json | 3 +-- requirements_all.txt | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index ce200fe196a..126fa407a37 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,9 +1,8 @@ { - "disabled": "Library has incompatible requirements.", "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": ["lupupy==0.0.21"], + "requirements": ["lupupy==0.0.24"], "codeowners": ["@majuss"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 24b1073952d..ce0b2b9ea3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,6 +968,9 @@ london-tube-status==0.2 # homeassistant.components.luftdaten luftdaten==0.7.1 +# homeassistant.components.lupusec +lupupy==0.0.24 + # homeassistant.components.lw12wifi lw12==0.9.2 From 1833ab96dc5d291e669f4a67c316e9e3ef7ae188 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 13 Dec 2021 08:41:45 -0800 Subject: [PATCH 160/366] Suppress errors for legacy nest api when using media source (#61629) --- homeassistant/components/nest/media_source.py | 3 ++ tests/components/nest/test_media_source.py | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 7c33e004b2b..8b62f2f4087 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -63,6 +63,9 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]: """Return a mapping of device id to eligible Nest event media devices.""" + if DATA_SUBSCRIBER not in hass.data[DOMAIN]: + # Integration unloaded, or is legacy nest integration + return {} subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] device_manager = await subscriber.async_get_device_manager() device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index f52b89c4f4d..95f2afa8a06 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -16,6 +16,7 @@ from homeassistant.components import media_source from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const from homeassistant.components.media_source.error import Unresolvable +from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr from homeassistant.helpers.template import DATE_STR_FORMAT import homeassistant.util.dt as dt_util @@ -164,6 +165,37 @@ async def test_supported_device(hass, auth): assert len(browse.children) == 0 +async def test_integration_unloaded(hass, auth): + """Test the media player loads, but has no devices, when config unloaded.""" + await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + CAMERA_TRAITS, + ) + + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert browse.domain == DOMAIN + assert browse.identifier == "" + assert browse.title == "Nest" + assert len(browse.children) == 1 + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ConfigEntryState.NOT_LOADED + + # No devices returned + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert browse.domain == DOMAIN + assert browse.identifier == "" + assert browse.title == "Nest" + assert len(browse.children) == 0 + + async def test_camera_event(hass, auth, hass_client): """Test a media source and image created for an event.""" event_timestamp = dt_util.now() From ed041d5b7c26b61965834897410e97d9657770b0 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Mon, 13 Dec 2021 08:39:11 -0800 Subject: [PATCH 161/366] Bump total_connect_client to 2021.12 (#61634) --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 0eec41968cc..15854881ae3 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2021.11.4"], + "requirements": ["total_connect_client==2021.12"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index ce0b2b9ea3b..ca314d0c7ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2329,7 +2329,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.11.4 +total_connect_client==2021.12 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c8e3e05459..24233f27008 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.11.4 +total_connect_client==2021.12 # homeassistant.components.transmission transmissionrpc==0.11 From 791c2f4b8a36a5725aeef625bdd482631fa88233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 13 Dec 2021 17:14:37 +0100 Subject: [PATCH 162/366] Add additional-tag to machine builds (#61693) --- .github/workflows/builder.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 3f8dea3b657..e7dc7ebb270 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -131,7 +131,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.11.4 + uses: home-assistant/builder@2021.12.0 with: args: | $BUILD_ARGS \ @@ -170,6 +170,17 @@ jobs: - name: Checkout the repository uses: actions/checkout@v2.4.0 + - name: Set build additional args + run: | + # Create general tags + if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then + echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV + elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then + echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV + else + echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV + fi + - name: Login to DockerHub uses: docker/login-action@v1.10.0 with: @@ -184,7 +195,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.11.4 + uses: home-assistant/builder@2021.12.0 with: args: | $BUILD_ARGS \ From 52c96654a457938f9681586c462f82d02d0144a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Dec 2021 10:46:57 -0800 Subject: [PATCH 163/366] Bump aiohue to 3.0.4 (#61709) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index ee337cd3d71..7f424f14594 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.3"], + "requirements": ["aiohue==3.0.4"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index ca314d0c7ae..76c24fcd989 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.3 +aiohue==3.0.4 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24233f27008..1e7860c893a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.3 +aiohue==3.0.4 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 37ecbc53a7a675ae60b35013f313695d5511e6f1 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Mon, 13 Dec 2021 23:52:35 +0200 Subject: [PATCH 164/366] Update pymelcloud to 2.5.6 (#61717) --- homeassistant/components/melcloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index f875984453d..355f4c9058b 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,7 +3,7 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.5.5"], + "requirements": ["pymelcloud==2.5.6"], "codeowners": ["@vilppuvuorinen"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 76c24fcd989..775246eb32d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1631,7 +1631,7 @@ pymazda==0.2.2 pymediaroom==0.6.4.1 # homeassistant.components.melcloud -pymelcloud==2.5.5 +pymelcloud==2.5.6 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e7860c893a..fa589181636 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -995,7 +995,7 @@ pymata-express==1.19 pymazda==0.2.2 # homeassistant.components.melcloud -pymelcloud==2.5.5 +pymelcloud==2.5.6 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 From 0626bc8b4fc9943bb7ae9791f2b94a40c2f752fa Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Dec 2021 01:04:55 +0100 Subject: [PATCH 165/366] Add check for incompatible device trigger in Hue integration (#61726) --- .../components/hue/v2/device_trigger.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 74863a1897e..3f474cdf70b 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -7,6 +7,7 @@ from aiohue.v2.models.button import ButtonEvent from aiohue.v2.models.resource import ResourceTypes import voluptuous as vol +from homeassistant.components import persistent_notification from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import ( @@ -35,7 +36,7 @@ if TYPE_CHECKING: TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): int, + vol.Required(CONF_SUBTYPE): vol.Union(int, str), vol.Optional(CONF_UNIQUE_ID): str, } ) @@ -54,6 +55,33 @@ DEVICE_SPECIFIC_EVENT_TYPES = { } +def check_invalid_device_trigger( + bridge: HueBridge, + config: ConfigType, + device_entry: DeviceEntry, + automation_info: AutomationTriggerInfo | None = None, +): + """Check automation config for deprecated format.""" + # NOTE: Remove this check after 2022.6 + if isinstance(config["subtype"], int): + return + # found deprecated V1 style trigger, notify the user that it should be adjusted + msg = ( + f"Incompatible device trigger detected for " + f"[{device_entry.name}](/config/devices/device/{device_entry.id}) " + "Please manually fix the outdated automation(s) once to fix this issue." + ) + if automation_info: + automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore + msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})." + persistent_notification.async_create( + bridge.hass, + msg, + title="Outdated device trigger found", + notification_id=f"hue_trigger_{device_entry.id}", + ) + + async def async_validate_trigger_config( bridge: "HueBridge", device_entry: DeviceEntry, @@ -61,6 +89,7 @@ async def async_validate_trigger_config( ): """Validate config.""" config = TRIGGER_SCHEMA(config) + check_invalid_device_trigger(bridge, config, device_entry) return config @@ -84,6 +113,7 @@ async def async_attach_trigger( }, } ) + check_invalid_device_trigger(bridge, config, device_entry, automation_info) return await event_trigger.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) From 03b88af032d2b0e667b43ede3d2ad89f37637a73 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Dec 2021 20:24:37 +0100 Subject: [PATCH 166/366] Fix turn_off with transition for grouped Hue lights (#61728) * fix turn_off with transition for grouped hue lights * add test --- homeassistant/components/hue/v2/group.py | 31 +++++++++++++++++++----- tests/components/hue/test_light_v2.py | 31 +++++++++++++++++++++++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 08f1dc72325..e29e015f85d 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -185,12 +185,31 @@ class GroupedHueLight(HueBaseEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - await self.bridge.async_request_call( - self.controller.set_state, - id=self.resource.id, - on=False, - allowed_errors=ALLOWED_ERRORS, - ) + transition = kwargs.get(ATTR_TRANSITION) + if transition is not None: + # hue transition duration is in milliseconds + transition = int(transition * 1000) + + # NOTE: a grouped_light can only handle turn on/off + # To set other features, you'll have to control the attached lights + if transition is None: + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=False, + allowed_errors=ALLOWED_ERRORS, + ) + return + + # redirect all other feature commands to underlying lights + for light in self.controller.get_lights(self.resource.id): + await self.bridge.async_request_call( + self.api.lights.set_state, + light.id, + on=False, + transition_time=transition, + allowed_errors=ALLOWED_ERRORS, + ) @callback def on_update(self) -> None: diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 362b7076a92..f2200b0c745 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -295,7 +295,12 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): await hass.services.async_call( "light", "turn_on", - {"entity_id": test_light_id, "brightness_pct": 100, "xy_color": (0.123, 0.123)}, + { + "entity_id": test_light_id, + "brightness_pct": 100, + "xy_color": (0.123, 0.123), + "transition": 6, + }, blocking=True, ) @@ -308,6 +313,9 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): ) assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123 assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123 + assert ( + mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000 + ) # Now generate update events by emitting the json we've sent as incoming events for index in range(0, 3): @@ -346,3 +354,24 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): test_light = hass.states.get(test_light_id) assert test_light is not None assert test_light.state == "off" + + # Test calling the turn off service on a grouped light with transition + mock_bridge_v2.mock_requests.clear() + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_off", + { + "entity_id": test_light_id, + "transition": 6, + }, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False + assert ( + mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000 + ) From bbef38964de83b1af8a062cc29a0b5390f1acdc8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Dec 2021 01:27:58 +0100 Subject: [PATCH 167/366] Fix Flash effect for Hue lights (#61733) --- homeassistant/components/hue/v2/group.py | 8 ++++++++ homeassistant/components/hue/v2/light.py | 9 +++++++++ tests/components/hue/test_light_v2.py | 11 +++++++++++ 3 files changed, 28 insertions(+) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index e29e015f85d..c5f7ae5d926 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -6,16 +6,19 @@ from typing import Any from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.groups import GroupedLight, Room, Zone +from aiohue.v2.models.feature import AlertEffectType from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_FLASH, ATTR_TRANSITION, ATTR_XY_COLOR, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, ) @@ -32,6 +35,7 @@ ALLOWED_ERRORS = [ 'device (groupedLight) is "soft off", command (on) may not have effect', "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', + "attribute (supportedAlertActions) cannot be written", ] @@ -88,6 +92,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): self.group = group self.controller = controller self.api: HueBridgeV2 = bridge.api + self._attr_supported_features |= SUPPORT_FLASH self._attr_supported_features |= SUPPORT_TRANSITION # Entities for Hue groups are disabled by default @@ -146,6 +151,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) brightness = kwargs.get(ATTR_BRIGHTNESS) + flash = kwargs.get(ATTR_FLASH) if brightness is not None: # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) @@ -160,6 +166,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): and xy_color is None and color_temp is None and transition is None + and flash is None ): await self.bridge.async_request_call( self.controller.set_state, @@ -180,6 +187,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): color_xy=xy_color if light.supports_color else None, color_temp=color_temp if light.supports_color_temperature else None, transition_time=transition, + alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index de5388e1220..afb4c3d88bd 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -6,17 +6,20 @@ from typing import Any from aiohue import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.lights import LightsController +from aiohue.v2.models.feature import AlertEffectType from aiohue.v2.models.light import Light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_FLASH, ATTR_TRANSITION, ATTR_XY_COLOR, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, ) @@ -31,6 +34,7 @@ from .entity import HueBaseEntity ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', + "attribute (supportedAlertActions) cannot be written", ] @@ -68,6 +72,7 @@ class HueLight(HueBaseEntity, LightEntity): ) -> None: """Initialize the light.""" super().__init__(bridge, controller, resource) + self._attr_supported_features |= SUPPORT_FLASH self.resource = resource self.controller = controller self._supported_color_modes = set() @@ -154,6 +159,7 @@ class HueLight(HueBaseEntity, LightEntity): xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) brightness = kwargs.get(ATTR_BRIGHTNESS) + flash = kwargs.get(ATTR_FLASH) if brightness is not None: # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) @@ -169,12 +175,14 @@ class HueLight(HueBaseEntity, LightEntity): color_xy=xy_color, color_temp=color_temp, transition_time=transition, + alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" transition = kwargs.get(ATTR_TRANSITION) + flash = kwargs.get(ATTR_FLASH) if transition is not None: # hue transition duration is in milliseconds transition = int(transition * 1000) @@ -183,5 +191,6 @@ class HueLight(HueBaseEntity, LightEntity): id=self.resource.id, on=False, transition_time=transition, + alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index f2200b0c745..70a5af6d98e 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -121,6 +121,17 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 + # test again with sending flash/alert + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "flash": "long"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 3 + assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True + assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe" + async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): """Test calling the turn off service on a light.""" From 1467668c940ea59d87921fb1d4850f792b7fe9f6 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Dec 2021 01:23:32 +0100 Subject: [PATCH 168/366] Blacklist availability check for a light at startup in Hue integration (#61737) --- homeassistant/components/hue/v2/entity.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 68c427fd3a5..ae345238c23 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -47,6 +47,20 @@ class HueBaseEntity(Entity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.device.id)}, ) + # some (3th party) Hue lights report their connection status incorrectly + # causing the zigbee availability to report as disconnected while in fact + # it can be controlled. Although this is in fact something the device manufacturer + # should fix, we work around it here. If the light is reported unavailable at + # startup, we ignore the availability status of the zigbee connection + self._ignore_availability = False + if self.device is None: + return + if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): + self._ignore_availability = ( + # Official Hue lights are reliable + self.device.product_data.manufacturer_name != "Signify Netherlands B.V." + and zigbee.status != ConnectivityServiceStatus.CONNECTED + ) @property def name(self) -> str: @@ -98,13 +112,12 @@ class HueBaseEntity(Entity): def available(self) -> bool: """Return entity availability.""" if self.device is None: - # devices without a device attached should be always available + # entities without a device attached should be always available return True if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY: # the zigbee connectivity sensor itself should be always available return True - if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.": - # availability status for non-philips brand lights is unreliable + if self._ignore_availability: return True if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): # all device-attached entities get availability from the zigbee connectivity From 8cbd89282be9598c17216c1cc38fb18b686938f1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Dec 2021 01:39:51 +0100 Subject: [PATCH 169/366] Upgrade tailscale to 0.1.5 (#61744) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index 4d47e397b76..eaa51855d38 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.4"], + "requirements": ["tailscale==0.1.5"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 775246eb32d..7c884b9168d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2272,7 +2272,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.4 +tailscale==0.1.5 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa589181636..00aeb5239aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.4 +tailscale==0.1.5 # homeassistant.components.tellduslive tellduslive==0.10.11 From ffe84e8ece131c113275acdd422c9869e8836c07 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 14 Dec 2021 13:01:30 +0100 Subject: [PATCH 170/366] Bump brunt package to 1.0.1 (#61784) --- homeassistant/components/brunt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 976b017ca09..1ddbbb62f56 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,7 +3,7 @@ "name": "Brunt Blind Engine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==1.0.0"], + "requirements": ["brunt==1.0.1"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7c884b9168d..900e28d0bd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -440,7 +440,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==1.0.0 +brunt==1.0.1 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00aeb5239aa..0c6e826e221 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ broadlink==0.18.0 brother==1.1.0 # homeassistant.components.brunt -brunt==1.0.0 +brunt==1.0.1 # homeassistant.components.bsblan bsblan==0.4.0 From 34568aad89f3bfa67d9ad8dbe48d12c6039535fc Mon Sep 17 00:00:00 2001 From: MattWestb <49618193+MattWestb@users.noreply.github.com> Date: Tue, 14 Dec 2021 14:49:00 +0100 Subject: [PATCH 171/366] Fix ZHA unoccupied setpoints. (#61791) ATTR_UNOCCP_HEAT_SETPT and ATTR_UNOCCP_COOL_SETPT is mixed up. Fixing so heating is heating and cooling is colling. --- homeassistant/components/zha/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index f7a1d1815db..d57fb21b4a3 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -206,11 +206,11 @@ class Thermostat(ZhaEntity, ClimateEntity): unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint if unoccupied_cooling_setpoint is not None: - data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_cooling_setpoint + data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_cooling_setpoint unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint if unoccupied_heating_setpoint is not None: - data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_heating_setpoint + data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_heating_setpoint return data @property From 40f76d4ed95e618789fab18f5118cbfd88b54f10 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Dec 2021 10:40:37 +0100 Subject: [PATCH 172/366] Don't override pychromecast MediaController's APP ID (#61796) --- homeassistant/components/cast/media_player.py | 8 +--- tests/components/cast/test_media_player.py | 43 +++++++++++++++---- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 46c25501f3a..61922a4cd8b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -47,7 +47,6 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME from homeassistant.components.plex.services import lookup_plex_media from homeassistant.const import ( CAST_APP_ID_HOMEASSISTANT_LOVELACE, - CAST_APP_ID_HOMEASSISTANT_MEDIA, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, @@ -230,7 +229,6 @@ class CastDevice(MediaPlayerEntity): self._cast_info.cast_info, ChromeCastZeroconf.get_zeroconf(), ) - chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: @@ -527,9 +525,8 @@ class CastDevice(MediaPlayerEntity): self._chromecast.register_handler(controller) controller.play_media(media) else: - self._chromecast.media_controller.play_media( - media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {}) - ) + app_data = {"media_id": media_id, "media_type": media_type, **extra} + quick_play(self._chromecast, "homeassistant_media", app_data) def _media_status(self): """ @@ -820,7 +817,6 @@ class DynamicCastGroup: self._cast_info.cast_info, ChromeCastZeroconf.get_zeroconf(), ) - chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index adab55c50df..85562f39761 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -754,7 +754,7 @@ async def test_supported_features( assert state.attributes.get("supported_features") == supported_features -async def test_entity_play_media(hass: HomeAssistant): +async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): """Test playing media.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -776,8 +776,28 @@ async def test_entity_play_media(hass: HomeAssistant): assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # Play_media - await common.async_play_media(hass, "audio", "best.mp3", entity_id) - chromecast.media_controller.play_media.assert_called_once_with("best.mp3", "audio") + await hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: entity_id, + media_player.ATTR_MEDIA_CONTENT_TYPE: "audio", + media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3", + media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}}, + }, + blocking=True, + ) + + chromecast.media_controller.play_media.assert_not_called() + quick_play_mock.assert_called_once_with( + chromecast, + "homeassistant_media", + { + "media_id": "best.mp3", + "media_type": "audio", + "metadata": {"metadatatype": 3}, + }, + ) async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): @@ -865,7 +885,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): assert "App unknown not supported" in caplog.text -async def test_entity_play_media_sign_URL(hass: HomeAssistant): +async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock): """Test playing media.""" entity_id = "media_player.speaker" @@ -886,8 +906,10 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant): # Play_media await common.async_play_media(hass, "audio", "/best.mp3", entity_id) - chromecast.media_controller.play_media.assert_called_once_with(ANY, "audio") - assert chromecast.media_controller.play_media.call_args[0][0].startswith( + quick_play_mock.assert_called_once_with( + chromecast, "homeassistant_media", {"media_id": ANY, "media_type": "audio"} + ) + assert quick_play_mock.call_args[0][2]["media_id"].startswith( "http://example.com:8123/best.mp3?authSig=" ) @@ -1231,7 +1253,7 @@ async def test_group_media_states(hass, mz_mock): assert state.state == "playing" -async def test_group_media_control(hass, mz_mock): +async def test_group_media_control(hass, mz_mock, quick_play_mock): """Test media controls are handled by group if entity has no state.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -1286,7 +1308,12 @@ async def test_group_media_control(hass, mz_mock): # Verify play_media is not forwarded await common.async_play_media(hass, "music", "best.mp3", entity_id) assert not grp_media.play_media.called - assert chromecast.media_controller.play_media.called + assert not chromecast.media_controller.play_media.called + quick_play_mock.assert_called_once_with( + chromecast, + "homeassistant_media", + {"media_id": "best.mp3", "media_type": "music"}, + ) async def test_failed_cast_on_idle(hass, caplog): From 32bdcdd663af1d67f8df673b0fd8ee6e1a036b93 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Dec 2021 10:40:06 +0100 Subject: [PATCH 173/366] Bump pychromecast to 10.2.1 (#61811) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 3f3c31b8d3d..bee18948a33 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.1.1"], + "requirements": ["pychromecast==10.2.1"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 900e28d0bd1..04a35c673c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1396,7 +1396,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.1.1 +pychromecast==10.2.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c6e826e221..7dff2d9a1e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.1.1 +pychromecast==10.2.1 # homeassistant.components.climacell pyclimacell==0.18.2 From b51330136321e4cce7daa3cb6526c70b3be8b665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 14 Dec 2021 18:40:47 +0100 Subject: [PATCH 174/366] Tibber, update library, fixes #61525 (#61813) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index a653e91b991..c0b047a2856 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.21.0"], + "requirements": ["pyTibber==0.21.1"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 04a35c673c3..451fc9b9c3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1327,7 +1327,7 @@ pyRFXtrx==0.27.0 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.21.0 +pyTibber==0.21.1 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7dff2d9a1e6..b0de95305ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ pyMetno==0.9.0 pyRFXtrx==0.27.0 # homeassistant.components.tibber -pyTibber==0.21.0 +pyTibber==0.21.1 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 1faa111222f4fcceb41ed02db71f7fc2c8092333 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 14 Dec 2021 22:36:55 +0100 Subject: [PATCH 175/366] Bump python-miio to 0.5.9.2 (#61831) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 757fca8be1f..8de844cdd44 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.1"], + "requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 451fc9b9c3a..3a403b690a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1904,7 +1904,7 @@ python-kasa==0.4.0 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.9.1 +python-miio==0.5.9.2 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0de95305ab..b07994d9b92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1145,7 +1145,7 @@ python-juicenet==1.0.2 python-kasa==0.4.0 # homeassistant.components.xiaomi_miio -python-miio==0.5.9.1 +python-miio==0.5.9.2 # homeassistant.components.nest python-nest==4.1.0 From 77b1df59022f7f84c8894c0330aa176eb45d5da2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 15 Dec 2021 05:17:09 -0700 Subject: [PATCH 176/366] Ensure SimpliSafe websocket reconnects upon new token (#61835) --- homeassistant/components/simplisafe/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index cd04de5d34c..dd3ff717e28 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -612,10 +612,24 @@ class SimpliSafe: data={**self.entry.data, CONF_TOKEN: token}, ) + @callback + def async_handle_refresh_token(token: str) -> None: + """Handle a new refresh token.""" + async_save_refresh_token(token) + + if TYPE_CHECKING: + assert self._api.websocket + + if self._api.websocket.connected: + # If a websocket connection is open, reconnect it to use the + # new access token: + asyncio.create_task(self._api.websocket.async_reconnect()) + self.entry.async_on_unload( - self._api.add_refresh_token_callback(async_save_refresh_token) + self._api.add_refresh_token_callback(async_handle_refresh_token) ) + # Save the refresh token we got on entry setup: async_save_refresh_token(self._api.refresh_token) async def async_update(self) -> None: From f271fea07c413cb5404dab4848fc4a235901d482 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 15 Dec 2021 13:15:56 +0100 Subject: [PATCH 177/366] Allow setting local_ip for knx routing connections (#61836) --- homeassistant/components/knx/__init__.py | 1 + homeassistant/components/knx/config_flow.py | 19 ++++- homeassistant/components/knx/strings.json | 7 +- .../components/knx/translations/en.json | 5 +- tests/components/knx/test_config_flow.py | 64 ++++++++++++++- tests/components/knx/test_init.py | 78 +++++++++++++++++++ 6 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 tests/components/knx/test_init.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index ba6689a023d..5a66824fbcb 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -383,6 +383,7 @@ class KNXModule: if _conn_type == CONF_KNX_ROUTING: return ConnectionConfig( connection_type=ConnectionType.ROUTING, + local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), auto_reconnect=True, ) if _conn_type == CONF_KNX_TUNNELING: diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 30071752731..96aa8f67e3b 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -137,9 +137,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False ): vol.Coerce(bool), - vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, } + if self.show_advanced_options: + fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str + return self.async_show_form( step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors ) @@ -195,6 +197,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_KNX_INDIVIDUAL_ADDRESS: user_input[ CONF_KNX_INDIVIDUAL_ADDRESS ], + ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ), CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) @@ -211,6 +216,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): cv.port, } + if self.show_advanced_options: + fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str + return self.async_show_form( step_id="routing", data_schema=vol.Schema(fields), errors=errors ) @@ -306,7 +314,6 @@ class KNXOptionsFlowHandler(OptionsFlow): vol.Required( CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) ): cv.port, - vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=self.current_config.get( @@ -381,6 +388,14 @@ class KNXOptionsFlowHandler(OptionsFlow): } if self.show_advanced_options: + data_schema[ + vol.Optional( + ConnectionSchema.CONF_KNX_LOCAL_IP, + default=self.current_config.get( + ConnectionSchema.CONF_KNX_LOCAL_IP, + ), + ) + ] = str data_schema[ vol.Required( ConnectionSchema.CONF_KNX_STATE_UPDATER, diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 7f770c25427..4db92888aab 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -28,7 +28,8 @@ "data": { "individual_address": "Individual address for the routing connection", "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing" + "multicast_port": "The multicast port used for routing", + "local_ip": "Local IP (leave empty if unsure)" } } }, @@ -48,6 +49,7 @@ "individual_address": "Default individual address", "multicast_group": "Multicast group used for routing and discovery", "multicast_port": "Multicast port used for routing and discovery", + "local_ip": "Local IP (leave empty if unsure)", "state_updater": "Globally enable reading states from the KNX Bus", "rate_limit": "Maximum outgoing telegrams per second" } @@ -56,8 +58,7 @@ "data": { "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", - "route_back": "Route Back / NAT Mode", - "local_ip": "Local IP (leave empty if unsure)" + "route_back": "Route Back / NAT Mode" } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 5320f0cfb03..91b9dfce5f3 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -22,7 +22,8 @@ "data": { "individual_address": "Individual address for the routing connection", "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing" + "multicast_port": "The multicast port used for routing", + "local_ip": "Local IP (leave empty if unsure)" }, "description": "Please configure the routing options." }, @@ -48,6 +49,7 @@ "individual_address": "Default individual address", "multicast_group": "Multicast group used for routing and discovery", "multicast_port": "Multicast port used for routing and discovery", + "local_ip": "Local IP (leave empty if unsure)", "rate_limit": "Maximum outgoing telegrams per second", "state_updater": "Globally enable reading states from the KNX Bus" } @@ -55,7 +57,6 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" } diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index ff1fc362aa5..65289c2b173 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -83,6 +83,60 @@ async def test_routing_setup(hass: HomeAssistant) -> None: CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_routing_setup_advanced(hass: HomeAssistant) -> None: + """Test routing setup with advanced options.""" + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [] + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_USER, + "show_advanced_options": True, + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "routing" + assert not result2["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == CONF_KNX_ROUTING.capitalize() + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", } @@ -144,7 +198,11 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: gateways.return_value = [gateway] result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, + context={ + "source": config_entries.SOURCE_USER, + "show_advanced_options": True, + }, ) assert result["type"] == RESULT_TYPE_FORM assert not result["errors"] @@ -563,7 +621,6 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) @@ -581,7 +638,6 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } @@ -611,6 +667,7 @@ async def test_advanced_options( ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) @@ -626,4 +683,5 @@ async def test_advanced_options( ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py new file mode 100644 index 00000000000..4380b132cbd --- /dev/null +++ b/tests/components/knx/test_init.py @@ -0,0 +1,78 @@ +"""Test KNX init.""" +import pytest +from xknx import XKNX +from xknx.io import ConnectionConfig, ConnectionType + +from homeassistant.components.knx.const import ( + CONF_KNX_AUTOMATIC, + CONF_KNX_CONNECTION_TYPE, + CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_ROUTING, + CONF_KNX_TUNNELING, + DOMAIN as KNX_DOMAIN, +) +from homeassistant.components.knx.schema import ConnectionSchema +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "config_entry_data,connection_config", + [ + ( + { + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + }, + ConnectionConfig(), + ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.1", + }, + ConnectionConfig( + connection_type=ConnectionType.ROUTING, local_ip="192.168.1.1" + ), + ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ConnectionConfig( + connection_type=ConnectionType.TUNNELING, + route_back=False, + gateway_ip="192.168.0.2", + gateway_port=3675, + local_ip="192.168.1.112", + auto_reconnect=True, + ), + ), + ], +) +async def test_init_connection_handling( + hass: HomeAssistant, knx: KNXTestKit, config_entry_data, connection_config +): + """Test correctly generating connection config.""" + + config_entry = MockConfigEntry( + title="KNX", + domain=KNX_DOMAIN, + data=config_entry_data, + ) + knx.mock_config_entry = config_entry + await knx.setup_integration({}) + + assert hass.data.get(KNX_DOMAIN) is not None + + assert ( + hass.data[KNX_DOMAIN].connection_config().__dict__ == connection_config.__dict__ + ) From 686f6768fcab2051aa14856fb02a3fd2b1a47770 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Wed, 15 Dec 2021 07:13:59 -0500 Subject: [PATCH 178/366] Fix broken Environment Canada (#61848) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index b340674b480..868e62f07c3 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.18"], + "requirements": ["env_canada==0.5.20"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 3a403b690a6..638c8e4f353 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -600,7 +600,7 @@ enocean==0.50 enturclient==0.2.2 # homeassistant.components.environment_canada -env_canada==0.5.18 +env_canada==0.5.20 # homeassistant.components.envirophat # envirophat==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b07994d9b92..0d23c6d8706 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -375,7 +375,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.18 +env_canada==0.5.20 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 From 86622794e0027ccc1628999bafca944edb5472ff Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 15 Dec 2021 04:12:38 -0800 Subject: [PATCH 179/366] Bump google-nest-sdm to 0.4.8 (#61851) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/test_events.py | 115 ++++++++++++++++---- 4 files changed, 98 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 507711c73ff..0c14e59babc 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.6"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.8"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 638c8e4f353..b179629589c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.6 +google-nest-sdm==0.4.8 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d23c6d8706..b39b82b89f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.6 +google-nest-sdm==0.4.8 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 4767fd815d2..4a625999155 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -39,13 +39,12 @@ async def async_setup_devices(hass, device_type, traits={}): return await async_setup_sdm_platform(hass, PLATFORM, devices=devices) -def create_device_traits(event_trait): +def create_device_traits(event_traits=[]): """Create fake traits for a device.""" - return { + result = { "sdm.devices.traits.Info": { "customName": "Front", }, - event_trait: {}, "sdm.devices.traits.CameraLiveStream": { "maxVideoResolution": { "width": 640, @@ -55,6 +54,8 @@ def create_device_traits(event_trait): "audioCodecs": ["AAC"], }, } + result.update({t: {} for t in event_traits}) + return result def create_event(event_type, device_id=DEVICE_ID, timestamp=None): @@ -91,7 +92,7 @@ async def test_doorbell_chime_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.DoorbellChime"), + create_device_traits(["sdm.devices.traits.DoorbellChime"]), ) registry = er.async_get(hass) @@ -129,7 +130,7 @@ async def test_camera_motion_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.CAMERA", - create_device_traits("sdm.devices.traits.CameraMotion"), + create_device_traits(["sdm.devices.traits.CameraMotion"]), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -157,7 +158,7 @@ async def test_camera_sound_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.CAMERA", - create_device_traits("sdm.devices.traits.CameraSound"), + create_device_traits(["sdm.devices.traits.CameraSound"]), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -185,7 +186,7 @@ async def test_camera_person_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.CameraEventImage"), + create_device_traits(["sdm.devices.traits.CameraPerson"]), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -213,7 +214,9 @@ async def test_camera_multiple_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.CameraEventImage"), + create_device_traits( + ["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"] + ), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -256,7 +259,7 @@ async def test_unknown_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.DoorbellChime"), + create_device_traits(["sdm.devices.traits.DoorbellChime"]), ) await subscriber.async_receive_event(create_event("some-event-id")) await hass.async_block_till_done() @@ -270,7 +273,7 @@ async def test_unknown_device_id(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.DoorbellChime"), + create_device_traits(["sdm.devices.traits.DoorbellChime"]), ) await subscriber.async_receive_event( create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id") @@ -286,7 +289,7 @@ async def test_event_message_without_device_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.DoorbellChime"), + create_device_traits(["sdm.devices.traits.DoorbellChime"]), ) timestamp = utcnow() event = EventMessage( @@ -308,14 +311,12 @@ async def test_doorbell_event_thread(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - traits={ - "sdm.devices.traits.Info": { - "customName": "Front", - }, - "sdm.devices.traits.CameraLiveStream": {}, - "sdm.devices.traits.CameraClipPreview": {}, - "sdm.devices.traits.CameraPerson": {}, - }, + create_device_traits( + [ + "sdm.devices.traits.CameraClipPreview", + "sdm.devices.traits.CameraPerson", + ] + ), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -351,7 +352,7 @@ async def test_doorbell_event_thread(hass): ) await subscriber.async_receive_event(EventMessage(message_data_1, auth=None)) - # Publish message #1 that sends a no-op update to end the event thread + # Publish message #2 that sends a no-op update to end the event thread timestamp2 = timestamp1 + datetime.timedelta(seconds=1) message_data_2 = event_message_data.copy() message_data_2.update( @@ -371,3 +372,77 @@ async def test_doorbell_event_thread(hass): "timestamp": timestamp1.replace(microsecond=0), "nest_event_id": EVENT_SESSION_ID, } + + +async def test_doorbell_event_session_update(hass): + """Test a pubsub message with updates to an existing session.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits( + [ + "sdm.devices.traits.CameraClipPreview", + "sdm.devices.traits.CameraPerson", + "sdm.devices.traits.CameraMotion", + ] + ), + ) + registry = er.async_get(hass) + entry = registry.async_get("camera.front") + assert entry is not None + + # Message #1 has a motion event + timestamp1 = utcnow() + await subscriber.async_receive_event( + create_events( + { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:1", + }, + "sdm.devices.events.CameraClipPreview.ClipPreview": { + "eventSessionId": EVENT_SESSION_ID, + "previewUrl": "image-url-1", + }, + }, + timestamp=timestamp1, + ) + ) + + # Message #2 has an extra person event + timestamp2 = utcnow() + await subscriber.async_receive_event( + create_events( + { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:1", + }, + "sdm.devices.events.CameraPerson.Person": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:2", + }, + "sdm.devices.events.CameraClipPreview.ClipPreview": { + "eventSessionId": EVENT_SESSION_ID, + "previewUrl": "image-url-1", + }, + }, + timestamp=timestamp2, + ) + ) + await hass.async_block_till_done() + + assert len(events) == 2 + assert events[0].data == { + "device_id": entry.device_id, + "type": "camera_motion", + "timestamp": timestamp1.replace(microsecond=0), + "nest_event_id": EVENT_SESSION_ID, + } + assert events[1].data == { + "device_id": entry.device_id, + "type": "camera_person", + "timestamp": timestamp2.replace(microsecond=0), + "nest_event_id": EVENT_SESSION_ID, + } From 2772bae2e1271c975d758e0f2c65655addef3560 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 15 Dec 2021 12:15:05 +0100 Subject: [PATCH 180/366] Bump aiohue to 3.0.5 (#61875) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 7f424f14594..f32d8edc284 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.4"], + "requirements": ["aiohue==3.0.5"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index b179629589c..d792fa1afc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.4 +aiohue==3.0.5 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b39b82b89f4..d707eaa5d34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.4 +aiohue==3.0.5 # homeassistant.components.apache_kafka aiokafka==0.6.0 From e7e20533bd6f78f35b816cd784e67516b3d8f587 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 15 Dec 2021 15:15:47 +0100 Subject: [PATCH 181/366] Update frontend to 20211215.0 (#61877) --- 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 4380440408f..4ac95c38afc 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==20211212.0" + "home-assistant-frontend==20211215.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6c86e5804d8..e52542ccea0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211212.0 +home-assistant-frontend==20211215.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index d792fa1afc8..0631714600b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211212.0 +home-assistant-frontend==20211215.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d707eaa5d34..780a343463f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211212.0 +home-assistant-frontend==20211215.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From ac2897fc67a9c78b2f443ff5ff7bd99a9ccc312b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 15 Dec 2021 16:04:48 +0100 Subject: [PATCH 182/366] Bumped version to 2021.12.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 00e2b23df4e..6fd2760d618 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -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) From 5a03fffc20c11c2e05926ffc4b977aa9cf665e07 Mon Sep 17 00:00:00 2001 From: sindudas Date: Fri, 17 Dec 2021 09:34:27 +0100 Subject: [PATCH 183/366] Update ebusdpy version (#59899) --- homeassistant/components/ebusd/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index 347fee0bc85..390e8efe7d5 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -2,7 +2,7 @@ "domain": "ebusd", "name": "ebusd", "documentation": "https://www.home-assistant.io/integrations/ebusd", - "requirements": ["ebusdpy==0.0.16"], + "requirements": ["ebusdpy==0.0.17"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0631714600b..c0048a7cafc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -573,7 +573,7 @@ dweepy==0.3.0 dynalite_devices==0.1.46 # homeassistant.components.ebusd -ebusdpy==0.0.16 +ebusdpy==0.0.17 # homeassistant.components.ecoal_boiler ecoaliface==0.4.0 From 499cc2e51d6411498284bf81f0a37de56a034e36 Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 17 Dec 2021 00:39:55 -0800 Subject: [PATCH 184/366] Nextbus upcoming sort as integer (#61416) --- homeassistant/components/nextbus/sensor.py | 2 +- tests/components/nextbus/test_sensor.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 3756c1853b7..815875df0ca 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -214,7 +214,7 @@ class NextBusDepartureSensor(SensorEntity): # Generate list of upcoming times self._attributes["upcoming"] = ", ".join( - sorted(p["minutes"] for p in predictions) + sorted((p["minutes"] for p in predictions), key=int) ) latest_prediction = maybe_first(predictions) diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 016afed2b0f..f113d5c83da 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -40,6 +40,7 @@ BASIC_RESULTS = { {"minutes": "1", "epochTime": "1553807371000"}, {"minutes": "2", "epochTime": "1553807372000"}, {"minutes": "3", "epochTime": "1553807373000"}, + {"minutes": "10", "epochTime": "1553807380000"}, ], }, } @@ -128,7 +129,7 @@ async def test_verify_valid_state( assert state.attributes["route"] == VALID_ROUTE_TITLE assert state.attributes["stop"] == VALID_STOP_TITLE assert state.attributes["direction"] == "Outbound" - assert state.attributes["upcoming"] == "1, 2, 3" + assert state.attributes["upcoming"] == "1, 2, 3, 10" async def test_message_dict( From c73319e162e3403439b21b051004768046a2efc6 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 16 Dec 2021 14:08:40 +0100 Subject: [PATCH 185/366] Add restore logic to Shelly climate platform (#61632) * Add restore logic to Shelly climate platform * Handle missing channel on restore --- homeassistant/components/shelly/__init__.py | 3 +- homeassistant/components/shelly/climate.py | 198 +++++++++++++++++--- 2 files changed, 172 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 4109130ab80..27f25211a96 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -68,13 +68,12 @@ from .utils import ( BLOCK_PLATFORMS: Final = [ "binary_sensor", "button", - "climate", "cover", "light", "sensor", "switch", ] -BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] +BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"] RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index f2db157ecf2..af84e881447 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -3,12 +3,13 @@ from __future__ import annotations import asyncio import logging +from types import MappingProxyType from typing import Any, Final, cast from aioshelly.block_device import Block import async_timeout -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -20,11 +21,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.components.shelly import BlockDeviceWrapper -from homeassistant.components.shelly.entity import ShellyBlockEntity from homeassistant.components.shelly.utils import get_device_entry_gen from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers import device_registry, entity, entity_registry +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -49,10 +51,30 @@ async def async_setup_entry( if get_device_entry_gen(config_entry) == 2: return + wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ][BLOCK] + + if wrapper.device.initialized: + await async_setup_climate_entities(async_add_entities, wrapper) + else: + await async_restore_climate_entities( + hass, config_entry, async_add_entities, wrapper + ) + + +async def async_setup_climate_entities( + async_add_entities: AddEntitiesCallback, + wrapper: BlockDeviceWrapper, +) -> None: + """Set up online climate devices.""" + + _LOGGER.info("Setup online climate device %s", wrapper.name) device_block: Block | None = None sensor_block: Block | None = None - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + assert wrapper.device.blocks + for block in wrapper.device.blocks: if block.type == "device": device_block = block @@ -60,10 +82,37 @@ async def async_setup_entry( sensor_block = block if sensor_block and device_block: - async_add_entities([ShellyClimate(wrapper, sensor_block, device_block)]) + async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) -class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): +async def async_restore_climate_entities( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + wrapper: BlockDeviceWrapper, +) -> None: + """Restore sleeping climate devices.""" + _LOGGER.info("Setup sleeping climate device %s", wrapper.name) + + ent_reg = await entity_registry.async_get_registry(hass) + entries = entity_registry.async_entries_for_config_entry( + ent_reg, config_entry.entry_id + ) + + for entry in entries: + + if entry.domain != CLIMATE_DOMAIN: + continue + + _LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) + async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)]) + + +class BlockSleepingClimate( + RestoreEntity, + ClimateEntity, + entity.Entity, +): """Representation of a Shelly climate device.""" _attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT] @@ -74,45 +123,77 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = TEMP_CELSIUS + # pylint: disable=super-init-not-called def __init__( - self, wrapper: BlockDeviceWrapper, sensor_block: Block, device_block: Block + self, + wrapper: BlockDeviceWrapper, + sensor_block: Block | None, + device_block: Block | None, + entry: entity_registry.RegistryEntry | None = None, ) -> None: """Initialize climate.""" - super().__init__(wrapper, sensor_block) - - self.device_block = device_block - - assert self.block.channel + self.wrapper = wrapper + self.block: Block | None = sensor_block self.control_result: dict[str, Any] | None = None + self.device_block: Block | None = device_block + self.last_state: State | None = None + self.last_state_attributes: MappingProxyType[str, Any] + self._preset_modes: list[str] = [] - self._attr_name = self.wrapper.name - self._attr_unique_id = self.wrapper.mac - self._attr_preset_modes: list[str] = [ - PRESET_NONE, - *wrapper.device.settings["thermostats"][int(self.block.channel)][ - "schedule_profile_names" - ], - ] + if self.block is not None and self.device_block is not None: + self._unique_id = f"{self.wrapper.mac}-{self.block.description}" + assert self.block.channel + self._preset_modes = [ + PRESET_NONE, + *wrapper.device.settings["thermostats"][int(self.block.channel)][ + "schedule_profile_names" + ], + ] + elif entry is not None: + self._unique_id = entry.unique_id + + @property + def unique_id(self) -> str: + """Set unique id of entity.""" + return self._unique_id + + @property + def name(self) -> str: + """Name of entity.""" + return self.wrapper.name + + @property + def should_poll(self) -> bool: + """If device should be polled.""" + return False @property def target_temperature(self) -> float | None: """Set target temperature.""" - return cast(float, self.block.targetTemp) + if self.block is not None: + return cast(float, self.block.targetTemp) + return self.last_state_attributes.get("temperature") @property def current_temperature(self) -> float | None: """Return current temperature.""" - return cast(float, self.block.temp) + if self.block is not None: + return cast(float, self.block.temp) + return self.last_state_attributes.get("current_temperature") @property def available(self) -> bool: """Device availability.""" - return not cast(bool, self.device_block.valveError) + if self.device_block is not None: + return not cast(bool, self.device_block.valveError) + return True @property def hvac_mode(self) -> str: """HVAC current mode.""" + if self.device_block is None: + return self.last_state.state if self.last_state else HVAC_MODE_OFF if self.device_block.mode is None or self._check_is_off(): return HVAC_MODE_OFF @@ -121,20 +202,45 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): @property def preset_mode(self) -> str | None: """Preset current mode.""" + if self.device_block is None: + return self.last_state_attributes.get("preset_mode") if self.device_block.mode is None: - return None - return self._attr_preset_modes[cast(int, self.device_block.mode)] + return PRESET_NONE + return self._preset_modes[cast(int, self.device_block.mode)] @property def hvac_action(self) -> str | None: """HVAC current action.""" - if self.device_block.status is None or self._check_is_off(): + if ( + self.device_block is None + or self.device_block.status is None + or self._check_is_off() + ): return CURRENT_HVAC_OFF return ( CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT ) + @property + def preset_modes(self) -> list[str]: + """Preset available modes.""" + return self._preset_modes + + @property + def device_info(self) -> DeviceInfo: + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + + @property + def channel(self) -> str | None: + """Device channel.""" + if self.block is not None: + return self.block.channel + return self.last_state_attributes.get("channel") + def _check_is_off(self) -> bool: """Return if valve is off or on.""" return bool( @@ -148,7 +254,7 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.wrapper.device.http_request( - "get", f"thermostat/{self.block.channel}", kwargs + "get", f"thermostat/{self.channel}", kwargs ) except (asyncio.TimeoutError, OSError) as err: _LOGGER.error( @@ -186,3 +292,41 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): await self.set_state_full_path( schedule=1, schedule_profile=f"{preset_index}" ) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + _LOGGER.info("Restoring entity %s", self.name) + + last_state = await self.async_get_last_state() + + if last_state is not None: + self.last_state = last_state + self.last_state_attributes = self.last_state.attributes + self._preset_modes = cast( + list, self.last_state.attributes.get("preset_modes") + ) + + self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) + + async def async_update(self) -> None: + """Update entity with latest info.""" + await self.wrapper.async_request_refresh() + + @callback + def _update_callback(self) -> None: + """Handle device update.""" + if not self.wrapper.device.initialized: + self.async_write_ha_state() + return + + assert self.wrapper.device.blocks + + for block in self.wrapper.device.blocks: + if block.type == "device": + self.device_block = block + if hasattr(block, "targetTemp"): + self.block = block + + _LOGGER.debug("Entity %s attached to block", self.name) + self.async_write_ha_state() + return From cb8968887341cb0f8c636dc3286c34feb8ef76b8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 14:06:38 +0100 Subject: [PATCH 186/366] Fix OwnetError preventing onewire initialisation (#61696) Co-authored-by: epenet --- homeassistant/components/onewire/__init__.py | 7 ++++++- tests/components/onewire/test_init.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 753d30e5958..70a0a5fc856 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -1,6 +1,8 @@ """The 1-Wire component.""" import logging +from pyownet import protocol + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -18,7 +20,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: onewirehub = OneWireHub(hass) try: await onewirehub.initialize(entry) - except CannotConnect as exc: + except ( + CannotConnect, # Failed to connect to the server + protocol.OwnetError, # Connected to server, but failed to list the devices + ) as exc: raise ConfigEntryNotReady() from exc hass.data[DOMAIN][entry.entry_id] = onewirehub diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index e3a3fdcc564..763cdc9c071 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,6 +1,8 @@ """Tests for 1-Wire config flow.""" import logging +from unittest.mock import MagicMock +from pyownet import protocol import pytest from homeassistant.components.onewire.const import DOMAIN @@ -19,6 +21,20 @@ async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: Confi assert not hass.data.get(DOMAIN) +async def test_owserver_listing_failure( + hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock +): + """Test listing failure raises ConfigEntryNotReady.""" + owproxy.return_value.dir.side_effect = protocol.OwnetError() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) + + @pytest.mark.usefixtures("owproxy") async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Test being able to unload an entry.""" From 8047134c8845714e5e0655bfc6a0e68592bc1a1b Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 15 Dec 2021 19:30:38 +0100 Subject: [PATCH 187/366] Fix notify platform setup for KNX (#61842) * Fix notify platform setup for KNX * Apply review suggestions * Store hass config in DATA_HASS_CONFIG * Readd guard clause --- homeassistant/components/knx/__init__.py | 18 +++++++++---- homeassistant/components/knx/const.py | 3 +++ homeassistant/components/knx/notify.py | 33 ++++++++++++++---------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5a66824fbcb..61d49243430 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -44,6 +45,7 @@ from .const import ( CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_ROUTING, CONF_KNX_TUNNELING, + DATA_HASS_CONFIG, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, @@ -195,6 +197,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the KNX integration.""" + hass.data[DATA_HASS_CONFIG] = config conf: ConfigType | None = config.get(DOMAIN) if conf is None: @@ -251,15 +254,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.config_entries.async_setup_platforms( - entry, [platform for platform in SUPPORTED_PLATFORMS if platform in config] + entry, + [ + platform + for platform in SUPPORTED_PLATFORMS + if platform in config and platform is not Platform.NOTIFY + ], ) - # set up notify platform, no entry support for notify component yet, - # have to use discovery to load platform. - if NotifySchema.PLATFORM in conf: + # set up notify platform, no entry support for notify component yet + if NotifySchema.PLATFORM in config: hass.async_create_task( discovery.async_load_platform( - hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM], config + hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG] ) ) @@ -312,6 +319,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platform for platform in SUPPORTED_PLATFORMS if platform in hass.data[DATA_KNX_CONFIG] + and platform is not Platform.NOTIFY ], ) if unload_ok: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 950deff95c1..f50460de173 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -42,7 +42,10 @@ CONF_STATE_ADDRESS: Final = "state_address" CONF_SYNC_STATE: Final = "sync_state" CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING] +# yaml config merged with config entry data DATA_KNX_CONFIG: Final = "knx_config" +# original hass yaml config +DATA_HASS_CONFIG: Final = "knx_hass_config" ATTR_COUNTER: Final = "counter" ATTR_SOURCE: Final = "source" diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 61bee14e5e2..ee170f55802 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -11,7 +11,8 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN, KNX_ADDRESS +from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS +from .schema import NotifySchema async def async_get_service( @@ -20,24 +21,28 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> KNXNotificationService | None: """Get the KNX notification service.""" - if not discovery_info: + if discovery_info is None: return None - platform_config: dict = discovery_info - xknx: XKNX = hass.data[DOMAIN].xknx + if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM): + xknx: XKNX = hass.data[DOMAIN].xknx - notification_devices = [] - for device_config in platform_config: - notification_devices.append( - XknxNotification( - xknx, - name=device_config[CONF_NAME], - group_address=device_config[KNX_ADDRESS], + notification_devices = [] + for device_config in platform_config: + notification_devices.append( + XknxNotification( + xknx, + name=device_config[CONF_NAME], + group_address=device_config[KNX_ADDRESS], + ) ) + return ( + KNXNotificationService(notification_devices) + if notification_devices + else None ) - return ( - KNXNotificationService(notification_devices) if notification_devices else None - ) + + return None class KNXNotificationService(BaseNotificationService): From ec263840ba9a05f23d1a60b2b23c95af12e9798d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 16 Dec 2021 00:53:20 +0100 Subject: [PATCH 188/366] Bump aiohue to 3.0.6 (#61974) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index f32d8edc284..7003a3a8ccf 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.5"], + "requirements": ["aiohue==3.0.6"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index c0048a7cafc..23d5a4d9111 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.5 +aiohue==3.0.6 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 780a343463f..99627111554 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.5 +aiohue==3.0.6 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 95c0eeecfbe7ea10c9ffb78e65fee43096689348 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 16 Dec 2021 22:33:03 +0100 Subject: [PATCH 189/366] Brunt dependency bump to 1.0.2 (#62014) --- homeassistant/components/brunt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 1ddbbb62f56..7b9307e8ef2 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,7 +3,7 @@ "name": "Brunt Blind Engine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==1.0.1"], + "requirements": ["brunt==1.0.2"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 23d5a4d9111..8880ce371d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -440,7 +440,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==1.0.1 +brunt==1.0.2 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99627111554..74240b6e5b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ broadlink==0.18.0 brother==1.1.0 # homeassistant.components.brunt -brunt==1.0.1 +brunt==1.0.2 # homeassistant.components.bsblan bsblan==0.4.0 From e44d50e1b1762cf9200776500d1f012065de4b83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Dec 2021 05:37:48 -0600 Subject: [PATCH 190/366] Bump flux_led to 0.26.15 (#62017) --- homeassistant/components/flux_led/config_flow.py | 6 ++++++ homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index cefecf216db..674c0b42a15 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -94,6 +94,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): firmware_date=None, model_info=None, model_description=None, + remote_access_enabled=None, + remote_access_host=None, + remote_access_port=None, ) return await self._async_handle_discovery() @@ -261,6 +264,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): firmware_date=None, model_info=None, model_description=bulb.model_data.description, + remote_access_enabled=None, + remote_access_host=None, + remote_access_port=None, ) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 191cdef7c38..22a8aa405e4 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.7"], + "requirements": ["flux_led==0.26.15"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 8880ce371d3..8754825b8d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.7 +flux_led==0.26.15 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74240b6e5b1..dd320cb8222 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.7 +flux_led==0.26.15 # homeassistant.components.homekit fnvhash==0.1.0 From d92ad76ed988b73789f0b38a67fd5c2812d7c489 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 16:12:43 +0100 Subject: [PATCH 191/366] Fix none-check in template light (#62089) --- homeassistant/components/template/light.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 5d172489840..8c4ef1818a4 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -641,9 +641,13 @@ class LightTemplate(TemplateEntity, LightEntity): @callback def _update_color(self, render): """Update the hs_color from the template.""" + if render is None: + self._color = None + return + h_str = s_str = None if isinstance(render, str): - if render in (None, "None", ""): + if render in ("None", ""): self._color = None return h_str, s_str = map( From 78f40bd4bf6780533edca854c7c63c63518cba3c Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:39:49 +0000 Subject: [PATCH 192/366] Add missing timezone information (#62106) --- homeassistant/components/vallox/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 6eee46be737..220a9e9289b 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util from . import ValloxDataUpdateCoordinator from .const import ( @@ -107,7 +108,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): days_remaining_delta = timedelta(days=days_remaining) now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0) - return now + days_remaining_delta + return (now + days_remaining_delta).astimezone(dt_util.UTC) class ValloxCellStateSensor(ValloxSensor): From b4af32624d368a3ed3a1ae77eb9af1408d452e9c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 17 Dec 2021 09:16:02 +0100 Subject: [PATCH 193/366] Improve availability for Shelly Valve (#62129) --- homeassistant/components/shelly/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index af84e881447..7a877e60109 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -187,7 +187,7 @@ class BlockSleepingClimate( """Device availability.""" if self.device_block is not None: return not cast(bool, self.device_block.valveError) - return True + return self.wrapper.last_update_success @property def hvac_mode(self) -> str: From 82173f477c1ccd20e81522b653b80d5af7fd428b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Dec 2021 01:17:32 -0600 Subject: [PATCH 194/366] Fix Non-thread-safe operation in homekit light events (#62147) --- homeassistant/components/homekit/type_lights.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 90c55d52153..f925f0a15a4 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -120,10 +120,11 @@ class Light(HomeAccessory): if self._event_timer: self._event_timer() self._event_timer = async_call_later( - self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events + self.hass, CHANGE_COALESCE_TIME_WINDOW, self._async_send_events ) - def _send_events(self, *_): + @callback + def _async_send_events(self, *_): """Process all changes at once.""" _LOGGER.debug("Coalesced _set_chars: %s", self._pending_events) char_values = self._pending_events From 19a0644b50f3e7ca1525d991de2861258b692617 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Dec 2021 01:19:07 -0600 Subject: [PATCH 195/366] Fix Non-thread-safe operation in logbook (#62148) --- homeassistant/components/logbook/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 89e70f346ed..43cdbec2530 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -112,6 +112,7 @@ def log_entry(hass, name, message, domain=None, entity_id=None, context=None): hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context) +@callback @bind_hass def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None): """Add an entry to the logbook.""" From 9361c9ef60609c3e2dc1cacf589a515b7b6be3ba Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 17 Dec 2021 00:07:51 -0800 Subject: [PATCH 196/366] Bump google-nest-sdm to 0.4.9 (#62160) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 0c14e59babc..bca37ce41c6 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.8"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.9"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8754825b8d5..151881e5613 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.8 +google-nest-sdm==0.4.9 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd320cb8222..c59e534e52d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.8 +google-nest-sdm==0.4.9 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 614529d7c327609ee8121721a3995ba4825f7853 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 17 Dec 2021 10:50:10 +0100 Subject: [PATCH 197/366] Add guard in call to activate_scene in Hue (#62177) --- homeassistant/components/hue/services.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/services.py b/homeassistant/components/hue/services.py index 72e88f0d956..6e68bbffbb8 100644 --- a/homeassistant/components/hue/services.py +++ b/homeassistant/components/hue/services.py @@ -146,8 +146,10 @@ async def hue_activate_scene_v2( continue # found match! if transition: - transition = transition * 100 # in steps of 100ms - await api.scenes.recall(scene.id, dynamic=dynamic, duration=transition) + transition = transition * 1000 # transition is in ms + await bridge.async_request_call( + api.scenes.recall, scene.id, dynamic=dynamic, duration=transition + ) return True LOGGER.debug( "Unable to find scene %s for group %s on bridge %s", From c445e93d45906a9c4dc279ce31340516ec7c42cd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Dec 2021 11:41:54 +0100 Subject: [PATCH 198/366] Fix threading error in scripts with repeat or choose actions (#62168) --- homeassistant/helpers/script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d4d37e1b4ac..20a1dbb8aec 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1052,6 +1052,7 @@ class Script: if self._change_listener_job: self._hass.async_run_hass_job(self._change_listener_job) + @callback def _chain_change_listener(self, sub_script: Script) -> None: if sub_script.is_running: self.last_action = sub_script.last_action From 54d7380f4d10c65b75489319929fbca608d029ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Dec 2021 11:42:15 +0100 Subject: [PATCH 199/366] Fix threading error in zha (#62170) --- homeassistant/components/zha/core/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 1d9edb82980..780d7bc384b 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -234,6 +234,7 @@ class GroupProbe: unsub() self._unsubs.remove(unsub) + @callback def _reprobe_group(self, group_id: int) -> None: """Reprobe a group for entities after its members change.""" zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] From 5196a770cc4b257e28bec4749c3d104f0a1731f1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 17 Dec 2021 11:43:38 +0100 Subject: [PATCH 200/366] Bumped version to 2021.12.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6fd2760d618..81734046d2b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __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) From c507c72350e8a6a33d35e9141da929caffd1b149 Mon Sep 17 00:00:00 2001 From: RDFurman Date: Mon, 13 Dec 2021 21:38:43 -0700 Subject: [PATCH 201/366] Honeywell unique id fix (#59393) * Move error logging and remove reload * Change device assignment and improve logging * Use dictionary for devices * Check if new device exists in API response * Add test and make loop better * Make test assert on error in log --- .../components/honeywell/__init__.py | 35 ++++++++++++------- homeassistant/components/honeywell/climate.py | 2 +- tests/components/honeywell/test_init.py | 21 ++++++++++- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c61e4fc18eb..c7bff647dda 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -30,14 +30,13 @@ async def async_setup_entry(hass, config): loc_id = config.data.get(CONF_LOC_ID) dev_id = config.data.get(CONF_DEV_ID) - devices = [] + devices = {} for location in client.locations_by_id.values(): - for device in location.devices_by_id.values(): - if (not loc_id or location.locationid == loc_id) and ( - not dev_id or device.deviceid == dev_id - ): - devices.append(device) + if not loc_id or location.locationid == loc_id: + for device in location.devices_by_id.values(): + if not dev_id or device.deviceid == dev_id: + devices[device.deviceid] = device if len(devices) == 0: _LOGGER.debug("No devices found") @@ -107,23 +106,30 @@ class HoneywellData: if self._client is None: return False - devices = [ + refreshed_devices = [ device for location in self._client.locations_by_id.values() for device in location.devices_by_id.values() ] - if len(devices) == 0: - _LOGGER.error("Failed to find any devices") + if len(refreshed_devices) == 0: + _LOGGER.error("Failed to find any devices after retry") return False - self.devices = devices + for updated_device in refreshed_devices: + if updated_device.deviceid in self.devices: + self.devices[updated_device.deviceid] = updated_device + else: + _LOGGER.info( + "New device with ID %s detected, reload the honeywell integration if you want to access it in Home Assistant" + ) + await self._hass.config_entries.async_reload(self._config.entry_id) return True async def _refresh_devices(self): """Refresh each enabled device.""" - for device in self.devices: + for device in self.devices.values(): await self._hass.async_add_executor_job(device.refresh) await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME) @@ -143,11 +149,16 @@ class HoneywellData: ) as exp: retries -= 1 if retries == 0: + _LOGGER.error( + "Ran out of retry attempts (3 attempts allocated). Error: %s", + exp, + ) raise exp result = await self._retry() if not result: + _LOGGER.error("Retry result was empty. Error: %s", exp) raise exp - _LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp) + _LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index d2766515595..6c686e92b8e 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -122,7 +122,7 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non async_add_entities( [ HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp) - for device in data.devices + for device in data.devices.values() ] ) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 619d770c59e..49917aae151 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -1,6 +1,8 @@ """Test honeywell setup process.""" -from unittest.mock import patch +from unittest.mock import create_autospec, patch + +import somecomfort from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -29,3 +31,20 @@ async def test_setup_multiple_thermostats( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert hass.states.async_entity_ids_count() == 2 + + +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_setup_multiple_thermostats_with_same_deviceid( + hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client +) -> None: + """Test Honeywell TCC API returning duplicate device IDs.""" + mock_location2 = create_autospec(somecomfort.Location, instance=True) + mock_location2.locationid.return_value = "location2" + mock_location2.devices_by_id = {device.deviceid: device} + client.locations_by_id["location2"] = mock_location2 + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + assert hass.states.async_entity_ids_count() == 1 + assert "Platform honeywell does not generate unique IDs" not in caplog.text From 22867acaf82b28a73f7a756289666e76bc8e641e Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Fri, 17 Dec 2021 15:43:41 +0100 Subject: [PATCH 202/366] Add vicare strings (#61593) * Add vicare strings * Remove duplicates * Remove duplicates from english translation * Add missing strings --- homeassistant/components/vicare/strings.json | 26 +++++++++++++++++++ .../components/vicare/translations/en.json | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 homeassistant/components/vicare/strings.json create mode 100644 homeassistant/components/vicare/translations/en.json diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json new file mode 100644 index 00000000000..bf6b40fb6b2 --- /dev/null +++ b/homeassistant/components/vicare/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "user": { + "title": "{name}", + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "scan_interval": "Scan Interval (seconds)", + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", + "client_id": "[%key:common::config_flow::data::api_key%]", + "heating_type": "Heating type" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/en.json b/homeassistant/components/vicare/translations/en.json new file mode 100644 index 00000000000..d693cbe76cc --- /dev/null +++ b/homeassistant/components/vicare/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "unknown": "Unexpected error" + }, + "error": { + "invalid_auth": "Invalid authentication" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "name": "Name", + "scan_interval": "Scan Interval (seconds)", + "client_id": "API Key", + "heating_type": "Heating type", + "password": "Password", + "username": "Email" + }, + "title": "{name}", + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com" + } + } + } +} \ No newline at end of file From 735deff45ebb5b8425bce881b3e90ecf527d4d81 Mon Sep 17 00:00:00 2001 From: jkuettner <12213711+jkuettner@users.noreply.github.com> Date: Fri, 17 Dec 2021 16:54:19 +0100 Subject: [PATCH 203/366] Fix "vevent" KeyError in caldav component (#61718) --- homeassistant/components/caldav/calendar.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index d27555beb2c..8ed8ea24607 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -161,6 +161,9 @@ class WebDavCalendarData: ) event_list = [] for event in vevent_list: + if not hasattr(event.instance, "vevent"): + _LOGGER.warning("Skipped event with missing 'vevent' property") + continue vevent = event.instance.vevent if not self.is_matching(vevent, self.search): continue @@ -198,6 +201,9 @@ class WebDavCalendarData: # and they would not be properly parsed using their original start/end dates. new_events = [] for event in results: + if not hasattr(event.instance, "vevent"): + _LOGGER.warning("Skipped event with missing 'vevent' property") + continue vevent = event.instance.vevent for start_dt in vevent.getrruleset() or []: _start_of_today = start_of_today From a87ed13a048dcad950649cef55d04a27445be3c8 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 19 Dec 2021 11:37:14 +0100 Subject: [PATCH 204/366] Silently retry Fronius inverter endpoint 2 times (#61826) --- .../components/fronius/coordinator.py | 24 +++++++++--- tests/components/fronius/test_coordinator.py | 38 ++++++++++++++----- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index e89f828f47d..f5ea498e381 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from datetime import timedelta from typing import TYPE_CHECKING, Any, Dict, TypeVar -from pyfronius import FroniusError +from pyfronius import BadStatusError, FroniusError from homeassistant.components.sensor import SensorEntityDescription from homeassistant.core import callback @@ -43,6 +43,8 @@ class FroniusCoordinatorBase( error_interval: timedelta valid_descriptions: list[SensorEntityDescription] + MAX_FAILED_UPDATES = 3 + def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None: """Set up the FroniusCoordinatorBase class.""" self._failed_update_count = 0 @@ -62,7 +64,7 @@ class FroniusCoordinatorBase( data = await self._update_method() except FroniusError as err: self._failed_update_count += 1 - if self._failed_update_count == 3: + if self._failed_update_count == self.MAX_FAILED_UPDATES: self.update_interval = self.error_interval raise UpdateFailed(err) from err @@ -116,6 +118,8 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): error_interval = timedelta(minutes=10) valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS + SILENT_RETRIES = 3 + def __init__( self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any ) -> None: @@ -125,9 +129,19 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): async def _update_method(self) -> dict[SolarNetId, Any]: """Return data per solar net id from pyfronius.""" - data = await self.solar_net.fronius.current_inverter_data( - self.inverter_info.solar_net_id - ) + # almost 1% of `current_inverter_data` requests on Symo devices result in + # `BadStatusError Code: 8 - LNRequestTimeout` due to flaky internal + # communication between the logger and the inverter. + for silent_retry in range(self.SILENT_RETRIES): + try: + data = await self.solar_net.fronius.current_inverter_data( + self.inverter_info.solar_net_id + ) + except BadStatusError as err: + if silent_retry == (self.SILENT_RETRIES - 1): + raise err + continue + break # wrap a single devices data in a dict with solar_net_id key for # FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys return {self.inverter_info.solar_net_id: data} diff --git a/tests/components/fronius/test_coordinator.py b/tests/components/fronius/test_coordinator.py index b729c4d97ac..a2368975128 100644 --- a/tests/components/fronius/test_coordinator.py +++ b/tests/components/fronius/test_coordinator.py @@ -1,7 +1,7 @@ """Test the Fronius update coordinators.""" from unittest.mock import patch -from pyfronius import FroniusError +from pyfronius import BadStatusError, FroniusError from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, @@ -18,27 +18,32 @@ async def test_adaptive_update_interval(hass, aioclient_mock): with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data: mock_responses(aioclient_mock) await setup_fronius_integration(hass) - assert mock_inverter_data.call_count == 1 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 2 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() - mock_inverter_data.side_effect = FroniusError - # first 3 requests at default interval - 4th has different interval - for _ in range(4): + mock_inverter_data.side_effect = FroniusError() + # first 3 bad requests at default interval - 4th has different interval + for _ in range(3): async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 5 + assert mock_inverter_data.call_count == 3 + mock_inverter_data.reset_mock() + async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 6 + assert mock_inverter_data.call_count == 1 + mock_inverter_data.reset_mock() mock_inverter_data.side_effect = None # next successful request resets to default interval @@ -46,10 +51,23 @@ async def test_adaptive_update_interval(hass, aioclient_mock): hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 7 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 8 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() + + # BadStatusError on inverter endpoints have special handling + mock_inverter_data.side_effect = BadStatusError("mock_endpoint", 8) + # first 3 requests at default interval - 4th has different interval + for _ in range(3): + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval + ) + await hass.async_block_till_done() + # BadStatusError does 3 silent retries for inverter endpoint * 3 request intervals = 9 + assert mock_inverter_data.call_count == 9 From 4e96ff78b54153e46f286114d69f688a3042f6e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 00:26:19 -0600 Subject: [PATCH 205/366] Avoid setting nexia humidity to the same value since it causes the api to fail (#61843) --- homeassistant/components/nexia/climate.py | 24 +++++++++++++++++--- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 2c1fbf5a3f4..6212541f897 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -11,6 +11,7 @@ from nexia.const import ( SYSTEM_STATUS_IDLE, UNIT_FAHRENHEIT, ) +from nexia.util import find_humidity_setpoint import voluptuous as vol from homeassistant.components.climate import ClimateEntity @@ -58,6 +59,8 @@ from .coordinator import NexiaDataUpdateCoordinator from .entity import NexiaThermostatZoneEntity from .util import percent_conv +PARALLEL_UPDATES = 1 # keep data in sync with only one connection at a time + SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode" @@ -231,9 +234,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): def set_humidity(self, humidity): """Dehumidify target.""" if self._thermostat.has_dehumidify_support(): - self._thermostat.set_dehumidify_setpoint(humidity / 100.0) + self.set_dehumidify_setpoint(humidity) else: - self._thermostat.set_humidify_setpoint(humidity / 100.0) + self.set_humidify_setpoint(humidity) self._signal_thermostat_update() @property @@ -453,7 +456,22 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): def set_humidify_setpoint(self, humidity): """Set the humidify setpoint.""" - self._thermostat.set_humidify_setpoint(humidity / 100.0) + target_humidity = find_humidity_setpoint(humidity / 100.0) + if self._thermostat.get_humidify_setpoint() == target_humidity: + # Trying to set the humidify setpoint to the + # same value will cause the api to timeout + return + self._thermostat.set_humidify_setpoint(target_humidity) + self._signal_thermostat_update() + + def set_dehumidify_setpoint(self, humidity): + """Set the dehumidify setpoint.""" + target_humidity = find_humidity_setpoint(humidity / 100.0) + if self._thermostat.get_dehumidify_setpoint() == target_humidity: + # Trying to set the dehumidify setpoint to the + # same value will cause the api to timeout + return + self._thermostat.set_dehumidify_setpoint(target_humidity) self._signal_thermostat_update() def _signal_thermostat_update(self): diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 105cbdb62b7..624eee41db7 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==0.9.11"], + "requirements": ["nexia==0.9.12"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 151881e5613..7b5e4878e4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1065,7 +1065,7 @@ nettigo-air-monitor==1.2.1 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.11 +nexia==0.9.12 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c59e534e52d..9b79845b83b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -654,7 +654,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.1 # homeassistant.components.nexia -nexia==0.9.11 +nexia==0.9.12 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.3 From 895dcaf69077b50d79bfa187eb21d68eb2abdab1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 17 Dec 2021 11:33:41 +0000 Subject: [PATCH 206/366] Force Lyric token refresh on first authentication failure (#62100) --- homeassistant/components/lyric/__init__.py | 27 ++++++++++++++++++---- homeassistant/components/lyric/api.py | 12 ++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index a9d5cbdec7d..5dcb5fc557a 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from http import HTTPStatus import logging from aiohttp.client_exceptions import ClientResponseError @@ -30,7 +31,11 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation +from .api import ( + ConfigEntryLyricClient, + LyricLocalOAuth2Implementation, + OAuth2SessionLyric, +) from .config_flow import OAuth2FlowHandler from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN @@ -84,21 +89,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) session = aiohttp_client.async_get_clientsession(hass) - oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + oauth_session = OAuth2SessionLyric(hass, entry, implementation) client = ConfigEntryLyricClient(session, oauth_session) client_id = hass.data[DOMAIN][CONF_CLIENT_ID] lyric = Lyric(client, client_id) - async def async_update_data() -> Lyric: + async def async_update_data(force_refresh_token: bool = False) -> Lyric: """Fetch data from Lyric.""" - await oauth_session.async_ensure_token_valid() + try: + if not force_refresh_token: + await oauth_session.async_ensure_token_valid() + else: + await oauth_session.force_refresh_token() + except ClientResponseError as exception: + if exception.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): + raise ConfigEntryAuthFailed from exception + raise UpdateFailed(exception) from exception + try: async with async_timeout.timeout(60): await lyric.get_locations() return lyric except LyricAuthenticationException as exception: + # Attempt to refresh the token before failing. + # Honeywell appear to have issues keeping tokens saved. + _LOGGER.debug("Authentication failed. Attempting to refresh token") + if not force_refresh_token: + return await async_update_data(force_refresh_token=True) raise ConfigEntryAuthFailed from exception except (LyricException, ClientResponseError) as exception: raise UpdateFailed(exception) from exception diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py index 3b23f802ded..4a8aa44417f 100644 --- a/homeassistant/components/lyric/api.py +++ b/homeassistant/components/lyric/api.py @@ -8,6 +8,18 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession +class OAuth2SessionLyric(config_entry_oauth2_flow.OAuth2Session): + """OAuth2Session for Lyric.""" + + async def force_refresh_token(self) -> None: + """Force a token refresh.""" + new_token = await self.implementation.async_refresh_token(self.token) + + self.hass.config_entries.async_update_entry( + self.config_entry, data={**self.config_entry.data, "token": new_token} + ) + + class ConfigEntryLyricClient(LyricClient): """Provide Honeywell Lyric authentication tied to an OAuth2 based config entry.""" From 9a0f42f9a726fd958b29775f8819bd26a664d209 Mon Sep 17 00:00:00 2001 From: Gage Benne Date: Sun, 19 Dec 2021 07:05:51 -0500 Subject: [PATCH 207/366] Bump pydexcom to 0.2.2 (#62207) --- homeassistant/components/dexcom/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index 1bf15776cad..6133a67bcf1 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -3,7 +3,7 @@ "name": "Dexcom", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", - "requirements": ["pydexcom==0.2.1"], + "requirements": ["pydexcom==0.2.2"], "codeowners": ["@gagebenne"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7b5e4878e4d..d93f75e934b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1435,7 +1435,7 @@ pydeconz==85 pydelijn==0.6.1 # homeassistant.components.dexcom -pydexcom==0.2.1 +pydexcom==0.2.2 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b79845b83b..a7880c01cd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -868,7 +868,7 @@ pydaikin==2.6.0 pydeconz==85 # homeassistant.components.dexcom -pydexcom==0.2.1 +pydexcom==0.2.2 # homeassistant.components.zwave pydispatcher==2.0.5 From 57b7b28d60863f43b47b4ef0410759e1bb34a599 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 17 Dec 2021 08:31:23 -0700 Subject: [PATCH 208/366] Fix spurious RainMachine config entry reload (#62215) --- homeassistant/components/rainmachine/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 39884681967..989616f6367 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -85,7 +85,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): await self.async_set_unique_id(controller.mac) self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: ip_address} + updates={CONF_IP_ADDRESS: ip_address}, reload_on_update=False ) # A new rain machine: We will change out the unique id From f665c4e588b0e6e76e85f1ee3f2c2eb861e95bb1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 13:52:21 -0700 Subject: [PATCH 209/366] Fix bug in which SimpliSafe websocket won't reconnect on error (#62241) --- .../components/simplisafe/__init__.py | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index dd3ff717e28..2b87f7aa8b0 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -12,6 +12,7 @@ from simplipy.errors import ( EndpointUnavailableError, InvalidCredentialsError, SimplipyError, + WebsocketError, ) from simplipy.system import SystemNotification from simplipy.system.v3 import ( @@ -472,6 +473,7 @@ class SimpliSafe: self._api = api self._hass = hass self._system_notifications: dict[int, set[SystemNotification]] = {} + self._websocket_reconnect_task: asyncio.Task | None = None self.entry = entry self.initial_event_to_use: dict[int, dict[str, Any]] = {} self.systems: dict[int, SystemType] = {} @@ -516,11 +518,29 @@ class SimpliSafe: self._system_notifications[system.system_id] = latest_notifications - async def _async_websocket_on_connect(self) -> None: + async def _async_start_websocket_loop(self) -> None: """Define a callback for connecting to the websocket.""" if TYPE_CHECKING: assert self._api.websocket - await self._api.websocket.async_listen() + + should_reconnect = True + + try: + await self._api.websocket.async_reconnect() + await self._api.websocket.async_listen() + except asyncio.CancelledError: + LOGGER.debug("Request to cancel websocket loop received") + raise + except WebsocketError as err: + LOGGER.error("Failed to connect to websocket: %s", err) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Unknown exception while connecting to websocket: %s", err) + + if should_reconnect: + LOGGER.info("Disconnected from websocket; reconnecting") + self._websocket_reconnect_task = self._hass.async_create_task( + self._async_start_websocket_loop() + ) @callback def _async_websocket_on_event(self, event: WebsocketEvent) -> None: @@ -560,17 +580,25 @@ class SimpliSafe: assert self._api.refresh_token assert self._api.websocket - self._api.websocket.add_connect_callback(self._async_websocket_on_connect) self._api.websocket.add_event_callback(self._async_websocket_on_event) - asyncio.create_task(self._api.websocket.async_connect()) + self._websocket_reconnect_task = asyncio.create_task( + self._async_start_websocket_loop() + ) async def async_websocket_disconnect_listener(_: Event) -> None: """Define an event handler to disconnect from the websocket.""" if TYPE_CHECKING: assert self._api.websocket - if self._api.websocket.connected: - await self._api.websocket.async_disconnect() + if self._websocket_reconnect_task: + self._websocket_reconnect_task.cancel() + try: + await self._websocket_reconnect_task + except asyncio.CancelledError: + LOGGER.debug("Websocket reconnection task successfully canceled") + self._websocket_reconnect_task = None + + await self._api.websocket.async_disconnect() self.entry.async_on_unload( self._hass.bus.async_listen_once( @@ -620,10 +648,10 @@ class SimpliSafe: if TYPE_CHECKING: assert self._api.websocket - if self._api.websocket.connected: - # If a websocket connection is open, reconnect it to use the - # new access token: - asyncio.create_task(self._api.websocket.async_reconnect()) + # Open a new websocket connection with the fresh token: + self._websocket_reconnect_task = self._hass.async_create_task( + self._async_start_websocket_loop() + ) self.entry.async_on_unload( self._api.add_refresh_token_callback(async_handle_refresh_token) From bc2949ef310ea39247d9a1753a86cc3d222c226c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 18 Dec 2021 13:29:52 +0100 Subject: [PATCH 210/366] bump pynetgear to 0.8.0 (#62261) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index aa4c57ecdde..1f4a15f1de1 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.7.0"], + "requirements": ["pynetgear==0.8.0"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index d93f75e934b..22460cb0305 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1661,7 +1661,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.7.0 +pynetgear==0.8.0 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7880c01cd5..13865dc6c47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1019,7 +1019,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.7.0 +pynetgear==0.8.0 # homeassistant.components.nuki pynuki==1.4.1 From 2395c753fe82c86d2bdda72ae49bd1490bf75bf3 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 19 Dec 2021 07:28:26 +0100 Subject: [PATCH 211/366] Fix logging for Shelly climate platform (#62264) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 7a877e60109..06140fcba72 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -69,7 +69,6 @@ async def async_setup_climate_entities( ) -> None: """Set up online climate devices.""" - _LOGGER.info("Setup online climate device %s", wrapper.name) device_block: Block | None = None sensor_block: Block | None = None @@ -82,6 +81,7 @@ async def async_setup_climate_entities( sensor_block = block if sensor_block and device_block: + _LOGGER.debug("Setup online climate device %s", wrapper.name) async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) @@ -92,7 +92,6 @@ async def async_restore_climate_entities( wrapper: BlockDeviceWrapper, ) -> None: """Restore sleeping climate devices.""" - _LOGGER.info("Setup sleeping climate device %s", wrapper.name) ent_reg = await entity_registry.async_get_registry(hass) entries = entity_registry.async_entries_for_config_entry( @@ -104,6 +103,7 @@ async def async_restore_climate_entities( if entry.domain != CLIMATE_DOMAIN: continue + _LOGGER.debug("Setup sleeping climate device %s", wrapper.name) _LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)]) From cd65aaee6074246a016111acd570c68d6e5c262f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 18 Dec 2021 14:18:31 +0100 Subject: [PATCH 212/366] Upgrade tailscale to 0.1.6 (#62267) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index eaa51855d38..ac7cbe84459 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.5"], + "requirements": ["tailscale==0.1.6"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 22460cb0305..d033ec06d51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2272,7 +2272,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.5 +tailscale==0.1.6 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13865dc6c47..20aed6ae1e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.5 +tailscale==0.1.6 # homeassistant.components.tellduslive tellduslive==0.10.11 From 4efa3b634e09584147fe743f13a4455d4f70f376 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 18 Dec 2021 15:00:07 +0100 Subject: [PATCH 213/366] Fix fitbit no SSL URL handling (#62270) --- homeassistant/components/fitbit/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 2de121920af..0c638f0c455 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -112,10 +112,11 @@ def request_app_setup( Then come back here and hit the below button. """ except NoURLAvailableError: - error_msg = """Could not find a SSL enabled URL for your Home Assistant instance. - Fitbit requires that your Home Assistant instance is accessible via HTTPS. - """ - configurator.notify_errors(_CONFIGURING["fitbit"], error_msg) + _LOGGER.error( + "Could not find an SSL enabled URL for your Home Assistant instance. " + "Fitbit requires that your Home Assistant instance is accessible via HTTPS" + ) + return submit = "I have saved my Client ID and Client Secret into fitbit.conf." From 3fde6bfd732633c2ee573e4fd4376c4731d029e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Dec 2021 17:14:41 -0600 Subject: [PATCH 214/366] Fix Non-thread-safe operation in rflink binary_sensor (#62286) --- homeassistant/components/rflink/binary_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index 77a8a522f65..82457bd767e 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.event as evt @@ -81,6 +82,7 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity): if self._state and self._off_delay is not None: + @callback def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None From c1d0fe9eaedbe9b9cbe5c3dfce1e51dc2af3c478 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Dec 2021 16:17:54 -0600 Subject: [PATCH 215/366] Fix Non-thread-safe operation in zwave node_added (#62287) --- homeassistant/components/zwave/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index ef3ab223248..bfbd3925c55 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -490,7 +490,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 await platform.async_add_entities([entity]) if entity.unique_id: - hass.async_add_job(_add_node_to_component()) + hass.create_task(_add_node_to_component()) return @callback From dc4659b167c53a62d80142df5c66e67323bd86fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 00:27:34 -0600 Subject: [PATCH 216/366] Bump flux_led to 0.27.8 to fix discovery of older devices (#62292) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 22a8aa405e4..8d3d7416c00 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.15"], + "requirements": ["flux_led==0.27.8"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index d033ec06d51..95b7b7ef411 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.15 +flux_led==0.27.8 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20aed6ae1e7..100f35a3758 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.15 +flux_led==0.27.8 # homeassistant.components.homekit fnvhash==0.1.0 From 311ebd4a964819238528bedc9d853bb4f4e3ca4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Fri, 17 Dec 2021 22:57:02 +0000 Subject: [PATCH 217/366] Bump async-upnp-client to 0.23.0 (#62223) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dlna_dmr/test_media_player.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 9bc020f3693..fb942717817 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.12"], + "requirements": ["async-upnp-client==0.23.0"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index ea8b8ff73a4..c11138e0816 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.12"], + "requirements": ["async-upnp-client==0.23.0"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index da12c25c7d1..87c40346fed 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.12"], + "requirements": ["async-upnp-client==0.23.0"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 60098514125..1b4246f478f 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.8", "async-upnp-client==0.22.12"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.0"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e52542ccea0..a200f6b0a03 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.12 +async-upnp-client==0.23.0 async_timeout==4.0.0 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 95b7b7ef411..d93a567f1aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -336,7 +336,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.12 +async-upnp-client==0.23.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 100f35a3758..565a30b1f36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -236,7 +236,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.12 +async-upnp-client==0.23.0 # homeassistant.components.aurora auroranoaa==0.0.2 diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index fe2a916fdcc..b8c10b47b60 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -381,7 +381,7 @@ async def test_event_subscribe_rejected( Device state will instead be obtained via polling in async_update. """ - dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(501) + dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(status=501) mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_state = hass.states.get(mock_entity_id) From b327628b6ecc9bb804e49ff0dcce183e2868c562 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Sun, 19 Dec 2021 16:41:32 +1100 Subject: [PATCH 218/366] Update async-upnp-client library to 0.23.1 (#62298) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index fb942717817..ecc3cd4256d 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.23.0"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index c11138e0816..e95c0e13887 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.23.0"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 87c40346fed..2644a91b20f 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.23.0"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 1b4246f478f..c46878c0ef3 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.8", "async-upnp-client==0.23.0"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.1"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a200f6b0a03..fcad7a03bee 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.23.0 +async-upnp-client==0.23.1 async_timeout==4.0.0 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d93a567f1aa..b55024ed82e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -336,7 +336,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.0 +async-upnp-client==0.23.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 565a30b1f36..c61d621f064 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -236,7 +236,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.0 +async-upnp-client==0.23.1 # homeassistant.components.aurora auroranoaa==0.0.2 From 5f2a2280c566a878db003333ea2117e7e3e52697 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 18 Dec 2021 21:41:59 -0800 Subject: [PATCH 219/366] Bump ring to 0.7.2 (#62299) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 527fb143aff..3e745dc2d4b 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.7.1"], + "requirements": ["ring_doorbell==0.7.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index b55024ed82e..641a100d132 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2058,7 +2058,7 @@ rfk101py==0.0.1 rflink==0.0.58 # homeassistant.components.ring -ring_doorbell==0.7.1 +ring_doorbell==0.7.2 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c61d621f064..fae30d9a57b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1230,7 +1230,7 @@ restrictedpython==5.2 rflink==0.0.58 # homeassistant.components.ring -ring_doorbell==0.7.1 +ring_doorbell==0.7.2 # homeassistant.components.roku rokuecp==0.8.4 From 7db161868ec4989183efa101a3e5d43d7657eb94 Mon Sep 17 00:00:00 2001 From: Thijs Walcarius Date: Sun, 19 Dec 2021 20:14:56 +0100 Subject: [PATCH 220/366] Fix missing brightness for Velbus entities (#62314) * Fix #62169: missing brightness for Velbus-entities * Use default implementation of supported_features Co-authored-by: Thijs Walcarius --- homeassistant/components/velbus/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index bd903c76790..f1a90651716 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -49,7 +49,7 @@ class VelbusLight(VelbusEntity, LightEntity): """Representation of a Velbus light.""" _channel: VelbusDimmer - _attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + _attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @property def is_on(self) -> bool: @@ -96,7 +96,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity): _channel: VelbusButton _attr_entity_registry_enabled_default = False - _attr_supported_feature = SUPPORT_FLASH + _attr_supported_features = SUPPORT_FLASH def __init__(self, channel: VelbusChannel) -> None: """Initialize the button light (led).""" From 7c925778eb8cef57d01602da236d4a1c723d07d1 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 19 Dec 2021 20:22:41 +0100 Subject: [PATCH 221/366] Fix velbus climate current temp (#62329) --- homeassistant/components/velbus/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index c11698b1358..5dce4033074 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -61,6 +61,11 @@ class VelbusClimate(VelbusEntity, ClimateEntity): None, ) + @property + def current_temperature(self) -> int | None: + """Return the current temperature.""" + return self._channel.get_state() + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: From beb5a992e67e78538bf9f0a818a340dd3e122bb3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 15:05:13 -0700 Subject: [PATCH 222/366] Ensure existing SimpliSafe websocket tasks are cancelled appropriately (#62347) --- .../components/simplisafe/__init__.py | 33 +++++++++++-------- .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2b87f7aa8b0..59142674e66 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -519,14 +519,14 @@ class SimpliSafe: self._system_notifications[system.system_id] = latest_notifications async def _async_start_websocket_loop(self) -> None: - """Define a callback for connecting to the websocket.""" + """Start a websocket reconnection loop.""" if TYPE_CHECKING: assert self._api.websocket should_reconnect = True try: - await self._api.websocket.async_reconnect() + await self._api.websocket.async_connect() await self._api.websocket.async_listen() except asyncio.CancelledError: LOGGER.debug("Request to cancel websocket loop received") @@ -538,10 +538,25 @@ class SimpliSafe: if should_reconnect: LOGGER.info("Disconnected from websocket; reconnecting") + await self._async_cancel_websocket_loop() self._websocket_reconnect_task = self._hass.async_create_task( self._async_start_websocket_loop() ) + async def _async_cancel_websocket_loop(self) -> None: + """Stop any existing websocket reconnection loop.""" + if self._websocket_reconnect_task: + self._websocket_reconnect_task.cancel() + try: + await self._websocket_reconnect_task + except asyncio.CancelledError: + LOGGER.debug("Websocket reconnection task successfully canceled") + self._websocket_reconnect_task = None + + if TYPE_CHECKING: + assert self._api.websocket + await self._api.websocket.async_disconnect() + @callback def _async_websocket_on_event(self, event: WebsocketEvent) -> None: """Define a callback for receiving a websocket event.""" @@ -590,15 +605,7 @@ class SimpliSafe: if TYPE_CHECKING: assert self._api.websocket - if self._websocket_reconnect_task: - self._websocket_reconnect_task.cancel() - try: - await self._websocket_reconnect_task - except asyncio.CancelledError: - LOGGER.debug("Websocket reconnection task successfully canceled") - self._websocket_reconnect_task = None - - await self._api.websocket.async_disconnect() + await self._async_cancel_websocket_loop() self.entry.async_on_unload( self._hass.bus.async_listen_once( @@ -640,8 +647,7 @@ class SimpliSafe: data={**self.entry.data, CONF_TOKEN: token}, ) - @callback - def async_handle_refresh_token(token: str) -> None: + async def async_handle_refresh_token(token: str) -> None: """Handle a new refresh token.""" async_save_refresh_token(token) @@ -649,6 +655,7 @@ class SimpliSafe: assert self._api.websocket # Open a new websocket connection with the fresh token: + await self._async_cancel_websocket_loop() self._websocket_reconnect_task = self._hass.async_create_task( self._async_start_websocket_loop() ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 0b6cb385be6..8e494af013a 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2021.12.1"], + "requirements": ["simplisafe-python==2021.12.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 641a100d132..eb0d15b4eb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2146,7 +2146,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.12.1 +simplisafe-python==2021.12.2 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fae30d9a57b..4cf175b5e09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1273,7 +1273,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.12.1 +simplisafe-python==2021.12.2 # homeassistant.components.slack slackclient==2.5.0 From bfd85795666beb93a16e3af86bb8b5d9e1320167 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 19 Dec 2021 21:02:05 -0800 Subject: [PATCH 223/366] Bump pywemo==0.7.0 (#62360) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wemo/conftest.py | 2 +- tests/components/wemo/test_device_trigger.py | 10 +++++----- tests/components/wemo/test_wemo_device.py | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 59eae24c714..d0643ed51a9 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.6.7"], + "requirements": ["pywemo==0.7.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index eb0d15b4eb9..7d67fb479bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2007,7 +2007,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.7 +pywemo==0.7.0 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4cf175b5e09..78fb6dfa0c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1206,7 +1206,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.7 +pywemo==0.7.0 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 08abd140dac..13ec0cb2337 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -57,7 +57,7 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model): device.port = MOCK_PORT device.name = MOCK_NAME device.serialnumber = MOCK_SERIAL_NUMBER - device.model_name = pywemo_model + device.model_name = pywemo_model.replace("LongPress", "") device.get_state.return_value = 0 # Default to Off device.supports_long_press.return_value = cls.supports_long_press() diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py index 76016469b72..0ad7d95dd7a 100644 --- a/tests/components/wemo/test_device_trigger.py +++ b/tests/components/wemo/test_device_trigger.py @@ -3,7 +3,6 @@ import pytest from pywemo.subscribe import EVENT_TYPE_LONG_PRESS from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT from homeassistant.const import ( CONF_DEVICE_ID, @@ -11,6 +10,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE, + Platform, ) from homeassistant.setup import async_setup_component @@ -26,8 +26,8 @@ DATA_MESSAGE = {"message": "service-called"} @pytest.fixture def pywemo_model(): - """Pywemo Dimmer models use the light platform (WemoDimmer class).""" - return "Dimmer" + """Pywemo LightSwitch models use the switch platform.""" + return "LightSwitchLongPress" async def setup_automation(hass, device_id, trigger_type): @@ -67,14 +67,14 @@ async def test_get_triggers(hass, wemo_entity): }, { CONF_DEVICE_ID: wemo_entity.device_id, - CONF_DOMAIN: LIGHT_DOMAIN, + CONF_DOMAIN: Platform.SWITCH, CONF_ENTITY_ID: wemo_entity.entity_id, CONF_PLATFORM: "device", CONF_TYPE: "turned_off", }, { CONF_DEVICE_ID: wemo_entity.device_id, - CONF_DOMAIN: LIGHT_DOMAIN, + CONF_DOMAIN: Platform.SWITCH, CONF_ENTITY_ID: wemo_entity.entity_id, CONF_PLATFORM: "device", CONF_TYPE: "turned_on", diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py index e756e816a47..9ef9e6b5685 100644 --- a/tests/components/wemo/test_wemo_device.py +++ b/tests/components/wemo/test_wemo_device.py @@ -26,8 +26,8 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(True)) @pytest.fixture def pywemo_model(): - """Pywemo Dimmer models use the light platform (WemoDimmer class).""" - return "Dimmer" + """Pywemo LightSwitch models use the switch platform.""" + return "LightSwitchLongPress" async def test_async_register_device_longpress_fails(hass, pywemo_device): From eb4b041d459da4a1c0cb25beebcd48b104501501 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Dec 2021 22:28:15 -0800 Subject: [PATCH 224/366] Bump voluptuous_serialize to 2.5.0 (#62363) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fcad7a03bee..b943d4821aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ pyyaml==6.0 requests==2.26.0 scapy==2.4.5 sqlalchemy==1.4.27 -voluptuous-serialize==2.4.0 +voluptuous-serialize==2.5.0 voluptuous==0.12.2 yarl==1.6.3 zeroconf==0.37.0 diff --git a/requirements.txt b/requirements.txt index 5832d0ea2d0..4c6af849ce8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,5 @@ python-slugify==4.0.1 pyyaml==6.0 requests==2.26.0 voluptuous==0.12.2 -voluptuous-serialize==2.4.0 +voluptuous-serialize==2.5.0 yarl==1.6.3 diff --git a/setup.py b/setup.py index ee163bc79f4..270f5c58f58 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ REQUIRES = [ "pyyaml==6.0", "requests==2.26.0", "voluptuous==0.12.2", - "voluptuous-serialize==2.4.0", + "voluptuous-serialize==2.5.0", "yarl==1.6.3", ] From 4802e4e33f025cd1c2bce92506281f6874ac4a20 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Dec 2021 22:32:50 -0800 Subject: [PATCH 225/366] Bumped version to 2021.12.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 81734046d2b..bda65572d4e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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) From dcc08a0aac7917c133e85129e1bbab56fd896826 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Dec 2021 14:28:02 +0100 Subject: [PATCH 226/366] Don't use the homeassistant media app when casting media (#62385) --- homeassistant/components/cast/media_player.py | 2 +- tests/components/cast/test_media_player.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 61922a4cd8b..b687f96948b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -526,7 +526,7 @@ class CastDevice(MediaPlayerEntity): controller.play_media(media) else: app_data = {"media_id": media_id, "media_type": media_type, **extra} - quick_play(self._chromecast, "homeassistant_media", app_data) + quick_play(self._chromecast, "default_media_receiver", app_data) def _media_status(self): """ diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 85562f39761..dae9981ae67 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -791,7 +791,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): chromecast.media_controller.play_media.assert_not_called() quick_play_mock.assert_called_once_with( chromecast, - "homeassistant_media", + "default_media_receiver", { "media_id": "best.mp3", "media_type": "audio", @@ -907,7 +907,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock): # Play_media await common.async_play_media(hass, "audio", "/best.mp3", entity_id) quick_play_mock.assert_called_once_with( - chromecast, "homeassistant_media", {"media_id": ANY, "media_type": "audio"} + chromecast, "default_media_receiver", {"media_id": ANY, "media_type": "audio"} ) assert quick_play_mock.call_args[0][2]["media_id"].startswith( "http://example.com:8123/best.mp3?authSig=" @@ -1311,7 +1311,7 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock): assert not chromecast.media_controller.play_media.called quick_play_mock.assert_called_once_with( chromecast, - "homeassistant_media", + "default_media_receiver", {"media_id": "best.mp3", "media_type": "music"}, ) From 7ec369d8eface8a3f29907bb076cc4f928e94699 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Mon, 20 Dec 2021 18:31:59 +0100 Subject: [PATCH 227/366] Bump brunt to 1.1.0 (#62386) --- homeassistant/components/brunt/__init__.py | 2 +- homeassistant/components/brunt/cover.py | 21 +++++++++----------- homeassistant/components/brunt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 37c9fd73632..988a96ce08e 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with async_timeout.timeout(10): things = await bapi.async_get_things(force=True) - return {thing.SERIAL: thing for thing in things} + return {thing.serial: thing for thing in things} except ServerDisconnectedError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err except ClientResponseError as err: diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index cc0ecd0feab..230534a0848 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -100,7 +100,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): self._remove_update_listener = None - self._attr_name = self._thing.NAME + self._attr_name = self._thing.name self._attr_device_class = DEVICE_CLASS_SHADE self._attr_supported_features = COVER_FEATURES self._attr_attribution = ATTRIBUTION @@ -109,8 +109,8 @@ class BruntDevice(CoordinatorEntity, CoverEntity): name=self._attr_name, via_device=(DOMAIN, self._entry_id), manufacturer="Brunt", - sw_version=self._thing.FW_VERSION, - model=self._thing.MODEL, + sw_version=self._thing.fw_version, + model=self._thing.model, ) async def async_added_to_hass(self) -> None: @@ -127,8 +127,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): None is unknown, 0 is closed, 100 is fully open. """ - pos = self.coordinator.data[self.unique_id].currentPosition - return int(pos) if pos is not None else None + return self.coordinator.data[self.unique_id].current_position @property def request_cover_position(self) -> int | None: @@ -139,8 +138,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): to Brunt, at times there is a diff of 1 to current None is unknown, 0 is closed, 100 is fully open. """ - pos = self.coordinator.data[self.unique_id].requestPosition - return int(pos) if pos is not None else None + return self.coordinator.data[self.unique_id].request_position @property def move_state(self) -> int | None: @@ -149,8 +147,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): None is unknown, 0 when stopped, 1 when opening, 2 when closing """ - mov = self.coordinator.data[self.unique_id].moveState - return int(mov) if mov is not None else None + return self.coordinator.data[self.unique_id].move_state @property def is_opening(self) -> bool: @@ -190,11 +187,11 @@ class BruntDevice(CoordinatorEntity, CoverEntity): """Set the cover to the new position and wait for the update to be reflected.""" try: await self._bapi.async_change_request_position( - position, thingUri=self._thing.thingUri + position, thing_uri=self._thing.thing_uri ) except ClientResponseError as exc: raise HomeAssistantError( - f"Unable to reposition {self._thing.NAME}" + f"Unable to reposition {self._thing.name}" ) from exc self.coordinator.update_interval = FAST_INTERVAL await self.coordinator.async_request_refresh() @@ -204,7 +201,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): """Update the update interval after each refresh.""" if ( self.request_cover_position - == self._bapi.last_requested_positions[self._thing.thingUri] + == self._bapi.last_requested_positions[self._thing.thing_uri] and self.move_state == 0 ): self.coordinator.update_interval = REGULAR_INTERVAL diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 7b9307e8ef2..f970419b787 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,7 +3,7 @@ "name": "Brunt Blind Engine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==1.0.2"], + "requirements": ["brunt==1.1.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7d67fb479bf..67602f65d25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -440,7 +440,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==1.0.2 +brunt==1.1.0 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78fb6dfa0c8..b27b50b244b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ broadlink==0.18.0 brother==1.1.0 # homeassistant.components.brunt -brunt==1.0.2 +brunt==1.1.0 # homeassistant.components.bsblan bsblan==0.4.0 From e80f4e03a4c2b838bd09d29bb5469c1121343604 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 20 Dec 2021 18:49:16 +0100 Subject: [PATCH 228/366] Update frontend to 20211220.0 (#62389) --- 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 4ac95c38afc..6ae1709e418 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==20211215.0" + "home-assistant-frontend==20211220.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b943d4821aa..5ea2198a3aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 67602f65d25..0c7a139b094 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b27b50b244b..3baf1637652 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From b8f8b30b9bd640de278c9ab6ea839a960ed44515 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Dec 2021 18:37:11 +0100 Subject: [PATCH 229/366] Bump pychromecast to 10.2.2 (#62390) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index bee18948a33..b084540bebb 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.2.1"], + "requirements": ["pychromecast==10.2.2"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 0c7a139b094..2f675b87de1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1396,7 +1396,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.2.1 +pychromecast==10.2.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3baf1637652..2c6c1dffe62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.2.1 +pychromecast==10.2.2 # homeassistant.components.climacell pyclimacell==0.18.2 From 5a2bc8e4939c8014ed560cf25ef00889b491a21f Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 20 Dec 2021 19:22:04 +0100 Subject: [PATCH 230/366] Update xknx to 0.18.14 (#62411) Co-authored-by: Franck Nijhof --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/test_config_flow.py | 10 +++++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b793c667353..21ac4ce9ea4 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.18.13" + "xknx==0.18.14" ], "codeowners": [ "@Julius2342", diff --git a/requirements_all.txt b/requirements_all.txt index 2f675b87de1..cf2fef1e6bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2448,7 +2448,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.13 +xknx==0.18.14 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c6c1dffe62..3fabae380c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1450,7 +1450,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.13 +xknx==0.18.14 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 65289c2b173..4f3e1734b69 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -28,7 +28,15 @@ from tests.common import MockConfigEntry def _gateway_descriptor(ip: str, port: int) -> GatewayDescriptor: """Get mock gw descriptor.""" - return GatewayDescriptor("Test", ip, port, "eth0", "127.0.0.1", True) + return GatewayDescriptor( + "Test", + ip, + port, + "eth0", + "127.0.0.1", + supports_routing=True, + supports_tunnelling=True, + ) async def test_user_single_instance(hass): From d90c107b1bb38d25f144eed3c987a7b39530d12c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Dec 2021 18:09:36 +0100 Subject: [PATCH 231/366] Invalidate CI cache when bumping dependencies, part 2 (#62412) --- .github/workflows/ci.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ebbd2b2fb9e..73cec8b9635 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -517,10 +517,15 @@ jobs: key: >- ${{ runner.os }}-${{ matrix.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- + # Temporary disabling the restore of environments when bumping + # a dependency. It seems that we are experiencing issues with + # restoring environments in GitHub Actions, although unclear why. + # First attempt: https://github.com/home-assistant/core/pull/62383 + # + # restore-keys: | + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | From 520c3411ddc0fa0c531eaa96cc23a08bb955bb07 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Dec 2021 16:22:09 +0100 Subject: [PATCH 232/366] Invalidate CI cache when bumping dependencies (#62394) Co-authored-by: Martin Hjelmare --- .github/workflows/ci.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 73cec8b9635..71fe8af00ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -155,10 +155,15 @@ jobs: key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- + # Temporary disabling the restore of environments when bumping + # a dependency. It seems that we are experiencing issues with + # restoring environments in GitHub Actions, although unclear why. + # First attempt: https://github.com/home-assistant/core/pull/62383 + # + # restore-keys: | + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | From 836d8a6fca9c3d38f61d12d4530c5391a14fd0e7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Dec 2021 20:18:11 +0100 Subject: [PATCH 233/366] Make it possible to turn on audio only google cast devices (#62420) --- homeassistant/components/cast/media_player.py | 13 ++++++++----- tests/components/cast/test_media_player.py | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b687f96948b..8160c1f5bf0 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -395,11 +395,14 @@ class CastDevice(MediaPlayerEntity): return if self._chromecast.app_id is not None: - # Quit the previous app before starting splash screen + # Quit the previous app before starting splash screen or media player self._chromecast.quit_app() # The only way we can turn the Chromecast is on is by launching an app - self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: + self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + else: + self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) def turn_off(self): """Turn off the cast device.""" @@ -674,9 +677,9 @@ class CastDevice(MediaPlayerEntity): support = SUPPORT_CAST media_status = self._media_status()[0] - if ( - self._chromecast - and self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST + if self._chromecast and self._chromecast.cast_type in ( + pychromecast.const.CAST_TYPE_CHROMECAST, + pychromecast.const.CAST_TYPE_AUDIO, ): support |= SUPPORT_TURN_ON diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index dae9981ae67..3c5d4705713 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -683,10 +683,12 @@ async def test_entity_cast_status(hass: HomeAssistant): | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, ), From a1fc223914685cb8181baa18c7d7e774c51a0a4a Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 20 Dec 2021 23:25:26 +0100 Subject: [PATCH 234/366] Bump bimmer_connected to 0.8.7 (#62435) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index fc641548aff..63046d9d441 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.8.5"], + "requirements": ["bimmer_connected==0.8.7"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index cf2fef1e6bc..b0d8a41215e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ beautifulsoup4==4.10.0 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.5 +bimmer_connected==0.8.7 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fabae380c3..e391eeb234d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ base36==0.1.1 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.5 +bimmer_connected==0.8.7 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 1f0c13f259effd0cd97f7af5476d40f834016d38 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 20 Dec 2021 22:45:38 +0100 Subject: [PATCH 235/366] bump aiohue to 3.0.7 (#62444) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 7003a3a8ccf..ba2d97f44a5 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.6"], + "requirements": ["aiohue==3.0.7"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index b0d8a41215e..0fa32a93289 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.6 +aiohue==3.0.7 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e391eeb234d..e80eb4395a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.6 +aiohue==3.0.7 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 5e0ea9fd240afa10399c7de771df06af7a7ebfc5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 21 Dec 2021 01:09:14 +0100 Subject: [PATCH 236/366] Change Hue availability blacklist logic a bit (#62446) --- homeassistant/components/hue/v2/entity.py | 63 ++++++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index ae345238c23..7371efff3bb 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -47,20 +47,9 @@ class HueBaseEntity(Entity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.device.id)}, ) - # some (3th party) Hue lights report their connection status incorrectly - # causing the zigbee availability to report as disconnected while in fact - # it can be controlled. Although this is in fact something the device manufacturer - # should fix, we work around it here. If the light is reported unavailable at - # startup, we ignore the availability status of the zigbee connection - self._ignore_availability = False - if self.device is None: - return - if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): - self._ignore_availability = ( - # Official Hue lights are reliable - self.device.product_data.manufacturer_name != "Signify Netherlands B.V." - and zigbee.status != ConnectivityServiceStatus.CONNECTED - ) + # used for availability workaround + self._ignore_availability = None + self._last_state = None @property def name(self) -> str: @@ -82,6 +71,7 @@ class HueBaseEntity(Entity): async def async_added_to_hass(self) -> None: """Call when entity is added.""" + self._check_availability_workaround() # Add value_changed callbacks. self.async_on_remove( self.controller.subscribe( @@ -140,5 +130,50 @@ class HueBaseEntity(Entity): ent_reg.async_remove(self.entity_id) else: self.logger.debug("Received status update for %s", self.entity_id) + self._check_availability_workaround() self.on_update() self.async_write_ha_state() + + @callback + def _check_availability_workaround(self): + """Check availability of the device.""" + if self.resource.type != ResourceTypes.LIGHT: + return + if self._ignore_availability is not None: + # already processed + return + cur_state = self.resource.on.on + if self._last_state is None: + self._last_state = cur_state + return + # some (3th party) Hue lights report their connection status incorrectly + # causing the zigbee availability to report as disconnected while in fact + # it can be controlled. Although this is in fact something the device manufacturer + # should fix, we work around it here. If the light is reported unavailable + # by the zigbee connectivity but the state changesm its considered as a + # malfunctioning device and we report it. + # while the user should actually fix this issue instead of ignoring it, we + # ignore the availability for this light from this point. + if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): + if ( + self._last_state != cur_state + and zigbee.status != ConnectivityServiceStatus.CONNECTED + ): + # the device state changed from on->off or off->on + # while it was reported as not connected! + self.logger.warning( + "Light %s changed state while reported as disconnected. " + "This is an indicator that routing is not working properly for this device. " + "Home Assistant will ignore availability for this light from now on. " + "Device details: %s - %s (%s) fw: %s", + self.name, + self.device.product_data.manufacturer_name, + self.device.product_data.product_name, + self.device.product_data.model_id, + self.device.product_data.software_version, + ) + # do we want to store this in some persistent storage? + self._ignore_availability = True + else: + self._ignore_availability = False + self._last_state = cur_state From ef9419f001aec3c0f1f98402ac53223ca1d45478 Mon Sep 17 00:00:00 2001 From: Angelo Gagliano <25516409+TheGardenMonkey@users.noreply.github.com> Date: Tue, 21 Dec 2021 16:09:28 -0500 Subject: [PATCH 237/366] Require RPi.GPIO and bump adafruit-circuitpython-dht to 3.7.0 in dht (#61751) --- homeassistant/components/dht/manifest.json | 11 ++++++++--- requirements_all.txt | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json index 9067b930f0a..3eb3cfd202c 100644 --- a/homeassistant/components/dht/manifest.json +++ b/homeassistant/components/dht/manifest.json @@ -2,7 +2,12 @@ "domain": "dht", "name": "DHT Sensor", "documentation": "https://www.home-assistant.io/integrations/dht", - "requirements": ["adafruit-circuitpython-dht==3.6.0"], - "codeowners": ["@thegardenmonkey"], + "requirements": [ + "adafruit-circuitpython-dht==3.7.0", + "RPi.GPIO==0.7.1a4" + ], + "codeowners": [ + "@thegardenmonkey" + ], "iot_class": "local_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 0fa32a93289..58e91d6a8b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -61,6 +61,7 @@ PyViCare==2.13.1 PyXiaomiGateway==0.13.4 # homeassistant.components.bmp280 +# homeassistant.components.dht # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio # homeassistant.components.rpi_rf @@ -94,7 +95,7 @@ accuweather==0.3.0 adafruit-circuitpython-bmp280==3.1.1 # homeassistant.components.dht -adafruit-circuitpython-dht==3.6.0 +adafruit-circuitpython-dht==3.7.0 # homeassistant.components.mcp23017 adafruit-circuitpython-mcp230xx==2.2.2 From 050bddb9fe8de500c3a99eacf9ef4554233fdd97 Mon Sep 17 00:00:00 2001 From: schmyd Date: Wed, 22 Dec 2021 09:29:54 +0100 Subject: [PATCH 238/366] Fix deconz light service parameter handling (#62128) * Only check presence of values, not their content * Add tests * Revert "Only check presence of values, not their content" This reverts commit 046f0ed5fd631cbac0d26e4d3869ad2c6254c0f9. * Validate existence of keys, not their values * Properly handle cases of missing keys --- homeassistant/components/deconz/light.py | 14 +++++------ tests/components/deconz/test_light.py | 30 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index e287d574633..5330fdb3226 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -199,7 +199,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Turn on light.""" data: dict[str, bool | float | int | str | tuple[float, float]] = {"on": True} - if attr_brightness := kwargs.get(ATTR_BRIGHTNESS): + if (attr_brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: data["brightness"] = attr_brightness if attr_color_temp := kwargs.get(ATTR_COLOR_TEMP): @@ -215,16 +215,16 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if ATTR_XY_COLOR in kwargs: data["xy"] = kwargs[ATTR_XY_COLOR] - if attr_transition := kwargs.get(ATTR_TRANSITION): + if (attr_transition := kwargs.get(ATTR_TRANSITION)) is not None: data["transition_time"] = int(attr_transition * 10) elif "IKEA" in self._device.manufacturer: data["transition_time"] = 0 - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: data["alert"] = alert del data["on"] - if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT, ""))) is not None: + if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT))) is not None: data["effect"] = effect await self._device.set_state(**data) @@ -236,11 +236,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data: dict[str, bool | int | str] = {"on": False} - if ATTR_TRANSITION in kwargs: + if (attr_transition := kwargs.get(ATTR_TRANSITION)) is not None: data["brightness"] = 0 - data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10) + data["transition_time"] = int(attr_transition * 10) - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: data["alert"] = alert del data["on"] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index c2b12651fc0..2405ed159e6 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -399,6 +399,20 @@ async def test_light_state_change(hass, aioclient_mock, mock_deconz_websocket): "xy": (0.411, 0.351), }, ), + ( # Turn on light without transition time + { + "light_on": True, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_TRANSITION: 0, + }, + }, + { + "on": True, + "transitiontime": 0, + }, + ), ( # Turn on light with short color loop { "light_on": False, @@ -453,6 +467,22 @@ async def test_light_state_change(hass, aioclient_mock, mock_deconz_websocket): "alert": "select", }, ), + ( # Turn off light without transition time + { + "light_on": True, + "service": SERVICE_TURN_OFF, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_TRANSITION: 0, + ATTR_FLASH: FLASH_SHORT, + }, + }, + { + "bri": 0, + "transitiontime": 0, + "alert": "select", + }, + ), ( # Turn off light with long flashing { "light_on": True, From 4f0c20cf3391a036fc9a721257181d604ec97dd4 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Thu, 23 Dec 2021 07:48:31 +0100 Subject: [PATCH 239/366] Fix broken Vallox integration in 2021.12 (#62308) --- homeassistant/components/vallox/__init__.py | 23 +++++++++++++++++++-- homeassistant/components/vallox/const.py | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 73dc633834e..9beddaaad66 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -1,6 +1,7 @@ """Support for Vallox ventilation units.""" from __future__ import annotations +import asyncio from dataclasses import dataclass, field import ipaddress import logging @@ -13,7 +14,7 @@ from vallox_websocket_api.vallox import get_uuid as calculate_uuid import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import CoreState, HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.typing import ConfigType, StateType @@ -25,6 +26,7 @@ from .const import ( DEFAULT_FAN_SPEED_HOME, DEFAULT_NAME, DOMAIN, + INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS, METRIC_KEY_PROFILE_FAN_SPEED_AWAY, METRIC_KEY_PROFILE_FAN_SPEED_BOOST, METRIC_KEY_PROFILE_FAN_SPEED_HOME, @@ -171,7 +173,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = {"client": client, "coordinator": coordinator, "name": name} async def _async_load_platform_delayed(*_: Any) -> None: - await coordinator.async_refresh() + # We need a successful update before loading the platforms, because platform init code + # derives the UUIDs from the data the coordinator fetches. + warned_once = False + while hass.state == CoreState.running: + await coordinator.async_refresh() + if coordinator.last_update_success: + break + + if not warned_once: + _LOGGER.warning( + "Vallox integration not ready yet; Retrying in background" + ) + warned_once = True + + await asyncio.sleep(INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS) + else: + return + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config)) diff --git a/homeassistant/components/vallox/const.py b/homeassistant/components/vallox/const.py index aba10188bde..96767bd0e18 100644 --- a/homeassistant/components/vallox/const.py +++ b/homeassistant/components/vallox/const.py @@ -7,6 +7,7 @@ from vallox_websocket_api import PROFILE as VALLOX_PROFILE DOMAIN = "vallox" DEFAULT_NAME = "Vallox" +INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS = 5 STATE_SCAN_INTERVAL = timedelta(seconds=60) # Common metric keys and (default) values. From 0b4bfcc9415b1414b1eb848b0b44a978d115d78d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 21 Dec 2021 23:36:12 -0500 Subject: [PATCH 240/366] Fix Sonos updating when entities are disabled (#62456) Co-authored-by: J. Nick Koston --- .../components/sonos/binary_sensor.py | 4 +++ homeassistant/components/sonos/const.py | 1 - homeassistant/components/sonos/entity.py | 9 +----- .../components/sonos/media_player.py | 1 + homeassistant/components/sonos/number.py | 7 +++++ homeassistant/components/sonos/sensor.py | 6 ++++ homeassistant/components/sonos/speaker.py | 28 ++++--------------- 7 files changed, 24 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 615ad24e655..dc958625801 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -1,6 +1,7 @@ """Entity representing a Sonos power sensor.""" from __future__ import annotations +import logging from typing import Any from homeassistant.components.binary_sensor import ( @@ -16,11 +17,14 @@ from .speaker import SonosSpeaker ATTR_BATTERY_POWER_SOURCE = "power_source" +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" async def _async_create_entity(speaker: SonosSpeaker) -> None: + _LOGGER.debug("Creating battery binary_sensor on %s", speaker.zone_name) entity = SonosPowerEntity(speaker) async_add_entities([entity]) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 523ac9f561b..bffc6425928 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -149,7 +149,6 @@ SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_SWITCHES = "sonos_create_switches" SONOS_CREATE_LEVELS = "sonos_create_levels" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" -SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_POLL_UPDATE = "sonos_poll_update" SONOS_ALARMS_UPDATED = "sonos_alarms_updated" SONOS_FAVORITES_UPDATED = "sonos_favorites_updated" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index d8196ffdfa6..65f73eaf3f0 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -10,15 +10,11 @@ from soco.core import SoCo from soco.exceptions import SoCoException import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOMAIN, - SONOS_ENTITY_CREATED, SONOS_FAVORITES_UPDATED, SONOS_POLL_UPDATE, SONOS_STATE_UPDATED, @@ -60,9 +56,6 @@ class SonosEntity(Entity): self.async_write_ha_state, ) ) - async_dispatcher_send( - self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain - ) async def async_poll(self, now: datetime.datetime) -> None: """Poll the entity if subscriptions fail.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 90c33d7a4e6..f4dbf8f87d0 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -132,6 +132,7 @@ async def async_setup_entry( @callback def async_create_entities(speaker: SonosSpeaker) -> None: """Handle device discovery and create entities.""" + _LOGGER.debug("Creating media_player on %s", speaker.zone_name) async_add_entities([SonosMediaPlayerEntity(speaker)]) @service.verify_domain_control(hass, SONOS_DOMAIN) diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 2bcfe5cd5ec..3687c80474e 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -1,6 +1,8 @@ """Entity representing a Sonos number control.""" from __future__ import annotations +import logging + from homeassistant.components.number import NumberEntity from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback @@ -13,6 +15,8 @@ from .speaker import SonosSpeaker LEVEL_TYPES = ("bass", "treble") +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Sonos number platform from a config entry.""" @@ -21,6 +25,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def _async_create_entities(speaker: SonosSpeaker) -> None: entities = [] for level_type in LEVEL_TYPES: + _LOGGER.debug( + "Creating %s number control on %s", level_type, speaker.zone_name + ) entities.append(SonosLevelEntity(speaker, level_type)) async_add_entities(entities) diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 62017f4d541..c630b30351a 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,6 +1,8 @@ """Entity representing a Sonos battery level.""" from __future__ import annotations +import logging + from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -14,6 +16,8 @@ from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY from .entity import SonosEntity from .speaker import SonosSpeaker +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" @@ -22,11 +26,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def _async_create_audio_format_entity( speaker: SonosSpeaker, audio_format: str ) -> None: + _LOGGER.debug("Creating audio input format sensor on %s", speaker.zone_name) entity = SonosAudioInputFormatSensorEntity(speaker, audio_format) async_add_entities([entity]) @callback def _async_create_battery_sensor(speaker: SonosSpeaker) -> None: + _LOGGER.debug("Creating battery level sensor on %s", speaker.zone_name) entity = SonosBatteryEntity(speaker) async_add_entities([entity]) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 30b240c9fd7..8cd2abcf2b2 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -20,10 +20,7 @@ from soco.music_library import MusicLibrary from soco.plugins.sharelink import ShareLinkPlugin from soco.snapshot import Snapshot -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -42,7 +39,6 @@ from .const import ( BATTERY_SCAN_INTERVAL, DATA_SONOS, DOMAIN, - PLATFORMS, SCAN_INTERVAL, SONOS_CHECK_ACTIVITY, SONOS_CREATE_ALARM, @@ -51,7 +47,6 @@ from .const import ( SONOS_CREATE_LEVELS, SONOS_CREATE_MEDIA_PLAYER, SONOS_CREATE_SWITCHES, - SONOS_ENTITY_CREATED, SONOS_POLL_UPDATE, SONOS_REBOOTED, SONOS_SPEAKER_ACTIVITY, @@ -161,9 +156,6 @@ class SonosSpeaker: self._share_link_plugin: ShareLinkPlugin | None = None self.available = True - # Synchronization helpers - self._platforms_ready: set[str] = set() - # Subscriptions and events self.subscriptions_failed: bool = False self._subscriptions: list[SubscriptionBase] = [] @@ -217,7 +209,6 @@ class SonosSpeaker: dispatch_pairs = ( (SONOS_CHECK_ACTIVITY, self.async_check_activity), (SONOS_SPEAKER_ADDED, self.update_group_for_uid), - (f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.async_handle_new_entity), (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted), (f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", self.speaker_activity), ) @@ -253,15 +244,11 @@ class SonosSpeaker: self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL ) dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) - else: - self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) if new_alarms := [ alarm.alarm_id for alarm in self.alarms if alarm.zone.uid == self.soco.uid ]: dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - else: - self._platforms_ready.add(SWITCH_DOMAIN) dispatcher_send(self.hass, SONOS_CREATE_SWITCHES, self) @@ -277,19 +264,11 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) dispatcher_send(self.hass, SONOS_SPEAKER_ADDED, self.soco.uid) + self.hass.create_task(self.async_subscribe()) + # # Entity management # - async def async_handle_new_entity(self, entity_type: str) -> None: - """Listen to new entities to trigger first subscription.""" - if self._platforms_ready == PLATFORMS: - return - - self._platforms_ready.add(entity_type) - if self._platforms_ready == PLATFORMS: - self._resubscription_lock = asyncio.Lock() - await self.async_subscribe() - def write_entity_states(self) -> None: """Write states for associated SonosEntity instances.""" dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") @@ -405,6 +384,9 @@ class SonosSpeaker: async def async_resubscribe(self, exception: Exception) -> None: """Attempt to resubscribe when a renewal failure is detected.""" + if not self._resubscription_lock: + self._resubscription_lock = asyncio.Lock() + async with self._resubscription_lock: if not self.available: return From b19dc8bc372561349475cd7ec24326364e08dcdd Mon Sep 17 00:00:00 2001 From: ShadowBr0ther <38364191+ShadowBr0ther@users.noreply.github.com> Date: Wed, 22 Dec 2021 12:26:23 +0100 Subject: [PATCH 241/366] Fix repetier crash when printer is offline (#62490) --- CODEOWNERS | 2 +- homeassistant/components/repetier/__init__.py | 2 +- homeassistant/components/repetier/manifest.json | 4 ++-- requirements_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index f7333867069..e1dcf3d7dc9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -432,7 +432,7 @@ homeassistant/components/recollect_waste/* @bachya homeassistant/components/recorder/* @home-assistant/core homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/renault/* @epenet -homeassistant/components/repetier/* @MTrab +homeassistant/components/repetier/* @MTrab @ShadowBr0ther homeassistant/components/rflink/* @javicalle homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 homeassistant/components/ridwell/* @bachya diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 4c5534d1a28..66412c822b4 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from datetime import timedelta import logging -import pyrepetier +import pyrepetierng as pyrepetier import voluptuous as vol from homeassistant.components.sensor import SensorEntityDescription diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index 0fd3d904987..463c42c3a64 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -2,7 +2,7 @@ "domain": "repetier", "name": "Repetier-Server", "documentation": "https://www.home-assistant.io/integrations/repetier", - "requirements": ["pyrepetier==3.0.5"], - "codeowners": ["@MTrab"], + "requirements": ["pyrepetierng==0.1.0"], + "codeowners": ["@MTrab", "@ShadowBr0ther"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 58e91d6a8b6..c090a50980a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1754,7 +1754,7 @@ pyrainbird==0.4.3 pyrecswitch==1.0.2 # homeassistant.components.repetier -pyrepetier==3.0.5 +pyrepetierng==0.1.0 # homeassistant.components.risco pyrisco==0.3.1 From 2a515953ea4b648f032c5ee4cd96752e00bfa0ef Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 21 Dec 2021 15:14:44 -0500 Subject: [PATCH 242/366] Bump soco to 0.25.1 (#62523) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 1e31d2004b0..00006ab4e90 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.25.0"], + "requirements": ["soco==0.25.1"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index c090a50980a..d2851548bf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2185,7 +2185,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.25.0 +soco==0.25.1 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e80eb4395a9..9b74c2cdfe1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1291,7 +1291,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.25.0 +soco==0.25.1 # homeassistant.components.solaredge solaredge==0.0.2 From 79dae30b1964484596f1d9b292615143c6b64efa Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 22 Dec 2021 05:36:27 +0100 Subject: [PATCH 243/366] Update xknx to version 0.18.15 (#62557) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 21ac4ce9ea4..a0250a0cc92 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.18.14" + "xknx==0.18.15" ], "codeowners": [ "@Julius2342", diff --git a/requirements_all.txt b/requirements_all.txt index d2851548bf0..51f81b26f7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2449,7 +2449,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.14 +xknx==0.18.15 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b74c2cdfe1..a51c7929918 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1450,7 +1450,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.14 +xknx==0.18.15 # homeassistant.components.bluesound # homeassistant.components.fritz From d10716ff555b15924dab42b30668bb3a9655b821 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 21 Dec 2021 21:36:37 -0700 Subject: [PATCH 244/366] Bump pytile to 2021.12.0 (#62559) --- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 4e9913615a9..8702f3b62bf 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==5.2.4"], + "requirements": ["pytile==2021.12.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 51f81b26f7e..8347a65f1db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1962,7 +1962,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==5.2.4 +pytile==2021.12.0 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a51c7929918..54b312f6475 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1172,7 +1172,7 @@ python-twitch-client==0.6.0 python_awair==0.2.1 # homeassistant.components.tile -pytile==5.2.4 +pytile==2021.12.0 # homeassistant.components.traccar pytraccar==0.10.0 From 9b3d44c255eda1d9de698615aff17e5b1a3d7001 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Dec 2021 06:35:48 -0700 Subject: [PATCH 245/366] Bump flux_led to 0.27.12 to fix legacy cct controllers (#62573) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 8d3d7416c00..0f32d248384 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.8"], + "requirements": ["flux_led==0.27.12"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 8347a65f1db..99f86af5307 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.8 +flux_led==0.27.12 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54b312f6475..dbc05060732 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.8 +flux_led==0.27.12 # homeassistant.components.homekit fnvhash==0.1.0 From d10c5f459fc73c3c0df0cd87d71116545abd6ea4 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 22 Dec 2021 12:59:54 +0100 Subject: [PATCH 246/366] Fix missing object assignment for Fritz (#62575) --- homeassistant/components/fritz/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 3a0cc2b1301..3b6089d3272 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -117,7 +117,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._port = ssdp_location.port self._name = ( discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) - or self.fritz_tools.model + or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] ) self.context[CONF_HOST] = self._host From 9d235618ffeab6d6ff0f4d22d3dddcbf96e515da Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 22 Dec 2021 21:28:18 +0100 Subject: [PATCH 247/366] Fix timezone trafikverket_train (#62582) * Bugfix trafikverket train * Change from pytz to hass function * Fix datetime in extra attributes * Fix time timezone * Reset changes extra attributes --- .../components/trafikverket_train/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index d67fef5a0df..d80b58f5582 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.util.dt import get_time_zone _LOGGER = logging.getLogger(__name__) @@ -131,12 +132,15 @@ class TrainSensor(SensorEntity): self._state = None self._departure_state = None self._delay_in_minutes = None + self._timezone = get_time_zone("Europe/Stockholm") async def async_update(self): """Retrieve latest state.""" if self._time is not None: departure_day = next_departuredate(self._weekday) - when = datetime.combine(departure_day, self._time) + when = datetime.combine(departure_day, self._time).astimezone( + self._timezone + ) try: self._state = await self._train_api.async_get_train_stop( self._from_station, self._to_station, when @@ -193,8 +197,8 @@ class TrainSensor(SensorEntity): """Return the departure state.""" if (state := self._state) is not None: if state.time_at_location is not None: - return state.time_at_location + return state.time_at_location.astimezone(self._timezone) if state.estimated_time_at_location is not None: - return state.estimated_time_at_location - return state.advertised_time_at_location + return state.estimated_time_at_location.astimezone(self._timezone) + return state.advertised_time_at_location.astimezone(self._timezone) return None From 68f3f8db1e934d733e5aeae317a328d4a03a08bd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 22 Dec 2021 14:54:36 +0100 Subject: [PATCH 248/366] Improve google cast state reporting (#62587) --- homeassistant/components/cast/media_player.py | 7 +++- tests/components/cast/test_media_player.py | 34 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 8160c1f5bf0..f3cc9c32661 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -76,6 +76,8 @@ from .helpers import CastStatusListener, ChromecastInfo, ChromeCastZeroconf _LOGGER = logging.getLogger(__name__) +APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",) + CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" SUPPORT_CAST = SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF @@ -564,7 +566,10 @@ class CastDevice(MediaPlayerEntity): if media_status.player_is_idle: return STATE_IDLE if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID: - return STATE_PLAYING + if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO: + # Some apps don't report media status, show the player as playing + return STATE_PLAYING + return STATE_IDLE if self._chromecast is not None and self._chromecast.is_idle: return STATE_OFF return None diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 3c5d4705713..9bf1175c1b9 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1082,7 +1082,12 @@ async def test_entity_control(hass: HomeAssistant): chromecast.media_controller.seek.assert_called_once_with(123) -async def test_entity_media_states(hass: HomeAssistant): +# Some smart TV's with Google TV report "Netflix", not the Netflix app's ID +@pytest.mark.parametrize( + "app_id, state_no_media", + [(pychromecast.APP_YOUTUBE, "idle"), ("Netflix", "playing")], +) +async def test_entity_media_states(hass: HomeAssistant, app_id, state_no_media): """Test various entity media states.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -1090,7 +1095,7 @@ async def test_entity_media_states(hass: HomeAssistant): info = get_fake_chromecast_info() chromecast, _ = await async_setup_media_player_cast(hass, info) - _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) + cast_status_cb, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() connection_status.status = "CONNECTED" @@ -1103,6 +1108,15 @@ async def test_entity_media_states(hass: HomeAssistant): assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) + # App id updated, but no media status + chromecast.app_id = app_id + cast_status = MagicMock() + cast_status_cb(cast_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == state_no_media + + # Got media status media_status = MagicMock(images=None) media_status.player_is_playing = True media_status_cb(media_status) @@ -1124,15 +1138,23 @@ async def test_entity_media_states(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "idle" - media_status.player_is_idle = False - chromecast.is_idle = True - media_status_cb(media_status) + # No media status, app is still running + media_status_cb(None) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == state_no_media + + # App no longer running + chromecast.app_id = pychromecast.IDLE_APP_ID + cast_status = MagicMock() + cast_status_cb(cast_status) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "off" + # No cast status chromecast.is_idle = False - media_status_cb(media_status) + cast_status_cb(None) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "unknown" From a66d63e439b950e6d0eade2dad7d00f1d4f1f503 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Thu, 23 Dec 2021 17:08:40 +0100 Subject: [PATCH 249/366] Fix pvpc_hourly_pricing by changing data source and modernise integration (#62591) --- .../pvpc_hourly_pricing/__init__.py | 48 +- .../pvpc_hourly_pricing/manifest.json | 2 +- .../components/pvpc_hourly_pricing/sensor.py | 260 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../pvpc_hourly_pricing/conftest.py | 43 +- .../fixtures/PVPC_CURV_DD_2019_10_26.json | 820 ----------------- .../fixtures/PVPC_CURV_DD_2019_10_27.json | 854 ------------------ .../fixtures/PVPC_CURV_DD_2019_10_29.json | 820 ----------------- .../fixtures/PVPC_CURV_DD_2021_06_01.json | 604 ------------- .../fixtures/PVPC_DATA_2021_06_01.json | 154 ++++ .../pvpc_hourly_pricing/test_config_flow.py | 7 +- .../pvpc_hourly_pricing/test_sensor.py | 107 +-- 13 files changed, 388 insertions(+), 3335 deletions(-) delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json create mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index e628dfb9813..05e7c5940b7 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -1,12 +1,15 @@ """The pvpc_hourly_pricing integration to collect Spain official electric prices.""" +from datetime import datetime, timedelta import logging +from typing import Mapping -from aiopvpc import DEFAULT_POWER_KW, TARIFFS +from aiopvpc import DEFAULT_POWER_KW, TARIFFS, PVPCData import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import ( EntityRegistry, @@ -14,6 +17,8 @@ from homeassistant.helpers.entity_registry import ( async_migrate_entries, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import ( ATTR_POWER, @@ -99,6 +104,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_remove(entry.entry_id) return False + coordinator = ElecPricesDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True @@ -119,4 +128,39 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[Mapping[datetime, float]]): + """Class to manage fetching Electricity prices data from API.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self.api = PVPCData( + session=async_get_clientsession(hass), + tariff=entry.data[ATTR_TARIFF], + local_timezone=hass.config.time_zone, + power=entry.data[ATTR_POWER], + power_valley=entry.data[ATTR_POWER_P3], + ) + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) + ) + self._entry = entry + + @property + def entry_id(self) -> str: + """Return entry ID.""" + return self._entry.entry_id + + async def _async_update_data(self) -> Mapping[datetime, float]: + """Update electricity prices from the ESIOS API.""" + prices = await self.api.async_update_prices(dt_util.utcnow()) + self.api.process_state_and_attributes(dt_util.utcnow()) + if not prices: + raise UpdateFailed + + return prices diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 86696784638..5c9c06776b8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.2.4"], + "requirements": ["aiopvpc==3.0.0"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 1000d23b5bf..c1925fa12ff 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -1,73 +1,160 @@ """Sensor to collect the reference daily prices of electricity ('PVPC') in Spain.""" from __future__ import annotations +from collections.abc import Mapping +from datetime import datetime import logging -from random import randint from typing import Any -from aiopvpc import PVPCData - -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later, async_track_time_change -from homeassistant.helpers.restore_state import RestoreEntity -import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import async_track_time_change +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF +from . import ElecPricesDataUpdateCoordinator +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) - -ATTR_PRICE = "price" -ICON = "mdi:currency-eur" -UNIT = f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" - -_DEFAULT_TIMEOUT = 10 +PARALLEL_UPDATES = 1 +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="PVPC", + icon="mdi:currency-eur", + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + state_class=SensorStateClass.MEASUREMENT, + ), +) +_PRICE_SENSOR_ATTRIBUTES_MAP = { + "tariff": "tariff", + "period": "period", + "available_power": "available_power", + "next_period": "next_period", + "hours_to_next_period": "hours_to_next_period", + "next_better_price": "next_better_price", + "hours_to_better_price": "hours_to_better_price", + "num_better_prices_ahead": "num_better_prices_ahead", + "price_position": "price_position", + "price_ratio": "price_ratio", + "max_price": "max_price", + "max_price_at": "max_price_at", + "min_price": "min_price", + "min_price_at": "min_price_at", + "next_best_at": "next_best_at", + "price_00h": "price_00h", + "price_01h": "price_01h", + "price_02h": "price_02h", + "price_02h_d": "price_02h_d", # only on DST day change with 25h + "price_03h": "price_03h", + "price_04h": "price_04h", + "price_05h": "price_05h", + "price_06h": "price_06h", + "price_07h": "price_07h", + "price_08h": "price_08h", + "price_09h": "price_09h", + "price_10h": "price_10h", + "price_11h": "price_11h", + "price_12h": "price_12h", + "price_13h": "price_13h", + "price_14h": "price_14h", + "price_15h": "price_15h", + "price_16h": "price_16h", + "price_17h": "price_17h", + "price_18h": "price_18h", + "price_19h": "price_19h", + "price_20h": "price_20h", + "price_21h": "price_21h", + "price_22h": "price_22h", + "price_23h": "price_23h", + # only seen in the evening + "next_better_price (next day)": "next_better_price (next day)", + "hours_to_better_price (next day)": "hours_to_better_price (next day)", + "num_better_prices_ahead (next day)": "num_better_prices_ahead (next day)", + "price_position (next day)": "price_position (next day)", + "price_ratio (next day)": "price_ratio (next day)", + "max_price (next day)": "max_price (next day)", + "max_price_at (next day)": "max_price_at (next day)", + "min_price (next day)": "min_price (next day)", + "min_price_at (next day)": "min_price_at (next day)", + "next_best_at (next day)": "next_best_at (next day)", + "price_next_day_00h": "price_next_day_00h", + "price_next_day_01h": "price_next_day_01h", + "price_next_day_02h": "price_next_day_02h", + "price_next_day_02h_d": "price_next_day_02h_d", + "price_next_day_03h": "price_next_day_03h", + "price_next_day_04h": "price_next_day_04h", + "price_next_day_05h": "price_next_day_05h", + "price_next_day_06h": "price_next_day_06h", + "price_next_day_07h": "price_next_day_07h", + "price_next_day_08h": "price_next_day_08h", + "price_next_day_09h": "price_next_day_09h", + "price_next_day_10h": "price_next_day_10h", + "price_next_day_11h": "price_next_day_11h", + "price_next_day_12h": "price_next_day_12h", + "price_next_day_13h": "price_next_day_13h", + "price_next_day_14h": "price_next_day_14h", + "price_next_day_15h": "price_next_day_15h", + "price_next_day_16h": "price_next_day_16h", + "price_next_day_17h": "price_next_day_17h", + "price_next_day_18h": "price_next_day_18h", + "price_next_day_19h": "price_next_day_19h", + "price_next_day_20h": "price_next_day_20h", + "price_next_day_21h": "price_next_day_21h", + "price_next_day_22h": "price_next_day_22h", + "price_next_day_23h": "price_next_day_23h", +} async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the electricity price sensor from config_entry.""" - name = config_entry.data[CONF_NAME] - pvpc_data_handler = PVPCData( - tariff=config_entry.data[ATTR_TARIFF], - power=config_entry.data[ATTR_POWER], - power_valley=config_entry.data[ATTR_POWER_P3], - local_timezone=hass.config.time_zone, - websession=async_get_clientsession(hass), - timeout=_DEFAULT_TIMEOUT, - ) + coordinator: ElecPricesDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + name = entry.data[CONF_NAME] async_add_entities( - [ElecPriceSensor(name, config_entry.unique_id, pvpc_data_handler)], False + [ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id, name)] ) -class ElecPriceSensor(RestoreEntity, SensorEntity): +class ElecPriceSensor(CoordinatorEntity, SensorEntity): """Class to hold the prices of electricity as a sensor.""" - _attr_icon = ICON - _attr_native_unit_of_measurement = UNIT - _attr_should_poll = False - _attr_state_class = STATE_CLASS_MEASUREMENT + coordinator: ElecPricesDataUpdateCoordinator - def __init__(self, name, unique_id, pvpc_data_handler): - """Initialize the sensor object.""" - self._name = name - self._unique_id = unique_id - self._pvpc_data = pvpc_data_handler - self._num_retries = 0 + def __init__( + self, + coordinator: ElecPricesDataUpdateCoordinator, + description: SensorEntityDescription, + unique_id: str | None, + name: str, + ) -> None: + """Initialize ESIOS sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_attribution = coordinator.api.attribution + self._attr_unique_id = unique_id + self._attr_name = name + self._attr_device_info = DeviceInfo( + configuration_url="https://www.ree.es/es/apidatos", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.entry_id)}, + manufacturer="REE", + name="PVPC (REData API)", + ) + self._state: StateType = None + self._attrs: Mapping[str, Any] = {} async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - if state := await self.async_get_last_state(): - self._pvpc_data.state = state.state # Update 'state' value in hour changes self.async_on_remove( @@ -75,86 +162,31 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): self.hass, self.update_current_price, second=[0], minute=[0] ) ) - # Update prices at random time, 2 times/hour (don't want to upset API) - random_minute = randint(1, 29) - mins_update = [random_minute, random_minute + 30] - self.async_on_remove( - async_track_time_change( - self.hass, self.async_update_prices, second=[0], minute=mins_update - ) - ) _LOGGER.debug( - "Setup of price sensor %s (%s) with tariff '%s', " - "updating prices each hour at %s min", + "Setup of price sensor %s (%s) with tariff '%s'", self.name, self.entity_id, - self._pvpc_data.tariff, - mins_update, + self.coordinator.api.tariff, ) - now = dt_util.utcnow() - await self.async_update_prices(now) - self.update_current_price(now) - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self) -> float: - """Return the state of the sensor.""" - return self._pvpc_data.state - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._pvpc_data.state_available - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes.""" - return self._pvpc_data.attributes @callback - def update_current_price(self, now): + def update_current_price(self, now: datetime) -> None: """Update the sensor state, by selecting the current price for this hour.""" - self._pvpc_data.process_state_and_attributes(now) + self.coordinator.api.process_state_and_attributes(now) self.async_write_ha_state() - async def async_update_prices(self, now): - """Update electricity prices from the ESIOS API.""" - prices = await self._pvpc_data.async_update_prices(now) - if not prices and self._pvpc_data.source_available: - self._num_retries += 1 - if self._num_retries > 2: - _LOGGER.warning( - "%s: repeated bad data update, mark component as unavailable source", - self.entity_id, - ) - self._pvpc_data.source_available = False - return + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + self._state = self.coordinator.api.state + return self._state - retry_delay = 2 * self._num_retries * self._pvpc_data.timeout - _LOGGER.debug( - "%s: Bad update[retry:%d], will try again in %d s", - self.entity_id, - self._num_retries, - retry_delay, - ) - async_call_later(self.hass, retry_delay, self.async_update_prices) - return - - if not prices: - _LOGGER.debug("%s: data source is not yet available", self.entity_id) - return - - self._num_retries = 0 - if not self._pvpc_data.source_available: - self._pvpc_data.source_available = True - _LOGGER.warning("%s: component has recovered data access", self.entity_id) - self.update_current_price(now) + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return the state attributes.""" + self._attrs = { + _PRICE_SENSOR_ATTRIBUTES_MAP[key]: value + for key, value in self.coordinator.api.attributes.items() + if key in _PRICE_SENSOR_ATTRIBUTES_MAP + } + return self._attrs diff --git a/requirements_all.txt b/requirements_all.txt index 99f86af5307..a023f60b7a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -235,7 +235,7 @@ aiopulse==0.4.3 aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.4 +aiopvpc==3.0.0 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dbc05060732..486dd0c6199 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aiopulse==0.4.3 aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.4 +aiopvpc==3.0.0 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py index 2421c753518..632284774ee 100644 --- a/tests/components/pvpc_hourly_pricing/conftest.py +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -1,4 +1,6 @@ """Tests for the pvpc_hourly_pricing integration.""" +from http import HTTPStatus + import pytest from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN @@ -11,10 +13,7 @@ from homeassistant.const import ( from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -FIXTURE_JSON_DATA_2019_10_26 = "PVPC_CURV_DD_2019_10_26.json" -FIXTURE_JSON_DATA_2019_10_27 = "PVPC_CURV_DD_2019_10_27.json" -FIXTURE_JSON_DATA_2019_10_29 = "PVPC_CURV_DD_2019_10_29.json" -FIXTURE_JSON_DATA_2021_06_01 = "PVPC_CURV_DD_2021_06_01.json" +FIXTURE_JSON_DATA_2021_06_01 = "PVPC_DATA_2021_06_01.json" def check_valid_state(state, tariff: str, value=None, key_attr=None): @@ -27,7 +26,7 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None): try: _ = float(state.state) # safety margins for current electricity price (it shouldn't be out of [0, 0.2]) - assert -0.1 < float(state.state) < 0.3 + assert -0.1 < float(state.state) < 0.5 assert state.attributes[ATTR_TARIFF] == tariff except ValueError: pass @@ -43,28 +42,22 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None): @pytest.fixture def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker): """Create a mock config entry.""" - aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-26", - text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_26}"), - ) - aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-27", - text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_27}"), - ) - # missing day - aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-28", - text='{"message":"No values for specified archive"}', - ) - aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-29", - text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_29}"), - ) - + mask_url = "https://apidatos.ree.es/es/datos/mercados/precios-mercados-tiempo-real" + mask_url += "?time_trunc=hour&geo_ids={0}&start_date={1}T00:00&end_date={1}T23:59" # new format for prices >= 2021-06-01 + sample_data = load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2021_06_01}") + + # tariff variant with different geo_ids=8744 + aioclient_mock.get(mask_url.format(8741, "2021-06-01"), text=sample_data) + aioclient_mock.get(mask_url.format(8744, "2021-06-01"), text=sample_data) + # simulate missing day aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2021-06-01", - text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2021_06_01}"), + mask_url.format(8741, "2021-06-02"), + status=HTTPStatus.BAD_GATEWAY, + text=( + '{"errors":[{"code":502,"status":"502","title":"Bad response from ESIOS",' + '"detail":"There are no data for the selected filters."}]}' + ), ) return aioclient_mock diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json deleted file mode 100644 index dd8c73352d9..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json +++ /dev/null @@ -1,820 +0,0 @@ -{ - "PVPC": [ - { - "Dia": "26/10/2019", - "Hora": "00-01", - "GEN": "114,20", - "NOC": "65,17", - "VHC": "69,02", - "COFGEN": "0,000087148314000000", - "COFNOC": "0,000135978057000000", - "COFVHC": "0,000151138804000000", - "PMHGEN": "59,56", - "PMHNOC": "57,22", - "PMHVHC": "59,81", - "SAHGEN": "1,96", - "SAHNOC": "1,89", - "SAHVHC": "1,97", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,14", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,94", - "PCAPGEN": "5,54", - "PCAPNOC": "0,93", - "PCAPVHC": "1,31", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,01", - "CCVNOC": "1,86", - "CCVVHC": "1,95" - }, - { - "Dia": "26/10/2019", - "Hora": "01-02", - "GEN": "111,01", - "NOC": "62,10", - "VHC": "59,03", - "COFGEN": "0,000072922194000000", - "COFNOC": "0,000124822445000000", - "COFVHC": "0,000160597191000000", - "PMHGEN": "56,23", - "PMHNOC": "54,03", - "PMHVHC": "52,62", - "SAHGEN": "2,14", - "SAHNOC": "2,05", - "SAHVHC": "2,00", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,96", - "CCVNOC": "1,82", - "CCVVHC": "1,77" - }, - { - "Dia": "26/10/2019", - "Hora": "02-03", - "GEN": "105,17", - "NOC": "56,48", - "VHC": "53,56", - "COFGEN": "0,000064100056000000", - "COFNOC": "0,000117356595000000", - "COFVHC": "0,000158787037000000", - "PMHGEN": "50,26", - "PMHNOC": "48,29", - "PMHVHC": "47,03", - "SAHGEN": "2,35", - "SAHNOC": "2,26", - "SAHVHC": "2,20", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,55", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,87", - "CCVNOC": "1,73", - "CCVVHC": "1,68" - }, - { - "Dia": "26/10/2019", - "Hora": "03-04", - "GEN": "102,45", - "NOC": "53,87", - "VHC": "51,02", - "COFGEN": "0,000059549798000000", - "COFNOC": "0,000113408113000000", - "COFVHC": "0,000152391581000000", - "PMHGEN": "47,42", - "PMHNOC": "45,57", - "PMHVHC": "44,38", - "SAHGEN": "2,51", - "SAHNOC": "2,41", - "SAHVHC": "2,35", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,83", - "CCVNOC": "1,69", - "CCVVHC": "1,65" - }, - { - "Dia": "26/10/2019", - "Hora": "04-05", - "GEN": "102,15", - "NOC": "53,58", - "VHC": "50,73", - "COFGEN": "0,000057296575000000", - "COFNOC": "0,000111308472000000", - "COFVHC": "0,000145270809000000", - "PMHGEN": "47,05", - "PMHNOC": "45,21", - "PMHVHC": "44,03", - "SAHGEN": "2,58", - "SAHNOC": "2,48", - "SAHVHC": "2,41", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,83", - "CCVNOC": "1,69", - "CCVVHC": "1,64" - }, - { - "Dia": "26/10/2019", - "Hora": "05-06", - "GEN": "101,62", - "NOC": "53,13", - "VHC": "50,34", - "COFGEN": "0,000057285870000000", - "COFNOC": "0,000111061995000000", - "COFVHC": "0,000141535570000000", - "PMHGEN": "46,55", - "PMHNOC": "44,76", - "PMHVHC": "43,63", - "SAHGEN": "2,60", - "SAHNOC": "2,50", - "SAHVHC": "2,43", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,87", - "PCAPGEN": "5,54", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,82", - "CCVNOC": "1,68", - "CCVVHC": "1,64" - }, - { - "Dia": "26/10/2019", - "Hora": "06-07", - "GEN": "102,36", - "NOC": "53,90", - "VHC": "51,08", - "COFGEN": "0,000060011439000000", - "COFNOC": "0,000113191071000000", - "COFVHC": "0,000139395926000000", - "PMHGEN": "46,58", - "PMHNOC": "44,82", - "PMHVHC": "43,69", - "SAHGEN": "3,32", - "SAHNOC": "3,20", - "SAHVHC": "3,12", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,89", - "INTVHC": "0,87", - "PCAPGEN": "5,51", - "PCAPNOC": "0,92", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,82", - "CCVNOC": "1,69", - "CCVVHC": "1,64" - }, - { - "Dia": "26/10/2019", - "Hora": "07-08", - "GEN": "106,73", - "NOC": "58,10", - "VHC": "61,55", - "COFGEN": "0,000067624746000000", - "COFNOC": "0,000113073036000000", - "COFVHC": "0,000130165590000000", - "PMHGEN": "50,24", - "PMHNOC": "48,34", - "PMHVHC": "50,45", - "SAHGEN": "3,98", - "SAHNOC": "3,83", - "SAHVHC": "4,00", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,50", - "PCAPNOC": "0,92", - "PCAPVHC": "1,30", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,89", - "CCVNOC": "1,75", - "CCVVHC": "1,83" - }, - { - "Dia": "26/10/2019", - "Hora": "08-09", - "GEN": "107,75", - "NOC": "59,43", - "VHC": "62,66", - "COFGEN": "0,000083194704000000", - "COFNOC": "0,000083589950000000", - "COFVHC": "0,000069841029000000", - "PMHGEN": "51,74", - "PMHNOC": "50,02", - "PMHVHC": "51,97", - "SAHGEN": "3,62", - "SAHNOC": "3,50", - "SAHVHC": "3,63", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,40", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,89", - "CCVNOC": "1,76", - "CCVVHC": "1,83" - }, - { - "Dia": "26/10/2019", - "Hora": "09-10", - "GEN": "110,38", - "NOC": "62,09", - "VHC": "65,34", - "COFGEN": "0,000105869478000000", - "COFNOC": "0,000077963480000000", - "COFVHC": "0,000057355982000000", - "PMHGEN": "55,41", - "PMHNOC": "53,64", - "PMHVHC": "55,65", - "SAHGEN": "2,60", - "SAHNOC": "2,52", - "SAHVHC": "2,61", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,87", - "INTVHC": "0,91", - "PCAPGEN": "5,36", - "PCAPNOC": "0,90", - "PCAPVHC": "1,26", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,92", - "CCVNOC": "1,79", - "CCVVHC": "1,86" - }, - { - "Dia": "26/10/2019", - "Hora": "10-11", - "GEN": "108,10", - "NOC": "60,00", - "VHC": "63,02", - "COFGEN": "0,000121833263000000", - "COFNOC": "0,000085468800000000", - "COFVHC": "0,000063770407000000", - "PMHGEN": "53,39", - "PMHNOC": "51,77", - "PMHVHC": "53,58", - "SAHGEN": "2,42", - "SAHNOC": "2,34", - "SAHVHC": "2,42", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,87", - "INTVHC": "0,90", - "PCAPGEN": "5,32", - "PCAPNOC": "0,90", - "PCAPVHC": "1,25", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,88", - "CCVNOC": "1,76", - "CCVVHC": "1,82" - }, - { - "Dia": "26/10/2019", - "Hora": "11-12", - "GEN": "104,11", - "NOC": "56,20", - "VHC": "59,04", - "COFGEN": "0,000125947995000000", - "COFNOC": "0,000085228595000000", - "COFVHC": "0,000064070840000000", - "PMHGEN": "50,02", - "PMHNOC": "48,54", - "PMHVHC": "50,20", - "SAHGEN": "1,89", - "SAHNOC": "1,83", - "SAHVHC": "1,90", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,87", - "INTVHC": "0,90", - "PCAPGEN": "5,31", - "PCAPNOC": "0,90", - "PCAPVHC": "1,25", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,81", - "CCVNOC": "1,70", - "CCVVHC": "1,76" - }, - { - "Dia": "26/10/2019", - "Hora": "12-13", - "GEN": "103,61", - "NOC": "55,65", - "VHC": "58,52", - "COFGEN": "0,000128302145000000", - "COFNOC": "0,000082279443000000", - "COFVHC": "0,000063904657000000", - "PMHGEN": "49,50", - "PMHNOC": "47,99", - "PMHVHC": "49,67", - "SAHGEN": "1,90", - "SAHNOC": "1,84", - "SAHVHC": "1,90", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,87", - "INTVHC": "0,90", - "PCAPGEN": "5,32", - "PCAPNOC": "0,90", - "PCAPVHC": "1,25", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,81", - "CCVNOC": "1,69", - "CCVVHC": "1,75" - }, - { - "Dia": "26/10/2019", - "Hora": "13-14", - "GEN": "104,03", - "NOC": "122,60", - "VHC": "122,60", - "COFGEN": "0,000134270665000000", - "COFNOC": "0,000080726428000000", - "COFVHC": "0,000063976543000000", - "PMHGEN": "49,98", - "PMHNOC": "50,33", - "PMHVHC": "50,33", - "SAHGEN": "1,85", - "SAHNOC": "1,87", - "SAHVHC": "1,87", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,30", - "PCAPNOC": "5,50", - "PCAPVHC": "5,50", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,81", - "CCVNOC": "1,83", - "CCVVHC": "1,83" - }, - { - "Dia": "26/10/2019", - "Hora": "14-15", - "GEN": "103,44", - "NOC": "122,00", - "VHC": "122,00", - "COFGEN": "0,000130580837000000", - "COFNOC": "0,000079392022000000", - "COFVHC": "0,000064422150000000", - "PMHGEN": "49,25", - "PMHNOC": "49,60", - "PMHVHC": "49,60", - "SAHGEN": "1,97", - "SAHNOC": "1,98", - "SAHVHC": "1,98", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,32", - "PCAPNOC": "5,52", - "PCAPVHC": "5,52", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,81", - "CCVNOC": "1,82", - "CCVVHC": "1,82" - }, - { - "Dia": "26/10/2019", - "Hora": "15-16", - "GEN": "100,57", - "NOC": "119,16", - "VHC": "119,16", - "COFGEN": "0,000114850139000000", - "COFNOC": "0,000070924506000000", - "COFVHC": "0,000056150579000000", - "PMHGEN": "46,19", - "PMHNOC": "46,55", - "PMHVHC": "46,55", - "SAHGEN": "2,15", - "SAHNOC": "2,17", - "SAHVHC": "2,17", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,36", - "PCAPNOC": "5,57", - "PCAPVHC": "5,57", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,77", - "CCVNOC": "1,79", - "CCVVHC": "1,79" - }, - { - "Dia": "26/10/2019", - "Hora": "16-17", - "GEN": "99,90", - "NOC": "118,48", - "VHC": "118,48", - "COFGEN": "0,000105915899000000", - "COFNOC": "0,000065274280000000", - "COFVHC": "0,000051268616000000", - "PMHGEN": "45,44", - "PMHNOC": "45,80", - "PMHVHC": "45,80", - "SAHGEN": "2,25", - "SAHNOC": "2,27", - "SAHVHC": "2,27", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,35", - "PCAPNOC": "5,56", - "PCAPVHC": "5,56", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,76", - "CCVNOC": "1,78", - "CCVVHC": "1,78" - }, - { - "Dia": "26/10/2019", - "Hora": "17-18", - "GEN": "102,97", - "NOC": "121,53", - "VHC": "121,53", - "COFGEN": "0,000104178581000000", - "COFNOC": "0,000063611672000000", - "COFVHC": "0,000049947652000000", - "PMHGEN": "48,62", - "PMHNOC": "48,96", - "PMHVHC": "48,96", - "SAHGEN": "2,14", - "SAHNOC": "2,16", - "SAHVHC": "2,16", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,33", - "PCAPNOC": "5,53", - "PCAPVHC": "5,53", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,80", - "CCVNOC": "1,82", - "CCVVHC": "1,82" - }, - { - "Dia": "26/10/2019", - "Hora": "18-19", - "GEN": "107,71", - "NOC": "126,30", - "VHC": "126,30", - "COFGEN": "0,000106669089000000", - "COFNOC": "0,000070000350000000", - "COFVHC": "0,000061100876000000", - "PMHGEN": "53,37", - "PMHNOC": "53,74", - "PMHVHC": "53,74", - "SAHGEN": "2,05", - "SAHNOC": "2,06", - "SAHVHC": "2,06", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,33", - "PCAPNOC": "5,53", - "PCAPVHC": "5,53", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,87", - "CCVNOC": "1,89", - "CCVVHC": "1,89" - }, - { - "Dia": "26/10/2019", - "Hora": "19-20", - "GEN": "118,75", - "NOC": "137,49", - "VHC": "137,49", - "COFGEN": "0,000115010612000000", - "COFNOC": "0,000095780287000000", - "COFVHC": "0,000092687680000000", - "PMHGEN": "64,21", - "PMHNOC": "64,71", - "PMHVHC": "64,71", - "SAHGEN": "2,07", - "SAHNOC": "2,08", - "SAHVHC": "2,08", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,35", - "PCAPNOC": "5,55", - "PCAPVHC": "5,55", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,04", - "CCVNOC": "2,06", - "CCVVHC": "2,06" - }, - { - "Dia": "26/10/2019", - "Hora": "20-21", - "GEN": "124,00", - "NOC": "142,78", - "VHC": "142,78", - "COFGEN": "0,000129085428000000", - "COFNOC": "0,000144302922000000", - "COFVHC": "0,000185612441000000", - "PMHGEN": "69,13", - "PMHNOC": "69,67", - "PMHVHC": "69,67", - "SAHGEN": "2,30", - "SAHNOC": "2,32", - "SAHVHC": "2,32", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,36", - "PCAPNOC": "5,56", - "PCAPVHC": "5,56", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,12", - "CCVNOC": "2,14", - "CCVVHC": "2,14" - }, - { - "Dia": "26/10/2019", - "Hora": "21-22", - "GEN": "124,16", - "NOC": "143,00", - "VHC": "143,00", - "COFGEN": "0,000133109692000000", - "COFNOC": "0,000151101318000000", - "COFVHC": "0,000197574745000000", - "PMHGEN": "68,50", - "PMHNOC": "69,09", - "PMHVHC": "69,09", - "SAHGEN": "3,05", - "SAHNOC": "3,07", - "SAHVHC": "3,07", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,38", - "PCAPNOC": "5,60", - "PCAPVHC": "5,60", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,13", - "CCVNOC": "2,15", - "CCVVHC": "2,15" - }, - { - "Dia": "26/10/2019", - "Hora": "22-23", - "GEN": "120,30", - "NOC": "139,04", - "VHC": "139,04", - "COFGEN": "0,000120157209000000", - "COFNOC": "0,000148137882000000", - "COFVHC": "0,000194906294000000", - "PMHGEN": "64,33", - "PMHNOC": "64,82", - "PMHVHC": "64,82", - "SAHGEN": "3,38", - "SAHNOC": "3,41", - "SAHVHC": "3,41", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,42", - "PCAPNOC": "5,63", - "PCAPVHC": "5,63", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,08", - "CCVNOC": "2,10", - "CCVVHC": "2,10" - }, - { - "Dia": "26/10/2019", - "Hora": "23-24", - "GEN": "118,05", - "NOC": "69,05", - "VHC": "72,93", - "COFGEN": "0,000103870556000000", - "COFNOC": "0,000146233245000000", - "COFVHC": "0,000182184931000000", - "PMHGEN": "61,54", - "PMHNOC": "59,25", - "PMHVHC": "61,80", - "SAHGEN": "3,85", - "SAHNOC": "3,71", - "SAHVHC": "3,87", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,92", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,49", - "PCAPNOC": "0,92", - "PCAPVHC": "1,29", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,05", - "CCVNOC": "1,91", - "CCVVHC": "2,00" - } - ] -} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json deleted file mode 100644 index 66afc5a91d3..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json +++ /dev/null @@ -1,854 +0,0 @@ -{ - "PVPC": [ - { - "Dia": "27/10/2019", - "Hora": "00-01", - "GEN": "115,15", - "NOC": "65,95", - "VHC": "69,94", - "COFGEN": "0,000083408754000000", - "COFNOC": "0,000125204015000000", - "COFVHC": "0,000143740251000000", - "PMHGEN": "59,13", - "PMHNOC": "56,72", - "PMHVHC": "59,37", - "SAHGEN": "3,28", - "SAHNOC": "3,14", - "SAHVHC": "3,29", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,14", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,94", - "PCAPGEN": "5,58", - "PCAPNOC": "0,93", - "PCAPVHC": "1,32", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,03", - "CCVNOC": "1,88", - "CCVVHC": "1,97" - }, - { - "Dia": "27/10/2019", - "Hora": "01-02", - "GEN": "109,63", - "NOC": "60,60", - "VHC": "57,48", - "COFGEN": "0,000069962863000000", - "COFNOC": "0,000114629494000000", - "COFVHC": "0,000147622130000000", - "PMHGEN": "53,21", - "PMHNOC": "51,01", - "PMHVHC": "49,61", - "SAHGEN": "3,72", - "SAHNOC": "3,57", - "SAHVHC": "3,47", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,95", - "INTNOC": "0,91", - "INTVHC": "0,88", - "PCAPGEN": "5,61", - "PCAPNOC": "0,94", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,95", - "CCVNOC": "1,80", - "CCVVHC": "1,75" - }, - { - "Dia": "27/10/2019", - "Hora": "02-03", - "GEN": "108,41", - "NOC": "59,38", - "VHC": "56,29", - "COFGEN": "0,000065978330000000", - "COFNOC": "0,000111216294000000", - "COFVHC": "0,000145651145000000", - "PMHGEN": "52,09", - "PMHNOC": "49,90", - "PMHVHC": "48,53", - "SAHGEN": "3,62", - "SAHNOC": "3,47", - "SAHVHC": "3,37", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,95", - "INTNOC": "0,91", - "INTVHC": "0,88", - "PCAPGEN": "5,63", - "PCAPNOC": "0,94", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,93", - "CCVNOC": "1,79", - "CCVVHC": "1,73" - }, - { - "Dia": "27/10/2019", - "Hora": "03-04", - "GEN": "108,22", - "NOC": "59,31", - "VHC": "56,27", - "COFGEN": "0,000061999708000000", - "COFNOC": "0,000107809474000000", - "COFVHC": "0,000143671560000000", - "PMHGEN": "51,88", - "PMHNOC": "49,78", - "PMHVHC": "48,45", - "SAHGEN": "3,68", - "SAHNOC": "3,53", - "SAHVHC": "3,44", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,59", - "PCAPNOC": "0,93", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,93", - "CCVNOC": "1,78", - "CCVVHC": "1,73" - }, - { - "Dia": "27/10/2019", - "Hora": "04-05", - "GEN": "107,03", - "NOC": "58,16", - "VHC": "55,10", - "COFGEN": "0,000057358428000000", - "COFNOC": "0,000103595831000000", - "COFVHC": "0,000139122535000000", - "PMHGEN": "50,53", - "PMHNOC": "48,48", - "PMHVHC": "47,15", - "SAHGEN": "3,85", - "SAHNOC": "3,69", - "SAHVHC": "3,59", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,91", - "INTVHC": "0,88", - "PCAPGEN": "5,60", - "PCAPNOC": "0,93", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,91", - "CCVNOC": "1,76", - "CCVVHC": "1,71" - }, - { - "Dia": "27/10/2019", - "Hora": "05-06", - "GEN": "104,79", - "NOC": "56,01", - "VHC": "53,06", - "COFGEN": "0,000055060063000000", - "COFNOC": "0,000101732765000000", - "COFVHC": "0,000134441142000000", - "PMHGEN": "48,28", - "PMHNOC": "46,32", - "PMHVHC": "45,08", - "SAHGEN": "3,91", - "SAHNOC": "3,75", - "SAHVHC": "3,65", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,59", - "PCAPNOC": "0,93", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,87", - "CCVNOC": "1,73", - "CCVVHC": "1,68" - }, - { - "Dia": "27/10/2019", - "Hora": "06-07", - "GEN": "104,56", - "NOC": "55,85", - "VHC": "52,94", - "COFGEN": "0,000054511300000000", - "COFNOC": "0,000101250808000000", - "COFVHC": "0,000131206727000000", - "PMHGEN": "48,10", - "PMHNOC": "46,18", - "PMHVHC": "44,98", - "SAHGEN": "3,90", - "SAHNOC": "3,74", - "SAHVHC": "3,65", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,57", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,87", - "CCVNOC": "1,73", - "CCVVHC": "1,68" - }, - { - "Dia": "27/10/2019", - "Hora": "07-08", - "GEN": "107,72", - "NOC": "58,93", - "VHC": "55,95", - "COFGEN": "0,000056191283000000", - "COFNOC": "0,000102978398000000", - "COFVHC": "0,000130073563000000", - "PMHGEN": "50,23", - "PMHNOC": "48,26", - "PMHVHC": "47,01", - "SAHGEN": "4,89", - "SAHNOC": "4,70", - "SAHVHC": "4,57", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,91", - "CCVNOC": "1,77", - "CCVVHC": "1,72" - }, - { - "Dia": "27/10/2019", - "Hora": "08-09", - "GEN": "107,80", - "NOC": "59,29", - "VHC": "62,70", - "COFGEN": "0,000060083432000000", - "COFNOC": "0,000100348617000000", - "COFVHC": "0,000118460190000000", - "PMHGEN": "50,94", - "PMHNOC": "49,13", - "PMHVHC": "51,20", - "SAHGEN": "4,38", - "SAHNOC": "4,23", - "SAHVHC": "4,40", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,92", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,47", - "PCAPNOC": "0,92", - "PCAPVHC": "1,29", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,90", - "CCVNOC": "1,76", - "CCVVHC": "1,84" - }, - { - "Dia": "27/10/2019", - "Hora": "09-10", - "GEN": "106,74", - "NOC": "58,40", - "VHC": "61,63", - "COFGEN": "0,000070236674000000", - "COFNOC": "0,000071273888000000", - "COFVHC": "0,000062511624000000", - "PMHGEN": "50,00", - "PMHNOC": "48,29", - "PMHVHC": "50,22", - "SAHGEN": "4,34", - "SAHNOC": "4,20", - "SAHVHC": "4,36", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,92", - "PCAPGEN": "5,42", - "PCAPNOC": "0,91", - "PCAPVHC": "1,28", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,87", - "CCVNOC": "1,74", - "CCVVHC": "1,82" - }, - { - "Dia": "27/10/2019", - "Hora": "10-11", - "GEN": "106,13", - "NOC": "57,81", - "VHC": "61,02", - "COFGEN": "0,000089379429000000", - "COFNOC": "0,000066131351000000", - "COFVHC": "0,000053107930000000", - "PMHGEN": "50,32", - "PMHNOC": "48,60", - "PMHVHC": "50,54", - "SAHGEN": "3,43", - "SAHNOC": "3,31", - "SAHVHC": "3,44", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,92", - "PCAPGEN": "5,42", - "PCAPNOC": "0,91", - "PCAPVHC": "1,28", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,86", - "CCVNOC": "1,73", - "CCVVHC": "1,81" - }, - { - "Dia": "27/10/2019", - "Hora": "11-12", - "GEN": "105,00", - "NOC": "56,78", - "VHC": "59,91", - "COFGEN": "0,000106229062000000", - "COFNOC": "0,000075658481000000", - "COFVHC": "0,000058816566000000", - "PMHGEN": "50,34", - "PMHNOC": "48,65", - "PMHVHC": "50,56", - "SAHGEN": "2,33", - "SAHNOC": "2,25", - "SAHVHC": "2,34", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,39", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,84", - "CCVNOC": "1,72", - "CCVVHC": "1,79" - }, - { - "Dia": "27/10/2019", - "Hora": "12-13", - "GEN": "105,07", - "NOC": "56,79", - "VHC": "59,92", - "COFGEN": "0,000113739886000000", - "COFNOC": "0,000079251893000000", - "COFVHC": "0,000061868784000000", - "PMHGEN": "50,41", - "PMHNOC": "48,69", - "PMHVHC": "50,59", - "SAHGEN": "2,31", - "SAHNOC": "2,23", - "SAHVHC": "2,32", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,40", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,85", - "CCVNOC": "1,72", - "CCVVHC": "1,79" - }, - { - "Dia": "27/10/2019", - "Hora": "13-14", - "GEN": "104,67", - "NOC": "123,29", - "VHC": "59,59", - "COFGEN": "0,000116885572000000", - "COFNOC": "0,000077561607000000", - "COFVHC": "0,000061189779000000", - "PMHGEN": "50,08", - "PMHNOC": "50,47", - "PMHVHC": "50,30", - "SAHGEN": "2,29", - "SAHNOC": "2,31", - "SAHVHC": "2,30", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,37", - "PCAPNOC": "5,57", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "2,88", - "CCVGEN": "1,83", - "CCVNOC": "1,85", - "CCVVHC": "1,78" - }, - { - "Dia": "27/10/2019", - "Hora": "14-15", - "GEN": "107,41", - "NOC": "126,05", - "VHC": "126,05", - "COFGEN": "0,000122253070000000", - "COFNOC": "0,000076034460000000", - "COFVHC": "0,000059795888000000", - "PMHGEN": "52,87", - "PMHNOC": "53,28", - "PMHVHC": "53,28", - "SAHGEN": "2,20", - "SAHNOC": "2,22", - "SAHVHC": "2,22", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,37", - "PCAPNOC": "5,57", - "PCAPVHC": "5,57", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,87", - "CCVNOC": "1,89", - "CCVVHC": "1,89" - }, - { - "Dia": "27/10/2019", - "Hora": "15-16", - "GEN": "108,36", - "NOC": "127,06", - "VHC": "127,06", - "COFGEN": "0,000120316270000000", - "COFNOC": "0,000073732639000000", - "COFVHC": "0,000059483320000000", - "PMHGEN": "53,68", - "PMHNOC": "54,14", - "PMHVHC": "54,14", - "SAHGEN": "2,29", - "SAHNOC": "2,31", - "SAHVHC": "2,31", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,39", - "PCAPNOC": "5,60", - "PCAPVHC": "5,60", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,89", - "CCVNOC": "1,91", - "CCVVHC": "1,91" - }, - { - "Dia": "27/10/2019", - "Hora": "16-17", - "GEN": "106,15", - "NOC": "124,78", - "VHC": "124,78", - "COFGEN": "0,000106276301000000", - "COFNOC": "0,000065442255000000", - "COFVHC": "0,000053614900000000", - "PMHGEN": "51,38", - "PMHNOC": "51,78", - "PMHVHC": "51,78", - "SAHGEN": "2,40", - "SAHNOC": "2,42", - "SAHVHC": "2,42", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,40", - "PCAPNOC": "5,61", - "PCAPVHC": "5,61", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,86", - "CCVNOC": "1,88", - "CCVVHC": "1,88" - }, - { - "Dia": "27/10/2019", - "Hora": "17-18", - "GEN": "105,09", - "NOC": "123,72", - "VHC": "123,72", - "COFGEN": "0,000098092024000000", - "COFNOC": "0,000060340481000000", - "COFVHC": "0,000050280869000000", - "PMHGEN": "51,35", - "PMHNOC": "51,75", - "PMHVHC": "51,75", - "SAHGEN": "1,40", - "SAHNOC": "1,41", - "SAHVHC": "1,41", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,40", - "PCAPNOC": "5,61", - "PCAPVHC": "5,61", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,85", - "CCVNOC": "1,86", - "CCVVHC": "1,86" - }, - { - "Dia": "27/10/2019", - "Hora": "18-19", - "GEN": "108,12", - "NOC": "126,77", - "VHC": "126,77", - "COFGEN": "0,000095857172000000", - "COFNOC": "0,000058545227000000", - "COFVHC": "0,000049936767000000", - "PMHGEN": "54,41", - "PMHNOC": "54,83", - "PMHVHC": "54,83", - "SAHGEN": "1,35", - "SAHNOC": "1,36", - "SAHVHC": "1,36", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,38", - "PCAPNOC": "5,59", - "PCAPVHC": "5,59", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,89", - "CCVNOC": "1,91", - "CCVVHC": "1,91" - }, - { - "Dia": "27/10/2019", - "Hora": "19-20", - "GEN": "112,76", - "NOC": "131,51", - "VHC": "131,51", - "COFGEN": "0,000099686581000000", - "COFNOC": "0,000063674261000000", - "COFVHC": "0,000057884599000000", - "PMHGEN": "58,53", - "PMHNOC": "59,03", - "PMHVHC": "59,03", - "SAHGEN": "1,77", - "SAHNOC": "1,79", - "SAHVHC": "1,79", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,40", - "PCAPNOC": "5,62", - "PCAPVHC": "5,62", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,96", - "CCVNOC": "1,98", - "CCVVHC": "1,98" - }, - { - "Dia": "27/10/2019", - "Hora": "20-21", - "GEN": "118,69", - "NOC": "137,48", - "VHC": "137,48", - "COFGEN": "0,000111025948000000", - "COFNOC": "0,000087846097000000", - "COFVHC": "0,000084304207000000", - "PMHGEN": "64,79", - "PMHNOC": "65,35", - "PMHVHC": "65,35", - "SAHGEN": "1,34", - "SAHNOC": "1,35", - "SAHVHC": "1,35", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,40", - "PCAPNOC": "5,62", - "PCAPVHC": "5,62", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,05", - "CCVNOC": "2,07", - "CCVVHC": "2,07" - }, - { - "Dia": "27/10/2019", - "Hora": "21-22", - "GEN": "121,19", - "NOC": "139,94", - "VHC": "139,94", - "COFGEN": "0,000129356812000000", - "COFNOC": "0,000137580750000000", - "COFVHC": "0,000175068439000000", - "PMHGEN": "66,00", - "PMHNOC": "66,51", - "PMHVHC": "66,51", - "SAHGEN": "2,64", - "SAHNOC": "2,66", - "SAHVHC": "2,66", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,38", - "PCAPNOC": "5,58", - "PCAPVHC": "5,58", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,08", - "CCVNOC": "2,10", - "CCVVHC": "2,10" - }, - { - "Dia": "27/10/2019", - "Hora": "22-23", - "GEN": "120,21", - "NOC": "138,96", - "VHC": "138,96", - "COFGEN": "0,000132818174000000", - "COFNOC": "0,000143862321000000", - "COFVHC": "0,000185393247000000", - "PMHGEN": "65,72", - "PMHNOC": "66,23", - "PMHVHC": "66,23", - "SAHGEN": "1,94", - "SAHNOC": "1,96", - "SAHVHC": "1,96", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,38", - "PCAPNOC": "5,59", - "PCAPVHC": "5,59", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,07", - "CCVNOC": "2,09", - "CCVVHC": "2,09" - }, - { - "Dia": "27/10/2019", - "Hora": "23-24", - "GEN": "117,85", - "NOC": "68,93", - "VHC": "136,63", - "COFGEN": "0,000117725347000000", - "COFNOC": "0,000138623638000000", - "COFVHC": "0,000180725170000000", - "PMHGEN": "62,92", - "PMHNOC": "60,64", - "PMHVHC": "63,46", - "SAHGEN": "2,28", - "SAHNOC": "2,20", - "SAHVHC": "2,30", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,92", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,48", - "PCAPNOC": "0,92", - "PCAPVHC": "5,69", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "62,01", - "CCVGEN": "2,05", - "CCVNOC": "1,91", - "CCVVHC": "2,07" - }, - { - "Dia": "27/10/2019", - "Hora": "24-25", - "GEN": "118,42", - "NOC": "69,35", - "VHC": "73,34", - "COFGEN": "0,000097485259000000", - "COFNOC": "0,000133828173000000", - "COFVHC": "0,000166082424000000", - "PMHGEN": "63,21", - "PMHNOC": "60,82", - "PMHVHC": "63,52", - "SAHGEN": "2,51", - "SAHNOC": "2,41", - "SAHVHC": "2,52", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,94", - "PCAPGEN": "5,52", - "PCAPNOC": "0,92", - "PCAPVHC": "1,30", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,07", - "CCVNOC": "1,92", - "CCVVHC": "2,01" - } - ] -} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json deleted file mode 100644 index 631e77f5e76..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json +++ /dev/null @@ -1,820 +0,0 @@ -{ - "PVPC": [ - { - "Dia": "29/10/2019", - "Hora": "00-01", - "GEN": "117,17", - "NOC": "68,21", - "VHC": "72,10", - "COFGEN": "0,000077051997000000", - "COFNOC": "0,000124733002000000", - "COFVHC": "0,000143780107000000", - "PMHGEN": "62,55", - "PMHNOC": "60,23", - "PMHVHC": "62,86", - "SAHGEN": "1,96", - "SAHNOC": "1,89", - "SAHVHC": "1,97", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,50", - "PCAPNOC": "0,92", - "PCAPVHC": "1,30", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,04", - "CCVNOC": "1,90", - "CCVVHC": "1,99" - }, - { - "Dia": "29/10/2019", - "Hora": "01-02", - "GEN": "115,34", - "NOC": "66,27", - "VHC": "63,14", - "COFGEN": "0,000063669626000000", - "COFNOC": "0,000113703738000000", - "COFVHC": "0,000153709241000000", - "PMHGEN": "60,54", - "PMHNOC": "58,17", - "PMHVHC": "56,70", - "SAHGEN": "2,11", - "SAHNOC": "2,02", - "SAHVHC": "1,97", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,87", - "PCAPGEN": "5,54", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "2,02", - "CCVNOC": "1,88", - "CCVVHC": "1,83" - }, - { - "Dia": "29/10/2019", - "Hora": "02-03", - "GEN": "112,37", - "NOC": "63,40", - "VHC": "60,25", - "COFGEN": "0,000057299719000000", - "COFNOC": "0,000107847932000000", - "COFVHC": "0,000151346355000000", - "PMHGEN": "57,42", - "PMHNOC": "55,17", - "PMHVHC": "53,69", - "SAHGEN": "2,27", - "SAHNOC": "2,18", - "SAHVHC": "2,12", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,57", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,98", - "CCVNOC": "1,84", - "CCVVHC": "1,79" - }, - { - "Dia": "29/10/2019", - "Hora": "03-04", - "GEN": "111,32", - "NOC": "62,39", - "VHC": "59,27", - "COFGEN": "0,000054631496000000", - "COFNOC": "0,000105135123000000", - "COFVHC": "0,000145712713000000", - "PMHGEN": "55,92", - "PMHNOC": "53,73", - "PMHVHC": "52,29", - "SAHGEN": "2,74", - "SAHNOC": "2,63", - "SAHVHC": "2,56", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,57", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,97", - "CCVNOC": "1,82", - "CCVVHC": "1,77" - }, - { - "Dia": "29/10/2019", - "Hora": "04-05", - "GEN": "111,08", - "NOC": "62,17", - "VHC": "59,04", - "COFGEN": "0,000053587732000000", - "COFNOC": "0,000103791403000000", - "COFVHC": "0,000139739507000000", - "PMHGEN": "55,64", - "PMHNOC": "53,46", - "PMHVHC": "52,03", - "SAHGEN": "2,79", - "SAHNOC": "2,68", - "SAHVHC": "2,61", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,96", - "CCVNOC": "1,82", - "CCVVHC": "1,77" - }, - { - "Dia": "29/10/2019", - "Hora": "05-06", - "GEN": "113,57", - "NOC": "64,62", - "VHC": "61,53", - "COFGEN": "0,000054978965000000", - "COFNOC": "0,000104220226000000", - "COFVHC": "0,000135044065000000", - "PMHGEN": "58,23", - "PMHNOC": "55,99", - "PMHVHC": "54,57", - "SAHGEN": "2,69", - "SAHNOC": "2,58", - "SAHVHC": "2,52", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,87", - "PCAPGEN": "5,53", - "PCAPNOC": "0,92", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "2,00", - "CCVNOC": "1,85", - "CCVVHC": "1,80" - }, - { - "Dia": "29/10/2019", - "Hora": "06-07", - "GEN": "113,48", - "NOC": "64,78", - "VHC": "61,84", - "COFGEN": "0,000063808739000000", - "COFNOC": "0,000109956697000000", - "COFVHC": "0,000134904594000000", - "PMHGEN": "58,13", - "PMHNOC": "56,06", - "PMHVHC": "54,78", - "SAHGEN": "2,80", - "SAHNOC": "2,70", - "SAHVHC": "2,64", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,12", - "INTGEN": "0,92", - "INTNOC": "0,89", - "INTVHC": "0,87", - "PCAPGEN": "5,45", - "PCAPNOC": "0,91", - "PCAPVHC": "0,71", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,98", - "CCVNOC": "1,84", - "CCVVHC": "1,80" - }, - { - "Dia": "29/10/2019", - "Hora": "07-08", - "GEN": "118,40", - "NOC": "69,72", - "VHC": "73,36", - "COFGEN": "0,000086957107000000", - "COFNOC": "0,000119021762000000", - "COFVHC": "0,000131848949000000", - "PMHGEN": "63,73", - "PMHNOC": "61,60", - "PMHVHC": "64,00", - "SAHGEN": "2,12", - "SAHNOC": "2,05", - "SAHVHC": "2,13", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,40", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,05", - "CCVNOC": "1,91", - "CCVVHC": "1,99" - }, - { - "Dia": "29/10/2019", - "Hora": "08-09", - "GEN": "117,71", - "NOC": "69,47", - "VHC": "72,71", - "COFGEN": "0,000103659543000000", - "COFNOC": "0,000093080441000000", - "COFVHC": "0,000078478538000000", - "PMHGEN": "63,59", - "PMHNOC": "61,75", - "PMHVHC": "63,81", - "SAHGEN": "1,76", - "SAHNOC": "1,71", - "SAHVHC": "1,77", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,12", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,86", - "INTVHC": "0,89", - "PCAPGEN": "5,27", - "PCAPNOC": "0,89", - "PCAPVHC": "1,24", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,01", - "CCVNOC": "1,89", - "CCVVHC": "1,96" - }, - { - "Dia": "29/10/2019", - "Hora": "09-10", - "GEN": "115,84", - "NOC": "67,79", - "VHC": "70,80", - "COFGEN": "0,000109607743000000", - "COFNOC": "0,000077907419000000", - "COFVHC": "0,000061476325000000", - "PMHGEN": "62,27", - "PMHNOC": "60,57", - "PMHVHC": "62,44", - "SAHGEN": "1,29", - "SAHNOC": "1,25", - "SAHVHC": "1,29", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,12", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,86", - "INTVHC": "0,88", - "PCAPGEN": "5,23", - "PCAPNOC": "0,88", - "PCAPVHC": "1,23", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,98", - "CCVNOC": "1,86", - "CCVVHC": "1,92" - }, - { - "Dia": "29/10/2019", - "Hora": "10-11", - "GEN": "114,70", - "NOC": "66,74", - "VHC": "69,67", - "COFGEN": "0,000115808394000000", - "COFNOC": "0,000078426619000000", - "COFVHC": "0,000062221967000000", - "PMHGEN": "61,05", - "PMHNOC": "59,42", - "PMHVHC": "61,21", - "SAHGEN": "1,41", - "SAHNOC": "1,37", - "SAHVHC": "1,41", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,12", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,86", - "INTVHC": "0,88", - "PCAPGEN": "5,22", - "PCAPNOC": "0,88", - "PCAPVHC": "1,23", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,96", - "CCVNOC": "1,84", - "CCVVHC": "1,90" - }, - { - "Dia": "29/10/2019", - "Hora": "11-12", - "GEN": "114,45", - "NOC": "66,51", - "VHC": "69,43", - "COFGEN": "0,000117753360000000", - "COFNOC": "0,000076432674000000", - "COFVHC": "0,000061112533000000", - "PMHGEN": "60,85", - "PMHNOC": "59,23", - "PMHVHC": "61,01", - "SAHGEN": "1,37", - "SAHNOC": "1,34", - "SAHVHC": "1,38", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,12", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,85", - "INTVHC": "0,88", - "PCAPGEN": "5,21", - "PCAPNOC": "0,88", - "PCAPVHC": "1,23", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,95", - "CCVNOC": "1,84", - "CCVVHC": "1,90" - }, - { - "Dia": "29/10/2019", - "Hora": "12-13", - "GEN": "114,46", - "NOC": "133,04", - "VHC": "69,45", - "COFGEN": "0,000121492044000000", - "COFNOC": "0,000074703573000000", - "COFVHC": "0,000061457855000000", - "PMHGEN": "60,95", - "PMHNOC": "61,33", - "PMHVHC": "61,11", - "SAHGEN": "1,30", - "SAHNOC": "1,31", - "SAHVHC": "1,31", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,88", - "INTVHC": "0,88", - "PCAPGEN": "5,20", - "PCAPNOC": "5,39", - "PCAPVHC": "1,22", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "2,88", - "CCVGEN": "1,95", - "CCVNOC": "1,97", - "CCVVHC": "1,90" - }, - { - "Dia": "29/10/2019", - "Hora": "13-14", - "GEN": "113,37", - "NOC": "131,94", - "VHC": "131,94", - "COFGEN": "0,000126490319000000", - "COFNOC": "0,000074777760000000", - "COFVHC": "0,000060760068000000", - "PMHGEN": "59,86", - "PMHNOC": "60,24", - "PMHVHC": "60,24", - "SAHGEN": "1,32", - "SAHNOC": "1,33", - "SAHVHC": "1,33", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,87", - "INTNOC": "0,88", - "INTVHC": "0,88", - "PCAPGEN": "5,19", - "PCAPNOC": "5,38", - "PCAPVHC": "5,38", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,93", - "CCVNOC": "1,95", - "CCVVHC": "1,95" - }, - { - "Dia": "29/10/2019", - "Hora": "14-15", - "GEN": "112,88", - "NOC": "131,46", - "VHC": "131,46", - "COFGEN": "0,000120771211000000", - "COFNOC": "0,000072095790000000", - "COFVHC": "0,000058765117000000", - "PMHGEN": "59,31", - "PMHNOC": "59,68", - "PMHVHC": "59,68", - "SAHGEN": "1,37", - "SAHNOC": "1,38", - "SAHVHC": "1,38", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,88", - "INTVHC": "0,88", - "PCAPGEN": "5,21", - "PCAPNOC": "5,40", - "PCAPVHC": "5,40", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,93", - "CCVNOC": "1,94", - "CCVVHC": "1,94" - }, - { - "Dia": "29/10/2019", - "Hora": "15-16", - "GEN": "115,75", - "NOC": "134,41", - "VHC": "134,41", - "COFGEN": "0,000110808247000000", - "COFNOC": "0,000066006577000000", - "COFVHC": "0,000053763013000000", - "PMHGEN": "62,14", - "PMHNOC": "62,58", - "PMHVHC": "62,58", - "SAHGEN": "1,34", - "SAHNOC": "1,35", - "SAHVHC": "1,35", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,23", - "PCAPNOC": "5,42", - "PCAPVHC": "5,42", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,98", - "CCVNOC": "1,99", - "CCVVHC": "1,99" - }, - { - "Dia": "29/10/2019", - "Hora": "16-17", - "GEN": "118,08", - "NOC": "136,75", - "VHC": "136,75", - "COFGEN": "0,000107924950000000", - "COFNOC": "0,000063090606000000", - "COFVHC": "0,000052115396000000", - "PMHGEN": "64,48", - "PMHNOC": "64,93", - "PMHVHC": "64,93", - "SAHGEN": "1,31", - "SAHNOC": "1,32", - "SAHVHC": "1,32", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,22", - "PCAPNOC": "5,42", - "PCAPVHC": "5,42", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,01", - "CCVNOC": "2,03", - "CCVVHC": "2,03" - }, - { - "Dia": "29/10/2019", - "Hora": "17-18", - "GEN": "121,32", - "NOC": "139,95", - "VHC": "139,95", - "COFGEN": "0,000111993776000000", - "COFNOC": "0,000063840323000000", - "COFVHC": "0,000053264660000000", - "PMHGEN": "67,88", - "PMHNOC": "68,30", - "PMHVHC": "68,30", - "SAHGEN": "1,10", - "SAHNOC": "1,11", - "SAHVHC": "1,11", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,88", - "INTVHC": "0,88", - "PCAPGEN": "5,22", - "PCAPNOC": "5,41", - "PCAPVHC": "5,41", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,06", - "CCVNOC": "2,07", - "CCVVHC": "2,07" - }, - { - "Dia": "29/10/2019", - "Hora": "18-19", - "GEN": "126,19", - "NOC": "144,85", - "VHC": "144,85", - "COFGEN": "0,000117118878000000", - "COFNOC": "0,000072058416000000", - "COFVHC": "0,000066417528000000", - "PMHGEN": "69,04", - "PMHNOC": "69,47", - "PMHVHC": "69,47", - "SAHGEN": "4,73", - "SAHNOC": "4,76", - "SAHVHC": "4,76", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,22", - "PCAPNOC": "5,42", - "PCAPVHC": "5,42", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,13", - "CCVNOC": "2,15", - "CCVVHC": "2,15" - }, - { - "Dia": "29/10/2019", - "Hora": "19-20", - "GEN": "125,34", - "NOC": "144,06", - "VHC": "144,06", - "COFGEN": "0,000128443388000000", - "COFNOC": "0,000098772457000000", - "COFVHC": "0,000100678475000000", - "PMHGEN": "68,61", - "PMHNOC": "69,09", - "PMHVHC": "69,09", - "SAHGEN": "4,31", - "SAHNOC": "4,34", - "SAHVHC": "4,34", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,24", - "PCAPNOC": "5,43", - "PCAPVHC": "5,43", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,12", - "CCVNOC": "2,14", - "CCVVHC": "2,14" - }, - { - "Dia": "29/10/2019", - "Hora": "20-21", - "GEN": "120,62", - "NOC": "139,31", - "VHC": "139,31", - "COFGEN": "0,000144847952000000", - "COFNOC": "0,000148736569000000", - "COFVHC": "0,000192706770000000", - "PMHGEN": "67,11", - "PMHNOC": "67,58", - "PMHVHC": "67,58", - "SAHGEN": "1,12", - "SAHNOC": "1,12", - "SAHVHC": "1,12", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,27", - "PCAPNOC": "5,47", - "PCAPVHC": "5,47", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,06", - "CCVNOC": "2,07", - "CCVVHC": "2,07" - }, - { - "Dia": "29/10/2019", - "Hora": "21-22", - "GEN": "120,67", - "NOC": "139,36", - "VHC": "139,36", - "COFGEN": "0,000143400205000000", - "COFNOC": "0,000153448551000000", - "COFVHC": "0,000201113372000000", - "PMHGEN": "66,43", - "PMHNOC": "66,90", - "PMHVHC": "66,90", - "SAHGEN": "1,80", - "SAHNOC": "1,81", - "SAHVHC": "1,81", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,30", - "PCAPNOC": "5,50", - "PCAPVHC": "5,50", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,06", - "CCVNOC": "2,08", - "CCVVHC": "2,08" - }, - { - "Dia": "29/10/2019", - "Hora": "22-23", - "GEN": "117,80", - "NOC": "69,35", - "VHC": "136,53", - "COFGEN": "0,000122948482000000", - "COFNOC": "0,000146077289000000", - "COFVHC": "0,000194614149000000", - "PMHGEN": "63,25", - "PMHNOC": "61,28", - "PMHVHC": "63,75", - "SAHGEN": "2,10", - "SAHNOC": "2,03", - "SAHVHC": "2,11", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,87", - "INTVHC": "0,91", - "PCAPGEN": "5,34", - "PCAPNOC": "0,90", - "PCAPVHC": "5,54", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "62,01", - "CCVGEN": "2,03", - "CCVNOC": "1,90", - "CCVVHC": "2,04" - }, - { - "Dia": "29/10/2019", - "Hora": "23-24", - "GEN": "111,95", - "NOC": "63,48", - "VHC": "66,87", - "COFGEN": "0,000098841799000000", - "COFNOC": "0,000139677463000000", - "COFVHC": "0,000176886301000000", - "PMHGEN": "56,97", - "PMHNOC": "55,07", - "PMHVHC": "57,22", - "SAHGEN": "2,52", - "SAHNOC": "2,44", - "SAHVHC": "2,53", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,40", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,95", - "CCVNOC": "1,82", - "CCVVHC": "1,89" - } - ] -} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json deleted file mode 100644 index 59559d3c3f7..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json +++ /dev/null @@ -1,604 +0,0 @@ -{ - "PVPC": [ - { - "Dia": "01/06/2021", - "Hora": "00-01", - "PCB": "116,33", - "CYM": "116,33", - "COF2TD": "0,000088075182000000", - "PMHPCB": "104,00", - "PMHCYM": "104,00", - "SAHPCB": "3,56", - "SAHCYM": "3,56", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,57", - "CCVCYM": "2,57", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "01-02", - "PCB": "115,95", - "CYM": "115,95", - "COF2TD": "0,000073094842000000", - "PMHPCB": "103,18", - "PMHCYM": "103,18", - "SAHPCB": "3,99", - "SAHCYM": "3,99", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,58", - "CCVCYM": "2,58", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "02-03", - "PCB": "114,89", - "CYM": "114,89", - "COF2TD": "0,000065114032000000", - "PMHPCB": "101,87", - "PMHCYM": "101,87", - "SAHPCB": "4,25", - "SAHCYM": "4,25", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,56", - "CCVCYM": "2,56", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "03-04", - "PCB": "114,96", - "CYM": "114,96", - "COF2TD": "0,000061272596000000", - "PMHPCB": "102,01", - "PMHCYM": "102,01", - "SAHPCB": "4,19", - "SAHCYM": "4,19", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,57", - "CCVCYM": "2,57", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "04-05", - "PCB": "114,84", - "CYM": "114,84", - "COF2TD": "0,000059563056000000", - "PMHPCB": "101,87", - "PMHCYM": "101,87", - "SAHPCB": "4,21", - "SAHCYM": "4,21", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,56", - "CCVCYM": "2,56", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "05-06", - "PCB": "116,03", - "CYM": "116,03", - "COF2TD": "0,000059907686000000", - "PMHPCB": "103,14", - "PMHCYM": "103,14", - "SAHPCB": "4,11", - "SAHCYM": "4,11", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,58", - "CCVCYM": "2,58", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "06-07", - "PCB": "116,29", - "CYM": "116,29", - "COF2TD": "0,000062818713000000", - "PMHPCB": "103,64", - "PMHCYM": "103,64", - "SAHPCB": "3,88", - "SAHCYM": "3,88", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,57", - "CCVCYM": "2,57", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "07-08", - "PCB": "115,70", - "CYM": "115,70", - "COF2TD": "0,000072575564000000", - "PMHPCB": "103,85", - "PMHCYM": "103,85", - "SAHPCB": "3,10", - "SAHCYM": "3,10", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,55", - "CCVCYM": "2,55", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "08-09", - "PCB": "152,89", - "CYM": "152,89", - "COF2TD": "0,000086825264000000", - "PMHPCB": "105,65", - "PMHCYM": "105,65", - "SAHPCB": "2,36", - "SAHCYM": "2,36", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,57", - "CCVCYM": "2,57", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "09-10", - "PCB": "150,83", - "CYM": "150,83", - "COF2TD": "0,000095768317000000", - "PMHPCB": "103,77", - "PMHCYM": "103,77", - "SAHPCB": "2,24", - "SAHCYM": "2,24", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,53", - "CCVCYM": "2,53", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "10-11", - "PCB": "242,62", - "CYM": "149,28", - "COF2TD": "0,000102672431000000", - "PMHPCB": "102,38", - "PMHCYM": "102,11", - "SAHPCB": "2,38", - "SAHCYM": "2,37", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,01", - "PCAPCYM": "0,34", - "TEUPCB": "133,12", - "TEUCYM": "41,77", - "CCVPCB": "2,54", - "CCVCYM": "2,51", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "11-12", - "PCB": "240,50", - "CYM": "240,50", - "COF2TD": "0,000105691470000000", - "PMHPCB": "100,14", - "PMHCYM": "100,14", - "SAHPCB": "2,52", - "SAHCYM": "2,52", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,02", - "PCAPCYM": "2,02", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,51", - "CCVCYM": "2,51", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "12-13", - "PCB": "238,09", - "CYM": "238,09", - "COF2TD": "0,000110462952000000", - "PMHPCB": "97,58", - "PMHCYM": "97,58", - "SAHPCB": "2,71", - "SAHCYM": "2,71", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,02", - "PCAPCYM": "2,02", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,47", - "CCVCYM": "2,47", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "13-14", - "PCB": "235,30", - "CYM": "235,30", - "COF2TD": "0,000119052052000000", - "PMHPCB": "94,65", - "PMHCYM": "94,65", - "SAHPCB": "2,89", - "SAHCYM": "2,89", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,02", - "PCAPCYM": "2,02", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,43", - "CCVCYM": "2,43", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "14-15", - "PCB": "137,96", - "CYM": "231,28", - "COF2TD": "0,000117990009000000", - "PMHPCB": "89,95", - "PMHCYM": "90,19", - "SAHPCB": "3,37", - "SAHCYM": "3,38", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "2,03", - "TEUPCB": "41,77", - "TEUCYM": "133,12", - "CCVPCB": "2,34", - "CCVCYM": "2,37", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "15-16", - "PCB": "132,88", - "CYM": "132,88", - "COF2TD": "0,000108598330000000", - "PMHPCB": "84,43", - "PMHCYM": "84,43", - "SAHPCB": "3,89", - "SAHCYM": "3,89", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,26", - "CCVCYM": "2,26", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "16-17", - "PCB": "131,93", - "CYM": "131,93", - "COF2TD": "0,000104114191000000", - "PMHPCB": "83,66", - "PMHCYM": "83,66", - "SAHPCB": "3,73", - "SAHCYM": "3,73", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,25", - "CCVCYM": "2,25", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "17-18", - "PCB": "135,99", - "CYM": "135,99", - "COF2TD": "0,000105171071000000", - "PMHPCB": "88,07", - "PMHCYM": "88,07", - "SAHPCB": "3,31", - "SAHCYM": "3,31", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,31", - "CCVCYM": "2,31", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "18-19", - "PCB": "231,44", - "CYM": "138,13", - "COF2TD": "0,000106417649000000", - "PMHPCB": "90,57", - "PMHCYM": "90,33", - "SAHPCB": "3,16", - "SAHCYM": "3,15", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,02", - "PCAPCYM": "0,34", - "TEUPCB": "133,12", - "TEUCYM": "41,77", - "CCVPCB": "2,37", - "CCVCYM": "2,34", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "19-20", - "PCB": "240,40", - "CYM": "240,40", - "COF2TD": "0,000108017615000000", - "PMHPCB": "99,53", - "PMHCYM": "99,53", - "SAHPCB": "3,00", - "SAHCYM": "3,00", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,04", - "PCAPCYM": "2,04", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,52", - "CCVCYM": "2,52", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "20-21", - "PCB": "246,20", - "CYM": "246,20", - "COF2TD": "0,000114631042000000", - "PMHPCB": "104,32", - "PMHCYM": "104,32", - "SAHPCB": "3,90", - "SAHCYM": "3,90", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,05", - "PCAPCYM": "2,05", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,61", - "CCVCYM": "2,61", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "21-22", - "PCB": "248,08", - "CYM": "248,08", - "COF2TD": "0,000127585671000000", - "PMHPCB": "107,28", - "PMHCYM": "107,28", - "SAHPCB": "2,78", - "SAHCYM": "2,78", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,06", - "PCAPCYM": "2,06", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,64", - "CCVCYM": "2,64", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "22-23", - "PCB": "155,91", - "CYM": "249,41", - "COF2TD": "0,000130129026000000", - "PMHPCB": "108,02", - "PMHCYM": "108,39", - "SAHPCB": "2,93", - "SAHCYM": "2,94", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,35", - "PCAPCYM": "2,09", - "TEUPCB": "41,77", - "TEUCYM": "133,12", - "CCVPCB": "2,64", - "CCVCYM": "2,67", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "23-24", - "PCB": "156,50", - "CYM": "156,50", - "COF2TD": "0,000110367990000000", - "PMHPCB": "108,02", - "PMHCYM": "108,02", - "SAHPCB": "3,50", - "SAHCYM": "3,50", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,35", - "PCAPCYM": "0,35", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,66", - "CCVCYM": "2,66", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - } - ] -} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json new file mode 100644 index 00000000000..8e65a242447 --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json @@ -0,0 +1,154 @@ +{ + "data": { + "type": "Precios mercado peninsular en tiempo real", + "id": "mer13", + "attributes": { + "title": "Precios mercado peninsular en tiempo real", + "last-update": "2021-05-31T20:19:18.000+02:00", + "description": null + }, + "meta": { + "cache-control": { + "cache": "MISS" + } + } + }, + "included": [ + { + "type": "PVPC (\u20ac\/MWh)", + "id": "1001", + "groupId": null, + "attributes": { + "title": "PVPC (\u20ac\/MWh)", + "description": null, + "color": "#ffcf09", + "type": null, + "magnitude": "price", + "composite": false, + "last-update": "2021-05-31T20:19:18.000+02:00", + "values": [ + { + "value": 116.33, + "percentage": 1, + "datetime": "2021-06-01T00:00:00.000+02:00" + }, + { + "value": 115.95, + "percentage": 1, + "datetime": "2021-06-01T01:00:00.000+02:00" + }, + { + "value": 114.89, + "percentage": 1, + "datetime": "2021-06-01T02:00:00.000+02:00" + }, + { + "value": 114.96, + "percentage": 1, + "datetime": "2021-06-01T03:00:00.000+02:00" + }, + { + "value": 114.84, + "percentage": 1, + "datetime": "2021-06-01T04:00:00.000+02:00" + }, + { + "value": 116.03, + "percentage": 1, + "datetime": "2021-06-01T05:00:00.000+02:00" + }, + { + "value": 116.29, + "percentage": 1, + "datetime": "2021-06-01T06:00:00.000+02:00" + }, + { + "value": 115.7, + "percentage": 1, + "datetime": "2021-06-01T07:00:00.000+02:00" + }, + { + "value": 152.89, + "percentage": 1, + "datetime": "2021-06-01T08:00:00.000+02:00" + }, + { + "value": 150.83, + "percentage": 1, + "datetime": "2021-06-01T09:00:00.000+02:00" + }, + { + "value": 149.28, + "percentage": 1, + "datetime": "2021-06-01T10:00:00.000+02:00" + }, + { + "value": 240.5, + "percentage": 1, + "datetime": "2021-06-01T11:00:00.000+02:00" + }, + { + "value": 238.09, + "percentage": 1, + "datetime": "2021-06-01T12:00:00.000+02:00" + }, + { + "value": 235.3, + "percentage": 1, + "datetime": "2021-06-01T13:00:00.000+02:00" + }, + { + "value": 231.28, + "percentage": 1, + "datetime": "2021-06-01T14:00:00.000+02:00" + }, + { + "value": 132.88, + "percentage": 1, + "datetime": "2021-06-01T15:00:00.000+02:00" + }, + { + "value": 131.93, + "percentage": 1, + "datetime": "2021-06-01T16:00:00.000+02:00" + }, + { + "value": 135.99, + "percentage": 1, + "datetime": "2021-06-01T17:00:00.000+02:00" + }, + { + "value": 138.13, + "percentage": 1, + "datetime": "2021-06-01T18:00:00.000+02:00" + }, + { + "value": 240.4, + "percentage": 1, + "datetime": "2021-06-01T19:00:00.000+02:00" + }, + { + "value": 246.2, + "percentage": 1, + "datetime": "2021-06-01T20:00:00.000+02:00" + }, + { + "value": 248.08, + "percentage": 1, + "datetime": "2021-06-01T21:00:00.000+02:00" + }, + { + "value": 249.41, + "percentage": 1, + "datetime": "2021-06-01T22:00:00.000+02:00" + }, + { + "value": 156.5, + "percentage": 1, + "datetime": "2021-06-01T23:00:00.000+02:00" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 1c9cb7e133d..ba0dc802007 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -90,10 +90,9 @@ async def test_config_flow( await hass.async_block_till_done() state = hass.states.get("sensor.test") check_valid_state(state, tariff=TARIFFS[1]) - price_pbc = state.state assert pvpc_aioclient_mock.call_count == 2 - assert state.attributes["period"] == "P2" - assert state.attributes["next_period"] == "P1" + assert state.attributes["period"] == "P1" + assert state.attributes["next_period"] == "P2" assert state.attributes["available_power"] == 4600 # check options flow @@ -111,10 +110,8 @@ async def test_config_flow( ) await hass.async_block_till_done() state = hass.states.get("sensor.test") - price_cym = state.state check_valid_state(state, tariff=TARIFFS[0]) assert pvpc_aioclient_mock.call_count == 3 assert state.attributes["period"] == "P2" assert state.attributes["next_period"] == "P1" assert state.attributes["available_power"] == 3000 - assert price_cym < price_pbc diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index cee2374f192..727a144e75d 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -11,100 +11,19 @@ from homeassistant.components.pvpc_hourly_pricing import ( TARIFFS, ) from homeassistant.const import CONF_NAME -from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED from homeassistant.util import dt as dt_util from .conftest import check_valid_state -from tests.common import MockConfigEntry, date_util, mock_registry +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + date_util, + mock_registry, +) from tests.test_util.aiohttp import AiohttpClientMocker -async def _process_time_step( - hass, mock_data, key_state=None, value=None, tariff="discrimination", delta_min=60 -): - state = hass.states.get("sensor.test_dst") - check_valid_state(state, tariff=tariff, value=value, key_attr=key_state) - - mock_data["return_time"] += timedelta(minutes=delta_min) - hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: mock_data["return_time"]}) - await hass.async_block_till_done() - return state - - -async def test_sensor_availability( - hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker -): - """Test sensor availability and handling of cloud access.""" - hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") - config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"} - ) - config_entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - mock_data = {"return_time": datetime(2019, 10, 27, 20, 0, 0, tzinfo=date_util.UTC)} - - def mock_now(): - return mock_data["return_time"] - - with patch("homeassistant.util.dt.utcnow", new=mock_now): - assert await hass.config_entries.async_setup(config_entry.entry_id) - - # check migration - current_entries = hass.config_entries.async_entries(DOMAIN) - assert len(current_entries) == 1 - migrated_entry = current_entries[0] - assert migrated_entry.version == 1 - assert migrated_entry.data[ATTR_POWER] == migrated_entry.data[ATTR_POWER_P3] - assert migrated_entry.data[ATTR_TARIFF] == TARIFFS[0] - - await hass.async_block_till_done() - caplog.clear() - assert pvpc_aioclient_mock.call_count == 2 - - await _process_time_step(hass, mock_data, "price_21h", 0.13896) - await _process_time_step(hass, mock_data, "price_22h", 0.06893) - assert pvpc_aioclient_mock.call_count == 4 - await _process_time_step(hass, mock_data, "price_23h", 0.06935) - assert pvpc_aioclient_mock.call_count == 5 - - # sensor has no more prices, state is "unavailable" from now on - await _process_time_step(hass, mock_data, value="unavailable") - await _process_time_step(hass, mock_data, value="unavailable") - num_errors = sum( - 1 - for x in caplog.records - if x.levelno == logging.ERROR and "unknown job listener" not in x.msg - ) - num_warnings = sum(1 for x in caplog.records if x.levelno == logging.WARNING) - assert num_warnings == 1 - assert num_errors == 0 - assert pvpc_aioclient_mock.call_count == 9 - - # check that it is silent until it becomes available again - caplog.clear() - with caplog.at_level(logging.WARNING): - # silent mode - for _ in range(21): - await _process_time_step(hass, mock_data, value="unavailable") - assert pvpc_aioclient_mock.call_count == 30 - assert len(caplog.messages) == 0 - - # warning about data access recovered - await _process_time_step(hass, mock_data, value="unavailable") - assert pvpc_aioclient_mock.call_count == 31 - assert len(caplog.messages) == 1 - assert caplog.records[0].levelno == logging.WARNING - - # working ok again - await _process_time_step(hass, mock_data, "price_00h", value=0.06821) - assert pvpc_aioclient_mock.call_count == 32 - await _process_time_step(hass, mock_data, "price_01h", value=0.06627) - assert pvpc_aioclient_mock.call_count == 33 - assert len(caplog.messages) == 1 - assert caplog.records[0].levelno == logging.WARNING - - async def test_multi_sensor_migration( hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker ): @@ -139,7 +58,7 @@ async def test_multi_sensor_migration( assert len(hass.config_entries.async_entries(DOMAIN)) == 2 assert len(entity_reg.entities) == 2 - mock_data = {"return_time": datetime(2019, 10, 27, 20, tzinfo=date_util.UTC)} + mock_data = {"return_time": datetime(2021, 6, 1, 21, tzinfo=date_util.UTC)} def mock_now(): return mock_data["return_time"] @@ -164,3 +83,15 @@ async def test_multi_sensor_migration( await hass.async_block_till_done() assert pvpc_aioclient_mock.call_count == 2 + + # check state and availability + state = hass.states.get("sensor.test_pvpc_1") + check_valid_state(state, tariff=TARIFFS[0], value=0.1565) + + mock_data["return_time"] += timedelta(minutes=60) + async_fire_time_changed(hass, mock_data["return_time"]) + await list(hass.data[DOMAIN].values())[0].async_refresh() + await hass.async_block_till_done() + state = hass.states.get("sensor.test_pvpc_1") + check_valid_state(state, tariff=TARIFFS[0], value="unavailable") + assert pvpc_aioclient_mock.call_count == 3 From 86c08d80c9748da50527d3af23db953bf7f41248 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Dec 2021 13:18:52 -0700 Subject: [PATCH 250/366] Bump flux_led to 0.27.13 to fix discovery of legacy devices (#62613) - The 2013/2014 devices have yet another format for the version --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0f32d248384..9b55c9f4549 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.12"], + "requirements": ["flux_led==0.27.13"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index a023f60b7a5..0b7d9d75122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.12 +flux_led==0.27.13 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 486dd0c6199..fe210e5e48a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.12 +flux_led==0.27.13 # homeassistant.components.homekit fnvhash==0.1.0 From d1110102c766c26ac7ab4deeb7daf865e8c3b97a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 22 Dec 2021 20:59:56 +0100 Subject: [PATCH 251/366] Fix missing exception handling from upstream lib in Fritz (#62617) * Fix missing exception handling from upstream lib * isort --- homeassistant/components/fritz/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 193e11f49f3..d9a3a5ccee8 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -3,6 +3,7 @@ import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError from fritzconnection.core.logger import fritzlogger +from requests import exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -45,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await fritz_tools.async_start(entry.options) except FritzSecurityError as ex: raise ConfigEntryAuthFailed from ex - except FritzConnectionException as ex: + except (FritzConnectionException, exceptions.ConnectionError) as ex: raise ConfigEntryNotReady from ex hass.data.setdefault(DOMAIN, {}) From 7523044d651391fb4b38a798fbfcf6b0f332c7dd Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Thu, 23 Dec 2021 15:08:24 +0100 Subject: [PATCH 252/366] Round Hue transition to steps of 100ms (#62619) * Adding round() to transition before firing turn_on, turn_off #62608 --- homeassistant/components/hue/scene.py | 8 ++++---- homeassistant/components/hue/v2/group.py | 16 ++++------------ homeassistant/components/hue/v2/helpers.py | 21 +++++++++++++++++++++ homeassistant/components/hue/v2/light.py | 17 +++++------------ tests/components/hue/test_light_v2.py | 18 +++++++++--------- tests/components/hue/test_scene.py | 4 ++-- 6 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/hue/v2/helpers.py diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index d67a3b097c7..90c1bddc970 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -8,6 +8,7 @@ from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.scenes import ScenesController from aiohue.v2.models.scene import Scene as HueScene +from homeassistant.components.light import ATTR_TRANSITION from homeassistant.components.scene import Scene as SceneEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -16,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .bridge import HueBridge from .const import DOMAIN from .v2.entity import HueBaseEntity +from .v2.helpers import normalize_hue_transition async def async_setup_entry( @@ -94,11 +96,9 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): async def async_activate(self, **kwargs: Any) -> None: """Activate Hue scene.""" - transition = kwargs.get("transition") - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) dynamic = kwargs.get("dynamic", self.is_dynamic) + await self.bridge.async_request_call( self.controller.recall, self.resource.id, diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index c5f7ae5d926..add3336764d 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -29,6 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN from .entity import HueBaseEntity +from .helpers import normalize_hue_brightness, normalize_hue_transition ALLOWED_ERRORS = [ "device (groupedLight) has communication issues, command (on) may not have effect", @@ -147,17 +148,11 @@ class GroupedHueLight(HueBaseEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - transition = kwargs.get(ATTR_TRANSITION) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) - brightness = kwargs.get(ATTR_BRIGHTNESS) + brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) - if brightness is not None: - # Hue uses a range of [0, 100] to control brightness. - brightness = float((brightness / 255) * 100) - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights @@ -193,10 +188,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - transition = kwargs.get(ATTR_TRANSITION) - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights diff --git a/homeassistant/components/hue/v2/helpers.py b/homeassistant/components/hue/v2/helpers.py new file mode 100644 index 00000000000..1c26652ce25 --- /dev/null +++ b/homeassistant/components/hue/v2/helpers.py @@ -0,0 +1,21 @@ +"""Helper functions for Philips Hue v2.""" + + +def normalize_hue_brightness(brightness): + """Returns calculated brightness values""" + + if brightness is not None: + # Hue uses a range of [0, 100] to control brightness. + brightness = float((brightness / 255) * 100) + + return brightness + + +def normalize_hue_transition(transition): + """Returns rounded transition values""" + + if transition is not None: + # hue transition duration is in milliseconds and round them to 100ms + transition = int(round(transition, 1) * 1000) + + return transition diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index afb4c3d88bd..d6578c8ef9a 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -30,6 +30,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge from ..const import DOMAIN from .entity import HueBaseEntity +from .helpers import normalize_hue_brightness, normalize_hue_transition ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", @@ -155,17 +156,11 @@ class HueLight(HueBaseEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - transition = kwargs.get(ATTR_TRANSITION) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) - brightness = kwargs.get(ATTR_BRIGHTNESS) + brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) - if brightness is not None: - # Hue uses a range of [0, 100] to control brightness. - brightness = float((brightness / 255) * 100) - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) await self.bridge.async_request_call( self.controller.set_state, @@ -181,11 +176,9 @@ class HueLight(HueBaseEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - transition = kwargs.get(ATTR_TRANSITION) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) flash = kwargs.get(ATTR_FLASH) - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) + await self.bridge.async_request_call( self.controller.set_state, id=self.resource.id, diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 70a5af6d98e..f9277fad528 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -110,16 +110,16 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert test_light.attributes["color_mode"] == COLOR_MODE_COLOR_TEMP assert test_light.attributes["brightness"] == 255 - # test again with sending transition + # test again with sending transition with 250ms which should round up to 200ms await hass.services.async_call( "light", "turn_on", - {"entity_id": test_light_id, "brightness_pct": 50, "transition": 6}, + {"entity_id": test_light_id, "brightness_pct": 50, "transition": 0.25}, blocking=True, ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True - assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200 # test again with sending flash/alert await hass.services.async_call( @@ -170,12 +170,12 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da await hass.services.async_call( "light", "turn_off", - {"entity_id": test_light_id, "transition": 6}, + {"entity_id": test_light_id, "transition": 0.25}, blocking=True, ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False - assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200 async def test_light_added(hass, mock_bridge_v2): @@ -310,7 +310,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): "entity_id": test_light_id, "brightness_pct": 100, "xy_color": (0.123, 0.123), - "transition": 6, + "transition": 0.25, }, blocking=True, ) @@ -325,7 +325,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123 assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123 assert ( - mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000 + mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 ) # Now generate update events by emitting the json we've sent as incoming events @@ -374,7 +374,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): "turn_off", { "entity_id": test_light_id, - "transition": 6, + "transition": 0.25, }, blocking=True, ) @@ -384,5 +384,5 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): for index in range(0, 3): assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False assert ( - mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000 + mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 ) diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 1d270706c99..8982b70fbfb 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -77,13 +77,13 @@ async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat await hass.services.async_call( "scene", "turn_on", - {"entity_id": test_entity_id, "transition": 6}, + {"entity_id": test_entity_id, "transition": 0.25}, blocking=True, ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["recall"] == { "action": "active", - "duration": 6000, + "duration": 200, } From 71bf4ad134ee1081a4de456c2f0749ddcce1ae05 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Dec 2021 23:17:04 -0800 Subject: [PATCH 253/366] Bump aiohue to 3.0.8 (#62651) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index ba2d97f44a5..788d1eb7bed 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.7"], + "requirements": ["aiohue==3.0.8"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 0b7d9d75122..32f8f619bff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.7 +aiohue==3.0.8 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe210e5e48a..4e085e83e48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.7 +aiohue==3.0.8 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 35cc2bf803f8609c2113f727ff57047a8e1a3c81 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 10:14:07 +0100 Subject: [PATCH 254/366] Bump aiohue to 3.0.9 (#62658) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 788d1eb7bed..a7c04ed422d 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.8"], + "requirements": ["aiohue==3.0.9"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 32f8f619bff..d50c511803a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.8 +aiohue==3.0.9 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e085e83e48..23b8da4c44b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.8 +aiohue==3.0.9 # homeassistant.components.apache_kafka aiokafka==0.6.0 From cf528c5234f1527ab3cc06ea99cc529b3923bd9e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 12:18:53 +0100 Subject: [PATCH 255/366] Bump aiohue to 3.0.10 (#62664) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index a7c04ed422d..7a69637fbb1 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.9"], + "requirements": ["aiohue==3.0.10"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index d50c511803a..f19a9aa6e6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.9 +aiohue==3.0.10 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23b8da4c44b..a4bee163175 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.9 +aiohue==3.0.10 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 094623e5281200a0478944e74bb6385bf98d2cf2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 14:04:10 +0100 Subject: [PATCH 256/366] Adjust Hue retry logic to changes in the aiohue library (#62665) --- homeassistant/components/hue/bridge.py | 64 ++++++-------------------- 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5005f858a58..5f51ff29218 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -3,13 +3,12 @@ from __future__ import annotations import asyncio from collections.abc import Callable -from http import HTTPStatus import logging from typing import Any from aiohttp import client_exceptions from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized -from aiohue.errors import AiohueException +from aiohue.errors import AiohueException, BridgeBusy import async_timeout from homeassistant import core @@ -38,9 +37,6 @@ class HueBridge: self.config_entry = config_entry self.hass = hass self.authorized = False - self.parallel_updates_semaphore = asyncio.Semaphore( - 3 if self.api_version == 1 else 10 - ) # Jobs to be executed when API is reset. self.reset_jobs: list[core.CALLBACK_TYPE] = [] self.sensor_manager: SensorManager | None = None @@ -83,6 +79,7 @@ class HueBridge: client_exceptions.ClientOSError, client_exceptions.ServerDisconnectedError, client_exceptions.ContentTypeError, + BridgeBusy, ) as err: raise ConfigEntryNotReady( f"Error connecting to the Hue bridge at {self.host}" @@ -115,50 +112,19 @@ class HueBridge: async def async_request_call( self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs ) -> Any: - """Limit parallel requests to Hue hub. - - The Hue hub can only handle a certain amount of parallel requests, total. - Although we limit our parallel requests, we still will run into issues because - other products are hitting up Hue. - - ClientOSError means hub closed the socket on us. - ContentResponseError means hub raised an error. - Since we don't make bad requests, this is on them. - """ - max_tries = 5 - async with self.parallel_updates_semaphore: - for tries in range(max_tries): - try: - return await task(*args, **kwargs) - except AiohueException as err: - # The new V2 api is a bit more fanatic with throwing errors - # some of which we accept in certain conditions - # handle that here. Note that these errors are strings and do not have - # an identifier or something. - if allowed_errors is not None and str(err) in allowed_errors: - # log only - self.logger.debug( - "Ignored error/warning from Hue API: %s", str(err) - ) - return None - raise err - except ( - client_exceptions.ClientOSError, - client_exceptions.ClientResponseError, - client_exceptions.ServerDisconnectedError, - ) as err: - if tries == max_tries: - self.logger.error("Request failed %s times, giving up", tries) - raise - - # We only retry if it's a server error. So raise on all 4XX errors. - if ( - isinstance(err, client_exceptions.ClientResponseError) - and err.status < HTTPStatus.INTERNAL_SERVER_ERROR - ): - raise - - await asyncio.sleep(HUB_BUSY_SLEEP * tries) + """Send request to the Hue bridge, optionally omitting error(s).""" + try: + return await task(*args, **kwargs) + except AiohueException as err: + # The (new) Hue api can be a bit fanatic with throwing errors + # some of which we accept in certain conditions + # handle that here. Note that these errors are strings and do not have + # an identifier or something. + if allowed_errors is not None and str(err) in allowed_errors: + # log only + self.logger.debug("Ignored error/warning from Hue API: %s", str(err)) + return None + raise err async def async_reset(self) -> bool: """Reset this bridge to default state. From 3076ead727294795e7027ce9b9244c0ed20c9722 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 14:24:37 +0100 Subject: [PATCH 257/366] Fix Hue button events (#62669) --- homeassistant/components/hue/v2/hue_event.py | 24 +++++++++++++++++-- .../components/hue/test_device_trigger_v2.py | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index 86dabc26660..496507aff4d 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType -from aiohue.v2.models.button import Button +from aiohue.v2.models.button import Button, ButtonEvent from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID from homeassistant.core import callback @@ -27,6 +27,11 @@ async def async_setup_hue_events(bridge: "HueBridge"): api: HueBridgeV2 = bridge.api # to satisfy typing conf_entry = bridge.config_entry dev_reg = device_registry.async_get(hass) + last_state = { + x.id: x.button.last_event + for x in api.sensors.button.items + if x.button is not None + } # at this time the `button` resource is the only source of hue events btn_controller = api.sensors.button @@ -35,6 +40,21 @@ async def async_setup_hue_events(bridge: "HueBridge"): def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: """Handle event from Hue devices controller.""" LOGGER.debug("Received button event: %s", hue_resource) + + # guard for missing button object on the resource + if hue_resource.button is None: + return + + cur_event = hue_resource.button.last_event + last_event = last_state.get(hue_resource.id) + # ignore the event if the last_event value is exactly the same + # this may happen if some other metadata of the button resource is adjusted + if cur_event == last_event: + return + if cur_event != ButtonEvent.REPEAT: + # do not store repeat event + last_state[hue_resource.id] = cur_event + hue_device = btn_controller.get_device(hue_resource.id) device = dev_reg.async_get_device({(DOMAIN, hue_device.id)}) @@ -44,7 +64,7 @@ async def async_setup_hue_events(bridge: "HueBridge"): CONF_ID: slugify(f"{hue_device.metadata.name}: Button"), CONF_DEVICE_ID: device.id, # type: ignore CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: hue_resource.button.last_event.value, + CONF_TYPE: cur_event.value, CONF_SUBTYPE: hue_resource.metadata.control_id, } hass.bus.async_fire(ATTR_HUE_EVENT, data) diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index 0641281b9fa..85fe7b66233 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -25,7 +25,7 @@ async def test_hue_event(hass, mock_bridge_v2, v2_resources_test_data): # Emit button update event btn_event = { - "button": {"last_event": "short_release"}, + "button": {"last_event": "initial_press"}, "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", "metadata": {"control_id": 1}, "type": "button", From 8217b3981f6b7dc527f543fd81438ef56d3d24ba Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 15:35:57 +0100 Subject: [PATCH 258/366] Never use availability workaround for certified Hue devices (#62676) --- homeassistant/components/hue/v2/entity.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 7371efff3bb..69b299d574f 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -142,18 +142,21 @@ class HueBaseEntity(Entity): if self._ignore_availability is not None: # already processed return - cur_state = self.resource.on.on - if self._last_state is None: - self._last_state = cur_state - return + if self.device.product_data.certified: + # certified products report their state correctly + self._ignore_availability = False # some (3th party) Hue lights report their connection status incorrectly # causing the zigbee availability to report as disconnected while in fact # it can be controlled. Although this is in fact something the device manufacturer # should fix, we work around it here. If the light is reported unavailable - # by the zigbee connectivity but the state changesm its considered as a + # by the zigbee connectivity but the state changes its considered as a # malfunctioning device and we report it. # while the user should actually fix this issue instead of ignoring it, we # ignore the availability for this light from this point. + cur_state = self.resource.on.on + if self._last_state is None: + self._last_state = cur_state + return if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): if ( self._last_state != cur_state @@ -163,7 +166,7 @@ class HueBaseEntity(Entity): # while it was reported as not connected! self.logger.warning( "Light %s changed state while reported as disconnected. " - "This is an indicator that routing is not working properly for this device. " + "This might be an indicator that routing is not working for this device. " "Home Assistant will ignore availability for this light from now on. " "Device details: %s - %s (%s) fw: %s", self.name, From 65f2db860bcb616e0728f8cd0b92833e41472bfd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 16:41:22 +0100 Subject: [PATCH 259/366] Fix Hue docstring (#62684) --- homeassistant/components/hue/v2/helpers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/v2/helpers.py b/homeassistant/components/hue/v2/helpers.py index 1c26652ce25..307e7c55e03 100644 --- a/homeassistant/components/hue/v2/helpers.py +++ b/homeassistant/components/hue/v2/helpers.py @@ -2,8 +2,7 @@ def normalize_hue_brightness(brightness): - """Returns calculated brightness values""" - + """Return calculated brightness values.""" if brightness is not None: # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) @@ -12,8 +11,7 @@ def normalize_hue_brightness(brightness): def normalize_hue_transition(transition): - """Returns rounded transition values""" - + """Return rounded transition values.""" if transition is not None: # hue transition duration is in milliseconds and round them to 100ms transition = int(round(transition, 1) * 1000) From d1a4e73ffde58e0c9de6169b8f98205052cf4e13 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Dec 2021 17:24:05 +0100 Subject: [PATCH 260/366] Bumped version to 2021.12.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bda65572d4e..814089c45b3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __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) From 1f82fbe0196a2fca9f6698423d22a4493979ba24 Mon Sep 17 00:00:00 2001 From: corneyl Date: Mon, 27 Dec 2021 17:44:45 +0100 Subject: [PATCH 261/366] Fix picnic sensor time unit (#62437) --- homeassistant/components/picnic/const.py | 30 ++++++++++++++------ homeassistant/components/picnic/sensor.py | 9 +++--- tests/components/picnic/test_sensor.py | 34 ++++++++++++++++++----- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index 228983d8189..be2b403b3b8 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -3,11 +3,13 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from typing import Any, Literal from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util DOMAIN = "picnic" @@ -42,7 +44,7 @@ class PicnicRequiredKeysMixin: """Mixin for required keys.""" data_type: Literal["cart_data", "slot_data", "last_order_data"] - value_fn: Callable[[Any], StateType] + value_fn: Callable[[Any], StateType | datetime] @dataclass @@ -73,7 +75,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:calendar-start", entity_registry_enabled_default=True, data_type="slot_data", - value_fn=lambda slot: slot.get("window_start"), + value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_start"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_END, @@ -81,7 +83,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:calendar-end", entity_registry_enabled_default=True, data_type="slot_data", - value_fn=lambda slot: slot.get("window_end"), + value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_end"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, @@ -89,7 +91,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-alert-outline", entity_registry_enabled_default=True, data_type="slot_data", - value_fn=lambda slot: slot.get("cut_off_time"), + value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("cut_off_time"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, @@ -108,14 +110,18 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_TIMESTAMP, icon="mdi:calendar-start", data_type="last_order_data", - value_fn=lambda last_order: last_order.get("slot", {}).get("window_start"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("slot", {}).get("window_start")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_SLOT_END, device_class=DEVICE_CLASS_TIMESTAMP, icon="mdi:calendar-end", data_type="last_order_data", - value_fn=lambda last_order: last_order.get("slot", {}).get("window_end"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("slot", {}).get("window_end")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_STATUS, @@ -129,7 +135,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-start", entity_registry_enabled_default=True, data_type="last_order_data", - value_fn=lambda last_order: last_order.get("eta", {}).get("start"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("eta", {}).get("start")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_ETA_END, @@ -137,7 +145,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-end", entity_registry_enabled_default=True, data_type="last_order_data", - value_fn=lambda last_order: last_order.get("eta", {}).get("end"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("eta", {}).get("end")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_DELIVERY_TIME, @@ -145,7 +155,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:timeline-clock", entity_registry_enabled_default=True, data_type="last_order_data", - value_fn=lambda last_order: last_order.get("delivery_time", {}).get("start"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("delivery_time", {}).get("start")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_TOTAL_PRICE, diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 95612e7b272..8e64283c5f0 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -1,6 +1,7 @@ """Definition of Picnic sensors.""" from __future__ import annotations +from datetime import datetime from typing import Any, cast from homeassistant.components.sensor import SensorEntity @@ -62,8 +63,8 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): self._attr_unique_id = f"{config_entry.unique_id}.{description.key}" @property - def native_value(self) -> StateType: - """Return the state of the entity.""" + def native_value(self) -> StateType | datetime: + """Return the value reported by the sensor.""" data_set = ( self.coordinator.data.get(self.entity_description.data_type, {}) if self.coordinator.data is not None @@ -73,8 +74,8 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): @property def available(self) -> bool: - """Return True if entity is available.""" - return self.coordinator.last_update_success and self.state is not None + """Return True if last update was successful.""" + return self.coordinator.last_update_success @property def device_info(self) -> DeviceInfo: diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index b0d12f1f080..4067d7e101f 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the Picnic sensor platform.""" import copy from datetime import timedelta +from typing import Dict import unittest from unittest.mock import patch @@ -15,6 +16,7 @@ from homeassistant.const import ( CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.util import dt @@ -103,6 +105,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): # Patch the api client self.picnic_patcher = patch("homeassistant.components.picnic.PicnicAPI") self.picnic_mock = self.picnic_patcher.start() + self.picnic_mock().session.auth_token = "3q29fpwhulzes" # Add a config entry and setup the integration config_data = { @@ -281,13 +284,11 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): await self._setup_platform() # Assert sensors are unknown - self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNAVAILABLE) - self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_selected_slot_max_order_time", STATE_UNKNOWN) self._assert_sensor( - "sensor.picnic_selected_slot_max_order_time", STATE_UNAVAILABLE - ) - self._assert_sensor( - "sensor.picnic_selected_slot_min_order_value", STATE_UNAVAILABLE + "sensor.picnic_selected_slot_min_order_value", STATE_UNKNOWN ) async def test_sensors_last_order_in_future(self): @@ -304,7 +305,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): await self._setup_platform() # Assert delivery time is not available, but eta is - self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN) self._assert_sensor( "sensor.picnic_last_order_eta_start", "2021-02-26T19:54:00+00:00" ) @@ -312,6 +313,25 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): "sensor.picnic_last_order_eta_end", "2021-02-26T20:14:00+00:00" ) + async def test_sensors_eta_date_malformed(self): + """Test sensor states when last order eta dates are malformed.""" + # Set-up platform with default mock responses + await self._setup_platform(use_default_responses=True) + + # Set non-datetime strings as eta + eta_dates: Dict[str, str] = { + "start": "wrong-time", + "end": "other-malformed-datetime", + } + delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) + delivery_response["eta2"] = eta_dates + self.picnic_mock().get_deliveries.return_value = [delivery_response] + await self._coordinator.async_refresh() + + # Assert eta times are not available due to malformed date strings + self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNKNOWN) + async def test_sensors_use_detailed_eta_if_available(self): """Test sensor states when last order is not yet delivered.""" # Set-up platform with default mock responses From 3de9c425f636627caf3ff423bf0e5df8a522cde0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 23 Dec 2021 14:07:29 -0500 Subject: [PATCH 262/366] Bump soco to 0.25.2 (#62691) --- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sonos/speaker.py | 4 ++-- homeassistant/components/sonos/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sonos/conftest.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 00006ab4e90..d4fce01fd78 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.25.1"], + "requirements": ["soco==0.25.2"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 8cd2abcf2b2..ffc0d08a1b4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -482,7 +482,7 @@ class SonosSpeaker: for bool_var in ( "dialog_level", - "night_mode", + "night_level", "sub_enabled", "surround_enabled", ): @@ -965,7 +965,7 @@ class SonosSpeaker: self.volume = self.soco.volume self.muted = self.soco.mute self.night_mode = self.soco.night_mode - self.dialog_level = self.soco.dialog_mode + self.dialog_level = self.soco.dialog_level self.bass = self.soco.bass self.treble = self.soco.treble diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index e92263991ab..ad98523258a 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -36,7 +36,7 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_CROSSFADE = "cross_fade" ATTR_NIGHT_SOUND = "night_mode" -ATTR_SPEECH_ENHANCEMENT = "dialog_mode" +ATTR_SPEECH_ENHANCEMENT = "dialog_level" ATTR_STATUS_LIGHT = "status_light" ATTR_SUB_ENABLED = "sub_enabled" ATTR_SURROUND_ENABLED = "surround_enabled" diff --git a/requirements_all.txt b/requirements_all.txt index f19a9aa6e6c..6fd2af8cc74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2185,7 +2185,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.25.1 +soco==0.25.2 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4bee163175..97e821bb4ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1291,7 +1291,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.25.1 +soco==0.25.2 # homeassistant.components.solaredge solaredge==0.0.2 diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f7f8d67589f..18c5366d4ae 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -68,7 +68,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True - mock_soco.dialog_mode = True + mock_soco.dialog_level = True mock_soco.volume = 19 mock_soco.bass = 1 mock_soco.treble = -1 From c4fe1d57d91c4ff6f93b86dd2dcf7305cd2de850 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Dec 2021 02:26:07 -1000 Subject: [PATCH 263/366] Bump zeroconf to 0.38.1 (#62720) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 338456ca576..16a8a8ff26e 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.37.0"], + "requirements": ["zeroconf==0.38.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5ea2198a3aa..c17acc7e436 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.27 voluptuous-serialize==2.5.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.37.0 +zeroconf==0.38.1 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 6fd2af8cc74..cef7ed280b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2484,7 +2484,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.37.0 +zeroconf==0.38.1 # homeassistant.components.zha zha-quirks==0.0.65 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97e821bb4ef..c43f4c246a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1473,7 +1473,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.37.0 +zeroconf==0.38.1 # homeassistant.components.zha zha-quirks==0.0.65 From fb26398ea7f1f2e5f70b9b01379fe8cc871fe6e1 Mon Sep 17 00:00:00 2001 From: htmltiger <1429451+htmltiger@users.noreply.github.com> Date: Mon, 27 Dec 2021 19:55:43 +0000 Subject: [PATCH 264/366] Fix TypeError of vacuum battery level None (#62722) Co-authored-by: Franck Nijhof --- homeassistant/components/google_assistant/trait.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 30ea244bac9..843ebfee87a 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -641,6 +641,8 @@ class EnergyStorageTrait(_Trait): def query_attributes(self): """Return EnergyStorage query attributes.""" battery_level = self.state.attributes.get(ATTR_BATTERY_LEVEL) + if battery_level is None: + return {} if battery_level == 100: descriptive_capacity_remaining = "FULL" elif 75 <= battery_level < 100: From 117fb8ef4c72e26b702ee7c2b1f3bff22b1e6471 Mon Sep 17 00:00:00 2001 From: flfue Date: Mon, 27 Dec 2021 08:31:51 +0100 Subject: [PATCH 265/366] Add return for certified devices to not apply availability check (#62728) --- homeassistant/components/hue/v2/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 69b299d574f..8253d4ffbef 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -145,6 +145,7 @@ class HueBaseEntity(Entity): if self.device.product_data.certified: # certified products report their state correctly self._ignore_availability = False + return # some (3th party) Hue lights report their connection status incorrectly # causing the zigbee availability to report as disconnected while in fact # it can be controlled. Although this is in fact something the device manufacturer From e8bb0eceed2d141d31c53658310094cfd0124910 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 24 Dec 2021 21:34:49 +0100 Subject: [PATCH 266/366] Bump roombapy to 1.6.4 (#62741) --- homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 907026fd77e..ad5857aa630 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "iRobot Roomba and Braava", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.6.4"], + "requirements": ["roombapy==1.6.5"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index cef7ed280b7..e0113d5d593 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2074,7 +2074,7 @@ rocketchat-API==0.6.1 rokuecp==0.8.4 # homeassistant.components.roomba -roombapy==1.6.4 +roombapy==1.6.5 # homeassistant.components.roon roonapi==0.0.38 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c43f4c246a5..37af6f3d26d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1236,7 +1236,7 @@ ring_doorbell==0.7.2 rokuecp==0.8.4 # homeassistant.components.roomba -roombapy==1.6.4 +roombapy==1.6.5 # homeassistant.components.roon roonapi==0.0.38 From 2e10432075f36f696b5e580a1a33be8fcc01f265 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 26 Dec 2021 21:26:24 +0100 Subject: [PATCH 267/366] Add missing entity category for gen2 devices (#62812) --- homeassistant/components/shelly/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 7fcf456b658..51cb462fa48 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -283,6 +283,7 @@ RPC_SENSORS: Final = { device_class=sensor.DEVICE_CLASS_TEMPERATURE, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, + entity_category=EntityCategory.DIAGNOSTIC, ), "rssi": RpcAttributeDescription( key="wifi", From 34ca39855e41a12bf27470067e599f165226eccd Mon Sep 17 00:00:00 2001 From: gjong Date: Mon, 27 Dec 2021 12:32:25 +0100 Subject: [PATCH 268/366] Upgrade youless library to version 0.16 (#62837) --- homeassistant/components/youless/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index 49a5b6187e6..f5713c51680 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -3,7 +3,7 @@ "name": "YouLess", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/youless", - "requirements": ["youless-api==0.15"], + "requirements": ["youless-api==0.16"], "codeowners": ["@gjong"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index e0113d5d593..4b456a35796 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2475,7 +2475,7 @@ yeelight==0.7.8 yeelightsunflower==0.0.10 # homeassistant.components.youless -youless-api==0.15 +youless-api==0.16 # homeassistant.components.media_extractor youtube_dl==2021.06.06 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37af6f3d26d..594a779e833 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1470,7 +1470,7 @@ yalexs==1.1.13 yeelight==0.7.8 # homeassistant.components.youless -youless-api==0.15 +youless-api==0.16 # homeassistant.components.zeroconf zeroconf==0.38.1 From bfe4f52c2deec3086d9006cd9d7aaa3a59fe7782 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 20:58:33 +0100 Subject: [PATCH 269/366] Update tuya-iot-py-sdk to 0.6.6 (#62858) --- homeassistant/components/tuya/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index c48771b85be..1b8772a36df 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,7 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuya-iot-py-sdk==0.6.3"], + "requirements": ["tuya-iot-py-sdk==0.6.6"], "dependencies": ["ffmpeg"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 4b456a35796..a6c47bb1fdc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2339,7 +2339,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuya-iot-py-sdk==0.6.3 +tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu twentemilieu==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 594a779e833..a0d5c7bf183 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1376,7 +1376,7 @@ total_connect_client==2021.12 transmissionrpc==0.11 # homeassistant.components.tuya -tuya-iot-py-sdk==0.6.3 +tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu twentemilieu==0.5.0 From 315864acbc8058688aae7b0578df86bc25569511 Mon Sep 17 00:00:00 2001 From: corneyl Date: Mon, 27 Dec 2021 20:31:35 +0100 Subject: [PATCH 270/366] Fix keyerror when no previous Picnic orders exist (#62870) --- homeassistant/components/picnic/coordinator.py | 13 +++++++------ tests/components/picnic/test_sensor.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index bcd4e79a098..71a6559975c 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -60,11 +60,11 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): """Fetch the data from the Picnic API and return a flat dict with only needed sensor data.""" # Fetch from the API and pre-process the data cart = self.picnic_api_client.get_cart() - last_order = self._get_last_order() - if not cart or not last_order: + if not cart: raise UpdateFailed("API response doesn't contain expected data.") + last_order = self._get_last_order() slot_data = self._get_slot_data(cart) return { @@ -102,11 +102,12 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): """Get data of the last order from the list of deliveries.""" # Get the deliveries deliveries = self.picnic_api_client.get_deliveries(summary=True) - if not deliveries: - return {} - # Determine the last order - last_order = copy.deepcopy(deliveries[0]) + # Determine the last order and return an empty dict if there is none + try: + last_order = copy.deepcopy(deliveries[0]) + except KeyError: + return {} # Get the position details if the order is not delivered yet delivery_position = {} diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 4067d7e101f..83653945d8c 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -387,6 +387,21 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNAVAILABLE) self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) + async def test_sensors_malformed_delivery_data(self): + """Test sensor states when the delivery api returns not a list.""" + # Setup platform with default responses + await self._setup_platform(use_default_responses=True) + + # Change mock responses to empty data and refresh the coordinator + self.picnic_mock().get_deliveries.return_value = {"error": "message"} + await self._coordinator.async_refresh() + + # Assert all last-order sensors have STATE_UNAVAILABLE because the delivery info fetch failed + assert self._coordinator.last_update_success is True + self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN) + async def test_sensors_malformed_response(self): """Test coordinator update fails when API yields ValueError.""" # Setup platform with default responses From 12fefe4c31201556a331e5390839439e9a9a1e92 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 27 Dec 2021 21:37:21 +0100 Subject: [PATCH 271/366] Update frontend to 20211227.0 (#62874) --- 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 6ae1709e418..a2c7e49f6e6 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==20211220.0" + "home-assistant-frontend==20211227.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c17acc7e436..19b764ee32f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211220.0 +home-assistant-frontend==20211227.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index a6c47bb1fdc..41318a26a28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -820,7 +820,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211220.0 +home-assistant-frontend==20211227.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0d5c7bf183..f2e191885c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211220.0 +home-assistant-frontend==20211227.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3af82b551c3a1815b48613b90a2b480ca5045328 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 21:48:25 +0100 Subject: [PATCH 272/366] Bumped version to 2021.12.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 814089c45b3..f20d78f9224 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __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) From e54e35367649ff875934795ed1cac3c73d044981 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 22:33:56 +0100 Subject: [PATCH 273/366] Temporary Shelly hotfix for 2021.12.6 patch release (#62885) --- homeassistant/components/shelly/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 51cb462fa48..bd9041d3344 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -283,7 +283,7 @@ RPC_SENSORS: Final = { device_class=sensor.DEVICE_CLASS_TEMPERATURE, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, - entity_category=EntityCategory.DIAGNOSTIC, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "rssi": RpcAttributeDescription( key="wifi", From d62d013fb6f0121474bfdb576f8bc4c807e3cdf5 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Mon, 27 Dec 2021 23:02:48 +0100 Subject: [PATCH 274/366] Fix unique_id of nuki config entry (#62840) * fix(nuki): fixed naming of nuki integration * parse_id function * migration path * fixes from ci runs * don't update title if it was changed * move to dedicated helper * use dict of params * Update homeassistant/components/nuki/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/nuki/__init__.py | 9 +++++++++ homeassistant/components/nuki/config_flow.py | 14 ++++++++------ homeassistant/components/nuki/helpers.py | 6 ++++++ tests/components/nuki/mock.py | 3 ++- tests/components/nuki/test_config_flow.py | 4 ++-- 5 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/nuki/helpers.py diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 99def8d4117..09f36661c5d 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -27,6 +27,7 @@ from .const import ( DOMAIN, ERROR_STATES, ) +from .helpers import parse_id _LOGGER = logging.getLogger(__name__) @@ -80,6 +81,14 @@ async def async_setup_entry(hass, entry): hass.data.setdefault(DOMAIN, {}) + # Migration of entry unique_id + if isinstance(entry.unique_id, int): + new_id = parse_id(entry.unique_id) + params = {"unique_id": new_id} + if entry.title == entry.unique_id: + params["title"] = new_id + hass.config_entries.async_update_entry(entry, **params) + try: bridge = await hass.async_add_executor_job( NukiBridge, diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index bd2c5a0d750..54796d53890 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN +from .helpers import parse_id _LOGGER = logging.getLogger(__name__) @@ -69,7 +70,7 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Prepare configuration for a DHCP discovered Nuki bridge.""" - await self.async_set_unique_id(int(discovery_info.hostname[12:], 16)) + await self.async_set_unique_id(discovery_info.hostname[12:].upper()) self._abort_if_unique_id_configured() @@ -114,7 +115,9 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if not errors: - existing_entry = await self.async_set_unique_id(info["ids"]["hardwareId"]) + existing_entry = await self.async_set_unique_id( + parse_id(info["ids"]["hardwareId"]) + ) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=conf) self.hass.async_create_task( @@ -143,11 +146,10 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if "base" not in errors: - await self.async_set_unique_id(info["ids"]["hardwareId"]) + bridge_id = parse_id(info["ids"]["hardwareId"]) + await self.async_set_unique_id(bridge_id) self._abort_if_unique_id_configured() - return self.async_create_entry( - title=info["ids"]["hardwareId"], data=user_input - ) + return self.async_create_entry(title=bridge_id, data=user_input) data_schema = self.discovery_schema or USER_SCHEMA return self.async_show_form( diff --git a/homeassistant/components/nuki/helpers.py b/homeassistant/components/nuki/helpers.py new file mode 100644 index 00000000000..3deedf9d8db --- /dev/null +++ b/homeassistant/components/nuki/helpers.py @@ -0,0 +1,6 @@ +"""nuki integration helpers.""" + + +def parse_id(hardware_id): + """Parse Nuki ID.""" + return hex(hardware_id).split("x")[-1].upper() diff --git a/tests/components/nuki/mock.py b/tests/components/nuki/mock.py index 30315915a73..e85d1de3933 100644 --- a/tests/components/nuki/mock.py +++ b/tests/components/nuki/mock.py @@ -7,6 +7,7 @@ HOST = "1.1.1.1" MAC = "01:23:45:67:89:ab" HW_ID = 123456789 +ID_HEX = "75BCD15" MOCK_INFO = {"ids": {"hardwareId": HW_ID}} @@ -16,7 +17,7 @@ async def setup_nuki_integration(hass): entry = MockConfigEntry( domain="nuki", - unique_id=HW_ID, + unique_id=ID_HEX, data={"host": HOST, "port": 8080, "token": "test-token"}, ) entry.add_to_hass(hass) diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index fd7bfa2137b..46f4a4b6e6b 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -41,7 +41,7 @@ async def test_form(hass): await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == 123456789 + assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", "port": 8080, @@ -204,7 +204,7 @@ async def test_dhcp_flow(hass): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == 123456789 + assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", "port": 8080, From e3b1ab9cc4e25f5d7c45c4e818d269d5e0d23c55 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Tue, 28 Dec 2021 16:26:53 -0800 Subject: [PATCH 275/366] Bump python-smarttub dependency to 0.0.29 (#62968) * Bump python-smarttub dependency to 0.0.29 * gen --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 5972d755e11..7d9a963b26c 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.28"], + "requirements": ["python-smarttub==0.0.29"], "quality_scale": "platinum", "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 41318a26a28..020e09769b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1929,7 +1929,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.28 +python-smarttub==0.0.29 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2e191885c9..6129743b932 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1157,7 +1157,7 @@ python-openzwave-mqtt[mqtt-client]==1.4.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.28 +python-smarttub==0.0.29 # homeassistant.components.songpal python-songpal==0.12 From f612f7e7a9b58cbabbc0fb7680c9690143aaa21b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Dec 2021 16:22:53 -1000 Subject: [PATCH 276/366] Bump flux_led to 0.27.21 (#62971) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 9b55c9f4549..9f1eaa5d4a4 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.13"], + "requirements": ["flux_led==0.27.21"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 020e09769b9..f2362e9a334 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.13 +flux_led==0.27.21 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6129743b932..eefe9b24547 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.13 +flux_led==0.27.21 # homeassistant.components.homekit fnvhash==0.1.0 From 28278862fd42e7ba7319d5fda9c3bf27c9775075 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 29 Dec 2021 07:20:20 +0100 Subject: [PATCH 277/366] Send commands to Hue grouped lights all at once (#62973) --- homeassistant/components/hue/v2/group.py | 49 ++++++++++++++---------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index add3336764d..7e0e50c3d61 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -1,6 +1,7 @@ """Support for Hue groups (room/zone).""" from __future__ import annotations +import asyncio from typing import Any from aiohue.v2 import HueBridgeV2 @@ -173,18 +174,22 @@ class GroupedHueLight(HueBaseEntity, LightEntity): # redirect all other feature commands to underlying lights # note that this silently ignores params sent to light that are not supported - for light in self.controller.get_lights(self.resource.id): - await self.bridge.async_request_call( - self.api.lights.set_state, - light.id, - on=True, - brightness=brightness if light.supports_dimming else None, - color_xy=xy_color if light.supports_color else None, - color_temp=color_temp if light.supports_color_temperature else None, - transition_time=transition, - alert=AlertEffectType.BREATHE if flash is not None else None, - allowed_errors=ALLOWED_ERRORS, - ) + await asyncio.gather( + *[ + self.bridge.async_request_call( + self.api.lights.set_state, + light.id, + on=True, + brightness=brightness if light.supports_dimming else None, + color_xy=xy_color if light.supports_color else None, + color_temp=color_temp if light.supports_color_temperature else None, + transition_time=transition, + alert=AlertEffectType.BREATHE if flash is not None else None, + allowed_errors=ALLOWED_ERRORS, + ) + for light in self.controller.get_lights(self.resource.id) + ] + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" @@ -202,14 +207,18 @@ class GroupedHueLight(HueBaseEntity, LightEntity): return # redirect all other feature commands to underlying lights - for light in self.controller.get_lights(self.resource.id): - await self.bridge.async_request_call( - self.api.lights.set_state, - light.id, - on=False, - transition_time=transition, - allowed_errors=ALLOWED_ERRORS, - ) + await asyncio.gather( + *[ + self.bridge.async_request_call( + self.api.lights.set_state, + light.id, + on=False, + transition_time=transition, + allowed_errors=ALLOWED_ERRORS, + ) + for light in self.controller.get_lights(self.resource.id) + ] + ) @callback def on_update(self) -> None: From 1e6a1a241d0924dfc1cbe5e47d871d84a3e42e23 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 29 Dec 2021 07:21:23 +0100 Subject: [PATCH 278/366] Remove duplicate filter for Hue button events (#62974) --- homeassistant/components/hue/v2/hue_event.py | 21 +++----------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index 496507aff4d..1d45293012c 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType -from aiohue.v2.models.button import Button, ButtonEvent +from aiohue.v2.models.button import Button from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID from homeassistant.core import callback @@ -27,11 +27,6 @@ async def async_setup_hue_events(bridge: "HueBridge"): api: HueBridgeV2 = bridge.api # to satisfy typing conf_entry = bridge.config_entry dev_reg = device_registry.async_get(hass) - last_state = { - x.id: x.button.last_event - for x in api.sensors.button.items - if x.button is not None - } # at this time the `button` resource is the only source of hue events btn_controller = api.sensors.button @@ -45,26 +40,16 @@ async def async_setup_hue_events(bridge: "HueBridge"): if hue_resource.button is None: return - cur_event = hue_resource.button.last_event - last_event = last_state.get(hue_resource.id) - # ignore the event if the last_event value is exactly the same - # this may happen if some other metadata of the button resource is adjusted - if cur_event == last_event: - return - if cur_event != ButtonEvent.REPEAT: - # do not store repeat event - last_state[hue_resource.id] = cur_event - hue_device = btn_controller.get_device(hue_resource.id) device = dev_reg.async_get_device({(DOMAIN, hue_device.id)}) # Fire event data = { # send slugified entity name as id = backwards compatibility with previous version - CONF_ID: slugify(f"{hue_device.metadata.name}: Button"), + CONF_ID: slugify(f"{hue_device.metadata.name} Button"), CONF_DEVICE_ID: device.id, # type: ignore CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: cur_event.value, + CONF_TYPE: hue_resource.button.last_event.value, CONF_SUBTYPE: hue_resource.metadata.control_id, } hass.bus.async_fire(ATTR_HUE_EVENT, data) From fc2bcd964c3d80fdd0135f75421c58c0358b10df Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 29 Dec 2021 12:15:52 +0100 Subject: [PATCH 279/366] Update frontend to 20211229.0 (#62981) --- 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 a2c7e49f6e6..c1d833ac169 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==20211227.0" + "home-assistant-frontend==20211229.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19b764ee32f..e90fdf9db12 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211227.0 +home-assistant-frontend==20211229.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index f2362e9a334..507d85dffe9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -820,7 +820,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211227.0 +home-assistant-frontend==20211229.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eefe9b24547..82a246d38cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211227.0 +home-assistant-frontend==20211229.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 7b797a2136e1191d2062aca9408b781a6c8360cb Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 29 Dec 2021 12:01:57 +0100 Subject: [PATCH 280/366] Bump aiohue to 3.0.11 (#62983) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 7a69637fbb1..832592f3f1b 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.10"], + "requirements": ["aiohue==3.0.11"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 507d85dffe9..47d2b09b186 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.10 +aiohue==3.0.11 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82a246d38cd..003ece83507 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.10 +aiohue==3.0.11 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 016e13154570d4d15ac196016b82bee464547497 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Dec 2021 14:52:08 +0100 Subject: [PATCH 281/366] Gracefully handle unknown HVAC mode in Tuya (#62984) Co-authored-by: Martin Hjelmare --- homeassistant/components/tuya/climate.py | 24 +++++++++++++++++++++--- homeassistant/components/tuya/const.py | 2 ++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index cb70fc5515a..83d635701db 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -32,7 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import EnumTypeData, IntegerTypeData, TuyaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode +from .const import DOMAIN, LOGGER, TUYA_DISCOVERY_NEW, DPCode TUYA_HVAC_TO_HA = { "auto": HVAC_MODE_HEAT_COOL, @@ -298,6 +298,21 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if DPCode.SWITCH_VERTICAL in device.function: self._attr_swing_modes.append(SWING_VERTICAL) + async def async_added_to_hass(self) -> None: + """Call when entity is added to hass.""" + await super().async_added_to_hass() + + # Log unknown modes + if DPCode.MODE in self.device.function: + data_type = EnumTypeData.from_json(self.device.function[DPCode.MODE].values) + for tuya_mode in data_type.range: + if tuya_mode not in TUYA_HVAC_TO_HA: + LOGGER.warning( + "Unknown HVAC mode '%s' for device %s; assuming it as off", + tuya_mode, + self.device.name, + ) + def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" commands = [{"code": DPCode.SWITCH, "value": hvac_mode != HVAC_MODE_OFF}] @@ -436,8 +451,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): return self.entity_description.switch_only_hvac_mode return HVAC_MODE_OFF - if self.device.status.get(DPCode.MODE) is not None: - return TUYA_HVAC_TO_HA[self.device.status[DPCode.MODE]] + if ( + mode := self.device.status.get(DPCode.MODE) + ) is not None and mode in TUYA_HVAC_TO_HA: + return TUYA_HVAC_TO_HA[mode] + return HVAC_MODE_OFF @property diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index fa69b76695c..02f8a8d356d 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass, field from enum import Enum +import logging from tuya_iot import TuyaCloudOpenAPIEndpoint @@ -38,6 +39,7 @@ from homeassistant.const import ( ) DOMAIN = "tuya" +LOGGER = logging.getLogger(__package__) CONF_AUTH_TYPE = "auth_type" CONF_PROJECT_TYPE = "tuya_project_type" From f7e5f1cc23cbdda08037fc20cae08220c4af4fd8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 29 Dec 2021 14:21:38 +0100 Subject: [PATCH 282/366] Fix short flash effect in Hue integration (#62988) --- homeassistant/components/hue/v2/group.py | 26 ++++++- homeassistant/components/hue/v2/light.py | 31 ++++++-- tests/components/hue/test_light_v2.py | 95 +++++++++++++++++++++++- 3 files changed, 144 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 7e0e50c3d61..52c1bc6117a 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + FLASH_SHORT, SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, @@ -37,7 +38,6 @@ ALLOWED_ERRORS = [ 'device (groupedLight) is "soft off", command (on) may not have effect', "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', - "attribute (supportedAlertActions) cannot be written", ] @@ -155,6 +155,11 @@ class GroupedHueLight(HueBaseEntity, LightEntity): brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) + if flash is not None: + await self.async_set_flash(flash) + # flash can not be sent with other commands at the same time + return + # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights if ( @@ -194,6 +199,12 @@ class GroupedHueLight(HueBaseEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) + flash = kwargs.get(ATTR_FLASH) + + if flash is not None: + await self.async_set_flash(flash) + # flash can not be sent with other commands at the same time + return # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights @@ -220,6 +231,19 @@ class GroupedHueLight(HueBaseEntity, LightEntity): ] ) + async def async_set_flash(self, flash: str) -> None: + """Send flash command to light.""" + await asyncio.gather( + *[ + self.bridge.async_request_call( + self.api.lights.set_flash, + id=light.id, + short=flash == FLASH_SHORT, + ) + for light in self.controller.get_lights(self.resource.id) + ] + ) + @callback def on_update(self) -> None: """Call on update event.""" diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index d6578c8ef9a..91afbe53e45 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -6,7 +6,6 @@ from typing import Any from aiohue import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.lights import LightsController -from aiohue.v2.models.feature import AlertEffectType from aiohue.v2.models.light import Light from homeassistant.components.light import ( @@ -19,6 +18,7 @@ from homeassistant.components.light import ( COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + FLASH_SHORT, SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, @@ -35,7 +35,6 @@ from .helpers import normalize_hue_brightness, normalize_hue_transition ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', - "attribute (supportedAlertActions) cannot be written", ] @@ -73,7 +72,8 @@ class HueLight(HueBaseEntity, LightEntity): ) -> None: """Initialize the light.""" super().__init__(bridge, controller, resource) - self._attr_supported_features |= SUPPORT_FLASH + if self.resource.alert and self.resource.alert.action_values: + self._attr_supported_features |= SUPPORT_FLASH self.resource = resource self.controller = controller self._supported_color_modes = set() @@ -162,6 +162,14 @@ class HueLight(HueBaseEntity, LightEntity): brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) + if flash is not None: + await self.async_set_flash(flash) + # flash can not be sent with other commands at the same time or result will be flaky + # Hue's default behavior is that a light returns to its previous state for short + # flash (identify) and the light is kept turned on for long flash (breathe effect) + # Why is this flash alert/effect hidden in the turn_on/off commands ? + return + await self.bridge.async_request_call( self.controller.set_state, id=self.resource.id, @@ -170,7 +178,6 @@ class HueLight(HueBaseEntity, LightEntity): color_xy=xy_color, color_temp=color_temp, transition_time=transition, - alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) @@ -179,11 +186,25 @@ class HueLight(HueBaseEntity, LightEntity): transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) flash = kwargs.get(ATTR_FLASH) + if flash is not None: + await self.async_set_flash(flash) + # flash can not be sent with other commands at the same time or result will be flaky + # Hue's default behavior is that a light returns to its previous state for short + # flash (identify) and the light is kept turned on for long flash (breathe effect) + return + await self.bridge.async_request_call( self.controller.set_state, id=self.resource.id, on=False, transition_time=transition, - alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) + + async def async_set_flash(self, flash: str) -> None: + """Send flash command to light.""" + await self.bridge.async_request_call( + self.controller.set_flash, + id=self.resource.id, + short=flash == FLASH_SHORT, + ) diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index f9277fad528..6bb084e6d06 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -121,7 +121,7 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200 - # test again with sending flash/alert + # test again with sending long flash await hass.services.async_call( "light", "turn_on", @@ -129,9 +129,18 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat blocking=True, ) assert len(mock_bridge_v2.mock_requests) == 3 - assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe" + # test again with sending short flash + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "flash": "short"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 4 + assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify" + async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): """Test calling the turn off service on a light.""" @@ -177,6 +186,26 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200 + # test again with sending long flash + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": test_light_id, "flash": "long"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 3 + assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe" + + # test again with sending short flash + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": test_light_id, "flash": "short"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 4 + assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify" + async def test_light_added(hass, mock_bridge_v2): """Test new light added to bridge.""" @@ -386,3 +415,65 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert ( mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 ) + + # Test sending short flash effect to a grouped light + mock_bridge_v2.mock_requests.clear() + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": test_light_id, + "flash": "short", + }, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert ( + mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"] + == "identify" + ) + + # Test sending long flash effect to a grouped light + mock_bridge_v2.mock_requests.clear() + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": test_light_id, + "flash": "long", + }, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert ( + mock_bridge_v2.mock_requests[index]["json"]["alert"]["action"] == "breathe" + ) + + # Test sending flash effect in turn_off call + mock_bridge_v2.mock_requests.clear() + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_off", + { + "entity_id": test_light_id, + "flash": "short", + }, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert ( + mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"] + == "identify" + ) From edea83f0039d166a3100e7ee697a86b242a13fdc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Dec 2021 15:35:51 +0100 Subject: [PATCH 283/366] Fix incorrect unit of measurement access in Tuya (#62989) --- homeassistant/components/tuya/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index d6870d4b9bb..07393b636e8 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -727,15 +727,15 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): # We cannot have a device class, if the UOM isn't set or the # device class cannot be found in the validation mapping. if ( - self.unit_of_measurement is None + self.native_unit_of_measurement is None or self.device_class not in DEVICE_CLASS_UNITS ): self._attr_device_class = None return uoms = DEVICE_CLASS_UNITS[self.device_class] - self._uom = uoms.get(self.unit_of_measurement) or uoms.get( - self.unit_of_measurement.lower() + self._uom = uoms.get(self.native_unit_of_measurement) or uoms.get( + self.native_unit_of_measurement.lower() ) # Unknown unit of measurement, device class should not be used. From 846aeae40bc7476dbc74b1b6cff9254866dba6e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Dec 2021 15:35:41 +0100 Subject: [PATCH 284/366] Fix Tuya data type information in lights (#62993) --- homeassistant/components/tuya/light.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 16eda1cc324..0669dee86c4 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -405,7 +405,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): if self._brightness_dpcode: self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS) self._brightness_type = IntegerTypeData.from_json( - device.status_range[self._brightness_dpcode].values + device.function[self._brightness_dpcode].values ) # Check if min/max capable @@ -416,17 +416,17 @@ class TuyaLightEntity(TuyaEntity, LightEntity): and description.brightness_min in device.function ): self._brightness_max_type = IntegerTypeData.from_json( - device.status_range[description.brightness_max].values + device.function[description.brightness_max].values ) self._brightness_min_type = IntegerTypeData.from_json( - device.status_range[description.brightness_min].values + device.function[description.brightness_min].values ) # Update internals based on found color temperature dpcode if self._color_temp_dpcode: self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) self._color_temp_type = IntegerTypeData.from_json( - device.status_range[self._color_temp_dpcode].values + device.function[self._color_temp_dpcode].values ) # Update internals based on found color data dpcode From ac92a7f4251bccc524116aefbd810d0f651c3401 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Dec 2021 15:34:56 +0100 Subject: [PATCH 285/366] Fix Tuya data type information in climate (#62994) --- homeassistant/components/tuya/climate.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 83d635701db..35e6f5814f3 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -182,10 +182,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): # it to define min, max & step temperatures if ( self._set_temperature_dpcode - and self._set_temperature_dpcode in device.status_range + and self._set_temperature_dpcode in device.function ): type_data = IntegerTypeData.from_json( - device.status_range[self._set_temperature_dpcode].values + device.function[self._set_temperature_dpcode].values ) self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE self._set_temperature_type = type_data @@ -232,14 +232,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ] # Determine dpcode to use for setting the humidity - if ( - DPCode.HUMIDITY_SET in device.status - and DPCode.HUMIDITY_SET in device.status_range - ): + if DPCode.HUMIDITY_SET in device.function: self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY self._set_humidity_dpcode = DPCode.HUMIDITY_SET type_data = IntegerTypeData.from_json( - device.status_range[DPCode.HUMIDITY_SET].values + device.function[DPCode.HUMIDITY_SET].values ) self._set_humidity_type = type_data self._attr_min_humidity = int(type_data.min_scaled) From e5c5a7334996eb5d95ac184eddab81ec45434497 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Dec 2021 15:34:36 +0100 Subject: [PATCH 286/366] Gracefully handle missing preset mode in Tuya fan (#62996) --- homeassistant/components/tuya/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 99d28f1b998..4ebed3cf9e0 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -160,9 +160,9 @@ class TuyaFanEntity(TuyaEntity, FanEntity): return self.ha_preset_modes @property - def preset_mode(self) -> str: + def preset_mode(self) -> str | None: """Return the current preset_mode.""" - return self.device.status[DPCode.MODE] + return self.device.status.get(DPCode.MODE) @property def percentage(self) -> int | None: From 70c16d4fb7dbb09b84eabc69d8b410de236cf3ee Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 29 Dec 2021 15:13:52 +0100 Subject: [PATCH 287/366] Normalize Hue colortemp if value outside of bounds (#62998) --- homeassistant/components/hue/v2/group.py | 8 ++++++-- homeassistant/components/hue/v2/helpers.py | 14 ++++++++++++-- homeassistant/components/hue/v2/light.py | 8 ++++++-- tests/components/hue/test_light_v2.py | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 52c1bc6117a..7ef91f684fe 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -31,7 +31,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN from .entity import HueBaseEntity -from .helpers import normalize_hue_brightness, normalize_hue_transition +from .helpers import ( + normalize_hue_brightness, + normalize_hue_colortemp, + normalize_hue_transition, +) ALLOWED_ERRORS = [ "device (groupedLight) has communication issues, command (on) may not have effect", @@ -151,7 +155,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): """Turn the light on.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) xy_color = kwargs.get(ATTR_XY_COLOR) - color_temp = kwargs.get(ATTR_COLOR_TEMP) + color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP)) brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) diff --git a/homeassistant/components/hue/v2/helpers.py b/homeassistant/components/hue/v2/helpers.py index 307e7c55e03..97fdbe6160a 100644 --- a/homeassistant/components/hue/v2/helpers.py +++ b/homeassistant/components/hue/v2/helpers.py @@ -1,7 +1,8 @@ """Helper functions for Philips Hue v2.""" +from __future__ import annotations -def normalize_hue_brightness(brightness): +def normalize_hue_brightness(brightness: float | None) -> float | None: """Return calculated brightness values.""" if brightness is not None: # Hue uses a range of [0, 100] to control brightness. @@ -10,10 +11,19 @@ def normalize_hue_brightness(brightness): return brightness -def normalize_hue_transition(transition): +def normalize_hue_transition(transition: float | None) -> float | None: """Return rounded transition values.""" if transition is not None: # hue transition duration is in milliseconds and round them to 100ms transition = int(round(transition, 1) * 1000) return transition + + +def normalize_hue_colortemp(colortemp: int | None) -> int | None: + """Return color temperature within Hue's ranges.""" + if colortemp is not None: + # Hue only accepts a range between 153..500 + colortemp = min(colortemp, 500) + colortemp = max(colortemp, 153) + return colortemp diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 91afbe53e45..42444fd9ad0 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -30,7 +30,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge from ..const import DOMAIN from .entity import HueBaseEntity -from .helpers import normalize_hue_brightness, normalize_hue_transition +from .helpers import ( + normalize_hue_brightness, + normalize_hue_colortemp, + normalize_hue_transition, +) ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", @@ -158,7 +162,7 @@ class HueLight(HueBaseEntity, LightEntity): """Turn the device on.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) xy_color = kwargs.get(ATTR_XY_COLOR) - color_temp = kwargs.get(ATTR_COLOR_TEMP) + color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP)) brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 6bb084e6d06..8b811ffe7c6 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -141,6 +141,25 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert len(mock_bridge_v2.mock_requests) == 4 assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify" + # test again with sending a colortemperature which is out of range + # which should be normalized to the upper/lower bounds Hue can handle + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "color_temp": 50}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 5 + assert mock_bridge_v2.mock_requests[4]["json"]["color_temperature"]["mirek"] == 153 + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "color_temp": 550}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 6 + assert mock_bridge_v2.mock_requests[5]["json"]["color_temperature"]["mirek"] == 500 + async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): """Test calling the turn off service on a light.""" From 65774ec3008b2475621c7f2adb0d21dcc5310907 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Dec 2021 16:01:54 +0100 Subject: [PATCH 288/366] Bumped version to 2021.12.7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f20d78f9224..f9131e0a4f3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __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) From d315dc2ce4226e491494b1e0740bffd9f8b6efb7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Dec 2021 16:52:55 +0100 Subject: [PATCH 289/366] Hotfix for Nuki integration tests (#63007) --- tests/components/nuki/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index 46f4a4b6e6b..634902e054e 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -69,7 +69,7 @@ async def test_import(hass): data={"host": "1.1.1.1", "port": 8080, "token": "test-token"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == 123456789 + assert result["title"] == "75BCD15" assert result["data"] == { "host": "1.1.1.1", "port": 8080, From 292ff974fa1a2ca442f0f437d80a64b1c1cbe880 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 29 Dec 2021 10:25:29 -0600 Subject: [PATCH 290/366] Fix night mode switch state on Sonos (#63009) --- homeassistant/components/sonos/speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index ffc0d08a1b4..385af8224a6 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -482,7 +482,7 @@ class SonosSpeaker: for bool_var in ( "dialog_level", - "night_level", + "night_mode", "sub_enabled", "surround_enabled", ): From 05b2569621deeec9dcf0a31a27f57e422916bfe7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 29 Dec 2021 10:31:04 -0600 Subject: [PATCH 291/366] Add missing migration for Sonos speech enhancement switch entities (#63010) --- homeassistant/components/sonos/switch.py | 54 +++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index ad98523258a..ce3130ceaf2 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -7,8 +7,9 @@ import logging from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity -from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG, Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -113,6 +114,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): available_soco_attributes, speaker ) for feature_type in available_features: + if feature_type == ATTR_SPEECH_ENHANCEMENT: + async_migrate_speech_enhancement_entity_unique_id( + hass, config_entry, speaker + ) _LOGGER.debug( "Creating %s switch on %s", FRIENDLY_NAMES[feature_type], @@ -344,3 +349,48 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): await self.hass.async_add_executor_job(self.alarm.save) except (OSError, SoCoException, SoCoUPnPException) as exc: _LOGGER.error("Could not update %s: %s", self.entity_id, exc) + + +@callback +def async_migrate_speech_enhancement_entity_unique_id( + hass: HomeAssistant, + config_entry: ConfigEntry, + speaker: SonosSpeaker, +) -> None: + """Migrate Speech Enhancement switch entity unique_id.""" + entity_registry = er.async_get(hass) + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + + speech_enhancement_entries = [ + entry + for entry in registry_entries + if entry.domain == Platform.SWITCH + and entry.original_icon == FEATURE_ICONS[ATTR_SPEECH_ENHANCEMENT] + and entry.unique_id.startswith(speaker.soco.uid) + ] + + if len(speech_enhancement_entries) > 1: + _LOGGER.warning( + "Migration of Speech Enhancement switches on %s failed, manual cleanup required: %s", + speaker.zone_name, + [e.entity_id for e in speech_enhancement_entries], + ) + return + + if len(speech_enhancement_entries) == 1: + old_entry = speech_enhancement_entries[0] + if old_entry.unique_id.endswith("dialog_level"): + return + + new_unique_id = f"{speaker.soco.uid}-{ATTR_SPEECH_ENHANCEMENT}" + _LOGGER.debug( + "Migrating unique_id for %s from %s to %s", + old_entry.entity_id, + old_entry.unique_id, + new_unique_id, + ) + entity_registry.async_update_entity( + old_entry.entity_id, new_unique_id=new_unique_id + ) From eb91941640808e36655ec368213adcb5d030c19d Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Mon, 20 Dec 2021 14:25:01 +0800 Subject: [PATCH 292/366] Update version of iZone library to add some bug fixes (#61548) --- homeassistant/components/izone/climate.py | 5 ----- homeassistant/components/izone/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index db47b087c0d..3aead1c2176 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -511,11 +511,6 @@ class ZoneDevice(ClimateEntity): """Return True if entity is available.""" return self._controller.available - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return self._controller.assumed_state - @property def unique_id(self): """Return the ID of the controller device.""" diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 82927fef795..9cdf30ad42b 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -2,11 +2,11 @@ "domain": "izone", "name": "iZone", "documentation": "https://www.home-assistant.io/integrations/izone", - "requirements": ["python-izone==1.1.8"], + "requirements": ["python-izone==1.2.3"], "codeowners": ["@Swamp-Ig"], "config_flow": true, "homekit": { "models": ["iZone"] }, - "iot_class": "local_push" + "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 47d2b09b186..10232f9496e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1890,7 +1890,7 @@ python-gitlab==1.6.0 python-hpilo==4.3 # homeassistant.components.izone -python-izone==1.1.8 +python-izone==1.2.3 # homeassistant.components.joaoapps_join python-join-api==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 003ece83507..0465c1aebec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1136,7 +1136,7 @@ python-ecobee-api==0.2.14 python-forecastio==1.4.0 # homeassistant.components.izone -python-izone==1.1.8 +python-izone==1.2.3 # homeassistant.components.juicenet python-juicenet==1.0.2 From 69b1a93793a72358bf239b08181c024d71fefafb Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sun, 2 Jan 2022 16:46:18 -0300 Subject: [PATCH 293/366] Fix Tuya vacuum display battery level (#61643) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/vacuum.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 2afeb1880f7..4fc90b88ac0 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -112,9 +112,9 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): self._supported_features |= SUPPORT_FAN_SPEED self._fan_speed_type = EnumTypeData.from_json(function.values) - if function := device.function.get(DPCode.ELECTRICITY_LEFT): + if status_range := device.status_range.get(DPCode.ELECTRICITY_LEFT): self._supported_features |= SUPPORT_BATTERY - self._battery_level_type = IntegerTypeData.from_json(function.values) + self._battery_level_type = IntegerTypeData.from_json(status_range.values) @property def battery_level(self) -> int | None: From e3d2993d98302796c5d651e9cd7a7f7d5ca38090 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 4 Jan 2022 13:42:54 +0100 Subject: [PATCH 294/366] Update no_ip URL (#62477) --- homeassistant/components/no_ip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 1c4bcb40819..245222724ab 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -35,7 +35,7 @@ NO_IP_ERRORS = { "911": "A fatal error on NO-IP's side such as a database outage", } -UPDATE_URL = "https://dynupdate.noip.com/nic/update" +UPDATE_URL = "https://dynupdate.no-ip.com/nic/update" HA_USER_AGENT = f"{SERVER_SOFTWARE} {EMAIL}" CONFIG_SCHEMA = vol.Schema( From 76d5e2ea90eb60752712f658e5a69b0d201823ca Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 4 Jan 2022 22:43:22 +0100 Subject: [PATCH 295/366] Do not create a number LED brightness entity for Xiaomi Miio devices that do not support it (#62819) Co-authored-by: Paulus Schoutsen --- homeassistant/components/xiaomi_miio/number.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 2823c6c2582..94b3412d44a 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES from homeassistant.core import callback @@ -248,6 +249,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): return for feature, description in NUMBER_TYPES.items(): + if feature == FEATURE_SET_LED_BRIGHTNESS and model != MODEL_FAN_ZA5: + # Delete LED bightness entity created by mistake if it exists + entity_reg = hass.helpers.entity_registry.async_get() + entity_id = entity_reg.async_get_entity_id( + PLATFORM_DOMAIN, DOMAIN, f"{description.key}_{config_entry.unique_id}" + ) + if entity_id: + entity_reg.async_remove(entity_id) + continue if feature & features: if ( description.key == ATTR_OSCILLATION_ANGLE From b78e44d1d190c69f8e9966314b34363e10f580ff Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Thu, 30 Dec 2021 01:13:58 +0100 Subject: [PATCH 296/366] Fix local_ip handling in KNX options flow (#62969) --- homeassistant/components/knx/config_flow.py | 21 +++-- homeassistant/components/knx/strings.json | 6 +- .../components/knx/translations/en.json | 12 ++- tests/components/knx/test_config_flow.py | 77 ++++++++++++++----- 4 files changed, 84 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 96aa8f67e3b..01e71eb37af 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -28,6 +28,7 @@ from .schema import ConnectionSchema CONF_KNX_GATEWAY: Final = "gateway" CONF_MAX_RATE_LIMIT: Final = 60 +CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0" DEFAULT_ENTRY_DATA: Final = { ConnectionSchema.CONF_KNX_STATE_UPDATER: ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER, @@ -328,6 +329,12 @@ class KNXOptionsFlowHandler(OptionsFlow): entry_data = { **DEFAULT_ENTRY_DATA, **self.general_settings, + ConnectionSchema.CONF_KNX_LOCAL_IP: self.general_settings.get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ) + if self.general_settings.get(ConnectionSchema.CONF_KNX_LOCAL_IP) + != CONF_DEFAULT_LOCAL_IP + else None, CONF_HOST: self.current_config.get(CONF_HOST, ""), } @@ -337,7 +344,7 @@ class KNXOptionsFlowHandler(OptionsFlow): **user_input, } - entry_title = entry_data[CONF_KNX_CONNECTION_TYPE].capitalize() + entry_title = str(entry_data[CONF_KNX_CONNECTION_TYPE]).capitalize() if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING: entry_title = f"{CONF_KNX_TUNNELING.capitalize()} @ {entry_data[CONF_HOST]}" @@ -388,12 +395,16 @@ class KNXOptionsFlowHandler(OptionsFlow): } if self.show_advanced_options: + local_ip = ( + self.current_config.get(ConnectionSchema.CONF_KNX_LOCAL_IP) + if self.current_config.get(ConnectionSchema.CONF_KNX_LOCAL_IP) + is not None + else CONF_DEFAULT_LOCAL_IP + ) data_schema[ - vol.Optional( + vol.Required( ConnectionSchema.CONF_KNX_LOCAL_IP, - default=self.current_config.get( - ConnectionSchema.CONF_KNX_LOCAL_IP, - ), + default=local_ip, ) ] = str data_schema[ diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 4db92888aab..d219880be5c 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -20,7 +20,7 @@ "host": "[%key:common::config_flow::data::host%]", "individual_address": "Individual address for the connection", "route_back": "Route Back / NAT Mode", - "local_ip": "Local IP (leave empty if unsure)" + "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)" } }, "routing": { @@ -29,7 +29,7 @@ "individual_address": "Individual address for the routing connection", "multicast_group": "The multicast group used for routing", "multicast_port": "The multicast port used for routing", - "local_ip": "Local IP (leave empty if unsure)" + "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)" } } }, @@ -49,7 +49,7 @@ "individual_address": "Default individual address", "multicast_group": "Multicast group used for routing and discovery", "multicast_port": "Multicast port used for routing and discovery", - "local_ip": "Local IP (leave empty if unsure)", + "local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)", "state_updater": "Globally enable reading states from the KNX Bus", "rate_limit": "Maximum outgoing telegrams per second" } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 91b9dfce5f3..43aeced6698 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -12,7 +12,7 @@ "data": { "host": "Host", "individual_address": "Individual address for the connection", - "local_ip": "Local IP (leave empty if unsure)", + "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)", "port": "Port", "route_back": "Route Back / NAT Mode" }, @@ -21,9 +21,9 @@ "routing": { "data": { "individual_address": "Individual address for the routing connection", + "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)", "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing", - "local_ip": "Local IP (leave empty if unsure)" + "multicast_port": "The multicast port used for routing" }, "description": "Please configure the routing options." }, @@ -47,6 +47,10 @@ "data": { "connection_type": "KNX Connection Type", "individual_address": "Default individual address", +<<<<<<< HEAD +======= + "local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)", +>>>>>>> b9247f3952 (Fix local_ip handling in KNX options flow (#62969)) "multicast_group": "Multicast group used for routing and discovery", "multicast_port": "Multicast port used for routing and discovery", "local_ip": "Local IP (leave empty if unsure)", @@ -63,4 +67,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 4f3e1734b69..aec757a1086 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -1,6 +1,7 @@ """Test the KNX config flow.""" from unittest.mock import patch +import pytest from xknx import XKNX from xknx.io import DEFAULT_MCAST_GRP from xknx.io.gateway_scanner import GatewayDescriptor @@ -8,6 +9,7 @@ from xknx.io.gateway_scanner import GatewayDescriptor from homeassistant import config_entries from homeassistant.components.knx import ConnectionSchema from homeassistant.components.knx.config_flow import ( + CONF_DEFAULT_LOCAL_IP, CONF_KNX_GATEWAY, DEFAULT_ENTRY_DATA, ) @@ -585,6 +587,7 @@ async def test_options_flow( CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", CONF_HOST: "", + ConnectionSchema.CONF_KNX_LOCAL_IP: None, ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, @@ -643,14 +646,65 @@ async def test_tunneling_options_flow( ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, ConnectionSchema.CONF_KNX_STATE_UPDATER: True, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, } +@pytest.mark.parametrize( + "user_input,config_entry_data", + [ + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, + ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + CONF_HOST: "", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, + ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, + ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: CONF_DEFAULT_LOCAL_IP, + }, + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + CONF_HOST: "", + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, + ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, + }, + ), + ], +) async def test_advanced_options( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + user_input, + config_entry_data, ) -> None: """Test options config flow.""" mock_config_entry.add_to_hass(hass) @@ -668,28 +722,11 @@ async def test_advanced_options( result2 = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={ - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, - ConnectionSchema.CONF_KNX_STATE_UPDATER: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", - }, + user_input=user_input, ) await hass.async_block_till_done() assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY assert not result2.get("data") - assert mock_config_entry.data == { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_HOST: "", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, - ConnectionSchema.CONF_KNX_STATE_UPDATER: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", - } + assert mock_config_entry.data == config_entry_data From 1f81f84b6eb904a8b21a645bc61d4958248e05c6 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 29 Dec 2021 18:26:52 +0100 Subject: [PATCH 297/366] Fix reporting correct colormode for 3rd party Hue lights (#63015) --- homeassistant/components/hue/v2/group.py | 2 - homeassistant/components/hue/v2/light.py | 60 +++++++++++++++++++----- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 7ef91f684fe..775c2dd1d76 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -7,7 +7,6 @@ from typing import Any from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.groups import GroupedLight, Room, Zone -from aiohue.v2.models.feature import AlertEffectType from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -193,7 +192,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity): color_xy=xy_color if light.supports_color else None, color_temp=color_temp if light.supports_color_temperature else None, transition_time=transition, - alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) for light in self.controller.get_lights(self.resource.id) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 42444fd9ad0..ee40222b083 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -91,6 +91,9 @@ class HueLight(HueBaseEntity, LightEntity): self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) # support transition if brightness control self._attr_supported_features |= SUPPORT_TRANSITION + self._last_xy: tuple[float, float] | None = self.xy_color + self._last_color_temp: int = self.color_temp + self._set_color_mode() @property def brightness(self) -> int | None: @@ -100,18 +103,6 @@ class HueLight(HueBaseEntity, LightEntity): return round((dimming.brightness / 100) * 255) return None - @property - def color_mode(self) -> str: - """Return the current color mode of the light.""" - if color_temp := self.resource.color_temperature: - if color_temp.mirek_valid and color_temp.mirek is not None: - return COLOR_MODE_COLOR_TEMP - if self.resource.supports_color: - return COLOR_MODE_XY - if self.resource.supports_dimming: - return COLOR_MODE_BRIGHTNESS - return COLOR_MODE_ONOFF - @property def is_on(self) -> bool: """Return true if device is on (brightness above 0).""" @@ -158,6 +149,11 @@ class HueLight(HueBaseEntity, LightEntity): "dynamics": self.resource.dynamics.status.value, } + @callback + def on_update(self) -> None: + """Call on update event.""" + self._set_color_mode() + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) @@ -212,3 +208,43 @@ class HueLight(HueBaseEntity, LightEntity): id=self.resource.id, short=flash == FLASH_SHORT, ) + + @callback + def _set_color_mode(self) -> None: + """Set current colormode of light.""" + last_xy = self._last_xy + last_color_temp = self._last_color_temp + self._last_xy = self.xy_color + self._last_color_temp = self.color_temp + + # Certified Hue lights return `mired_valid` to indicate CT is active + if color_temp := self.resource.color_temperature: + if color_temp.mirek_valid and color_temp.mirek is not None: + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + return + + # Non-certified lights do not report their current color mode correctly + # so we keep track of the color values to determine which is active + if last_color_temp != self.color_temp: + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + return + if last_xy != self.xy_color: + self._attr_color_mode = COLOR_MODE_XY + return + + # if we didn't detect any changes, abort and use previous values + if self._attr_color_mode is not None: + return + + # color mode not yet determined, work it out here + # Note that for lights that do not correctly report `mirek_valid` + # we might have an invalid startup state which will be auto corrected + if self.resource.supports_color: + self._attr_color_mode = COLOR_MODE_XY + elif self.resource.supports_color_temperature: + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + elif self.resource.supports_dimming: + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + else: + # fallback to on_off + self._attr_color_mode = COLOR_MODE_ONOFF From 5e42f95bc668a8a771e39a7a84b0c4bf652e6447 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 31 Dec 2021 05:46:52 +0100 Subject: [PATCH 298/366] Hue allow per-device availability override (#63025) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/config_flow.py | 66 +++++++++++++++---- homeassistant/components/hue/const.py | 1 + homeassistant/components/hue/strings.json | 3 +- .../components/hue/translations/en.json | 3 +- .../components/hue/translations/nl.json | 3 +- homeassistant/components/hue/v2/entity.py | 62 ++++++++++------- homeassistant/components/hue/v2/group.py | 2 +- tests/components/hue/test_config_flow.py | 26 +++++++- 8 files changed, 122 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 49fca2158d5..987afe17012 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -17,13 +17,15 @@ from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import aiohttp_client, device_registry +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, CONF_API_VERSION, + CONF_IGNORE_AVAILABILITY, DEFAULT_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_UNREACHABLE, DOMAIN, @@ -46,17 +48,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> HueOptionsFlowHandler: + ) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler: """Get the options flow for this handler.""" - return HueOptionsFlowHandler(config_entry) - - @classmethod - @callback - def async_supports_options_flow( - cls, config_entry: config_entries.ConfigEntry - ) -> bool: - """Return options flow support for this handler.""" - return config_entry.data.get(CONF_API_VERSION, 1) == 1 + if config_entry.data.get(CONF_API_VERSION, 1) == 1: + return HueV1OptionsFlowHandler(config_entry) + return HueV2OptionsFlowHandler(config_entry) def __init__(self) -> None: """Initialize the Hue flow.""" @@ -288,8 +284,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_link() -class HueOptionsFlowHandler(config_entries.OptionsFlow): - """Handle Hue options.""" +class HueV1OptionsFlowHandler(config_entries.OptionsFlow): + """Handle Hue options for V1 implementation.""" def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Hue options flow.""" @@ -319,3 +315,47 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow): } ), ) + + +class HueV2OptionsFlowHandler(config_entries.OptionsFlow): + """Handle Hue options for V2 implementation.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize Hue options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: + """Manage Hue options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + # create a list of Hue device ID's that the user can select + # to ignore availability status + dev_reg = device_registry.async_get(self.hass) + entries = device_registry.async_entries_for_config_entry( + dev_reg, self.config_entry.entry_id + ) + dev_ids = { + identifier[1]: entry.name + for entry in entries + for identifier in entry.identifiers + if identifier[0] == DOMAIN + } + # filter any non existing device id's from the list + cur_ids = [ + item + for item in self.config_entry.options.get(CONF_IGNORE_AVAILABILITY, []) + if item in dev_ids + ] + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_IGNORE_AVAILABILITY, + default=cur_ids, + ): cv.multi_select(dev_ids), + } + ), + ) diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index eef453fb83d..798148b92c0 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -3,6 +3,7 @@ DOMAIN = "hue" CONF_API_VERSION = "api_version" +CONF_IGNORE_AVAILABILITY = "ignore_availability" CONF_SUBTYPE = "subtype" diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 458e21419ab..266f26016c4 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -70,7 +70,8 @@ "data": { "allow_hue_groups": "Allow Hue groups", "allow_hue_scenes": "Allow Hue scenes", - "allow_unreachable": "Allow unreachable bulbs to report their state correctly" + "allow_unreachable": "Allow unreachable bulbs to report their state correctly", + "ignore_availability": "Ignore connectivity status for the given devices" } } } diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index f0b8e560729..7757aca9373 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -69,7 +69,8 @@ "data": { "allow_hue_groups": "Allow Hue groups", "allow_hue_scenes": "Allow Hue scenes", - "allow_unreachable": "Allow unreachable bulbs to report their state correctly" + "allow_unreachable": "Allow unreachable bulbs to report their state correctly", + "ignore_availability": "Ignore connectivity status for the given devices" } } } diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index 12eeaf71af0..7997a03c4aa 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -69,7 +69,8 @@ "data": { "allow_hue_groups": "Sta Hue-groepen toe", "allow_hue_scenes": "Sta Hue sc\u00e8nes toe", - "allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden" + "allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden", + "ignore_availability": "Negeer beschikbaarheid status voor deze apparaten" } } } diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 8253d4ffbef..70987fff2be 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from ..bridge import HueBridge -from ..const import DOMAIN +from ..const import CONF_IGNORE_AVAILABILITY, DOMAIN RESOURCE_TYPE_NAMES = { # a simple mapping of hue resource type to Hass name @@ -71,7 +71,7 @@ class HueBaseEntity(Entity): async def async_added_to_hass(self) -> None: """Call when entity is added.""" - self._check_availability_workaround() + self._check_availability() # Add value_changed callbacks. self.async_on_remove( self.controller.subscribe( @@ -80,7 +80,7 @@ class HueBaseEntity(Entity): (EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED), ) ) - # also subscribe to device update event to catch devicer changes (e.g. name) + # also subscribe to device update event to catch device changes (e.g. name) if self.device is None: return self.async_on_remove( @@ -92,25 +92,27 @@ class HueBaseEntity(Entity): ) # subscribe to zigbee_connectivity to catch availability changes if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): - self.bridge.api.sensors.zigbee_connectivity.subscribe( - self._handle_event, - zigbee.id, - EventType.RESOURCE_UPDATED, + self.async_on_remove( + self.bridge.api.sensors.zigbee_connectivity.subscribe( + self._handle_event, + zigbee.id, + EventType.RESOURCE_UPDATED, + ) ) @property def available(self) -> bool: """Return entity availability.""" + # entities without a device attached should be always available if self.device is None: - # entities without a device attached should be always available return True + # the zigbee connectivity sensor itself should be always available if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY: - # the zigbee connectivity sensor itself should be always available return True if self._ignore_availability: return True + # all device-attached entities get availability from the zigbee connectivity if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): - # all device-attached entities get availability from the zigbee connectivity return zigbee.status == ConnectivityServiceStatus.CONNECTED return True @@ -130,30 +132,41 @@ class HueBaseEntity(Entity): ent_reg.async_remove(self.entity_id) else: self.logger.debug("Received status update for %s", self.entity_id) - self._check_availability_workaround() + self._check_availability() self.on_update() self.async_write_ha_state() @callback - def _check_availability_workaround(self): + def _check_availability(self): """Check availability of the device.""" - if self.resource.type != ResourceTypes.LIGHT: - return + # return if we already processed this entity if self._ignore_availability is not None: - # already processed return + # only do the availability check for entities connected to a device + if self.device is None: + return + # ignore availability if user added device to ignore list + if self.device.id in self.bridge.config_entry.options.get( + CONF_IGNORE_AVAILABILITY, [] + ): + self._ignore_availability = True + self.logger.info( + "Device %s is configured to ignore availability status. ", + self.name, + ) + return + # certified products (normally) report their state correctly + # no need for workaround/reporting if self.device.product_data.certified: - # certified products report their state correctly self._ignore_availability = False return # some (3th party) Hue lights report their connection status incorrectly # causing the zigbee availability to report as disconnected while in fact - # it can be controlled. Although this is in fact something the device manufacturer - # should fix, we work around it here. If the light is reported unavailable + # it can be controlled. If the light is reported unavailable # by the zigbee connectivity but the state changes its considered as a # malfunctioning device and we report it. - # while the user should actually fix this issue instead of ignoring it, we - # ignore the availability for this light from this point. + # While the user should actually fix this issue, we allow to + # ignore the availability for this light/device from the config options. cur_state = self.resource.on.on if self._last_state is None: self._last_state = cur_state @@ -166,9 +179,10 @@ class HueBaseEntity(Entity): # the device state changed from on->off or off->on # while it was reported as not connected! self.logger.warning( - "Light %s changed state while reported as disconnected. " - "This might be an indicator that routing is not working for this device. " - "Home Assistant will ignore availability for this light from now on. " + "Device %s changed state while reported as disconnected. " + "This might be an indicator that routing is not working for this device " + "or the device is having connectivity issues. " + "You can disable availability reporting for this device in the Hue options. " "Device details: %s - %s (%s) fw: %s", self.name, self.device.product_data.manufacturer_name, @@ -178,6 +192,4 @@ class HueBaseEntity(Entity): ) # do we want to store this in some persistent storage? self._ignore_availability = True - else: - self._ignore_availability = False self._last_state = cur_state diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 775c2dd1d76..b8fdb0b0b1d 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -102,7 +102,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): # Entities for Hue groups are disabled by default # unless they were enabled in old version (legacy option) - self._attr_entity_registry_enabled_default = bridge.config_entry.data.get( + self._attr_entity_registry_enabled_default = bridge.config_entry.options.get( CONF_ALLOW_HUE_GROUPS, False ) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 6ce8ff3e1c4..0aa032ddb0d 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect +from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry @@ -701,12 +702,33 @@ async def test_options_flow_v2(hass): """Test options config flow for a V2 bridge.""" entry = MockConfigEntry( domain="hue", - unique_id="v2bridge", + unique_id="aabbccddeeff", data={"host": "0.0.0.0", "api_version": 2}, ) entry.add_to_hass(hass) - assert config_flow.HueFlowHandler.async_supports_options_flow(entry) is False + dev_reg = dr.async_get(hass) + mock_dev_id = "aabbccddee" + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, identifiers={(const.DOMAIN, mock_dev_id)} + ) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert _get_schema_default(schema, const.CONF_IGNORE_AVAILABILITY) == [] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={const.CONF_IGNORE_AVAILABILITY: [mock_dev_id]}, + ) + + assert result["type"] == "create_entry" + assert result["data"] == { + const.CONF_IGNORE_AVAILABILITY: [mock_dev_id], + } async def test_bridge_zeroconf(hass, aioclient_mock): From 17dbdbb395a9e40792ddffffe7b839cc23109c64 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 20 Dec 2021 06:00:55 +0100 Subject: [PATCH 299/366] Bump pyatmo to 6.2.1 (#62291) --- homeassistant/components/netatmo/climate.py | 11 +++++------ homeassistant/components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/select.py | 12 ++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1ead9d7cbdb..c8b5e01e5db 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -136,14 +136,13 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - try: - await data_handler.register_data_class( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) - except KeyError: + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id + ) + + if (climate_state := data_handler.data[signal_name]) is None: continue - climate_state = data_handler.data[signal_name] climate_topology.register_handler(home_id, climate_state.process_topology) for room in climate_state.homes[home_id].rooms.values(): diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 501d5142bcc..4ee7aeb0fde 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==6.2.0" + "pyatmo==6.2.1" ], "after_dependencies": [ "cloud", diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 98576497f3e..4d69c9ab853 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -49,17 +49,13 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - try: - await data_handler.register_data_class( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) - except KeyError: - continue - await data_handler.register_data_class( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) - climate_state = data_handler.data.get(signal_name) + + if (climate_state := data_handler.data[signal_name]) is None: + continue + climate_topology.register_handler(home_id, climate_state.process_topology) hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ diff --git a/requirements_all.txt b/requirements_all.txt index 10232f9496e..ab797dfdf03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1364,7 +1364,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.0 +pyatmo==6.2.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0465c1aebec..7a34615a511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -832,7 +832,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.0 +pyatmo==6.2.1 # homeassistant.components.apple_tv pyatv==0.8.2 From 30620ef48f91bdc1e780ae48e35be919b0bd13b6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 30 Dec 2021 09:00:30 +0100 Subject: [PATCH 300/366] Bump pyatmo to v6.2.2 (#63053) Signed-off-by: cgtobi --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 4ee7aeb0fde..581a954df30 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==6.2.1" + "pyatmo==6.2.2" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index ab797dfdf03..537c5cc62f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1364,7 +1364,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.1 +pyatmo==6.2.2 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a34615a511..37189de291b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -832,7 +832,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.1 +pyatmo==6.2.2 # homeassistant.components.apple_tv pyatv==0.8.2 From 17e5766d2322f7ac5c46b4f1161d69a79e525987 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 30 Dec 2021 10:25:44 +0100 Subject: [PATCH 301/366] Ignore serial number "blank" from NUT (#63066) --- homeassistant/components/nut/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 1a040b99f57..6de2640da9c 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -32,6 +32,8 @@ from .const import ( UNDO_UPDATE_LISTENER, ) +NUT_FAKE_SERIAL = ["unknown", "blank"] + _LOGGER = logging.getLogger(__name__) @@ -140,7 +142,9 @@ def _firmware_from_status(status): def _serial_from_status(status): """Find the best serialvalue from the status.""" serial = status.get("device.serial") or status.get("ups.serial") - if serial and (serial.lower() == "unknown" or serial.count("0") == len(serial)): + if serial and ( + serial.lower() in NUT_FAKE_SERIAL or serial.count("0") == len(serial) + ): return None return serial From c4d871a9d7d808e3abde4005264102b3f1729a16 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Thu, 30 Dec 2021 15:15:59 -0500 Subject: [PATCH 302/366] Bump greeclimate to 1.0.1 (#63092) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index f4f8cf153a3..3af6f77236d 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.12.5"], + "requirements": ["greeclimate==1.0.1"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 537c5cc62f7..53229f471ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -754,7 +754,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.12.5 +greeclimate==1.0.1 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37189de291b..e14b0e335b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -467,7 +467,7 @@ google-nest-sdm==0.4.9 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.12.5 +greeclimate==1.0.1 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 From 9df40d7056dddf11c07066d36d4e082b23a93801 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 31 Dec 2021 19:47:03 +0200 Subject: [PATCH 303/366] Fix Shelly error fetching device triggers for sleeping devices (#63103) --- .../components/shelly/device_trigger.py | 3 ++ .../components/shelly/test_device_trigger.py | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index f5abf76e8f2..3e839507127 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -123,6 +123,9 @@ async def async_get_triggers( append_input_triggers(triggers, input_triggers, device_id) return triggers + if not block_wrapper.device.initialized: + return triggers + assert block_wrapper.device.blocks for block in block_wrapper.device.blocks: diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index a81662159c2..73f92ca9640 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -168,6 +168,42 @@ async def test_get_triggers_button(hass): assert_lists_same(triggers, expected_triggers) +async def test_get_triggers_non_initialized_devices(hass): + """Test we get the empty triggers for non-initialized devices.""" + await async_setup_component(hass, "shelly", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"sleep_period": 43200, "model": "SHDW-2", "host": "1.2.3.4"}, + unique_id="12345678", + ) + config_entry.add_to_hass(hass) + + device = Mock( + blocks=None, + settings=None, + shelly=None, + update=AsyncMock(), + initialized=False, + ) + + hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} + hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} + coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ + BLOCK + ] = BlockDeviceWrapper(hass, config_entry, device) + + coap_wrapper.async_setup() + + expected_triggers = [] + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id + ) + + assert_lists_same(triggers, expected_triggers) + + async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper): """Test error raised for invalid shelly device_id.""" assert coap_wrapper From e39cf1a4c65419f8f8e9d81e0d331cb919ee7d6a Mon Sep 17 00:00:00 2001 From: Karthik T Date: Mon, 3 Jan 2022 04:51:50 +0800 Subject: [PATCH 304/366] Fix systemmonitor CPU temp for Armbian on PineA64 (#63111) --- homeassistant/components/systemmonitor/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 8bbdae820e3..a05601caa02 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -307,6 +307,7 @@ CPU_SENSOR_PREFIXES = [ "soc_thermal 1", "Tctl", "cpu0-thermal", + "cpu0_thermal", ] From 4c52b9731ff26d719c08ede8081fb1bfaef28e0f Mon Sep 17 00:00:00 2001 From: ryborg Date: Tue, 4 Jan 2022 12:24:44 -0500 Subject: [PATCH 305/366] Fix CO/CO2 sensors mixup in Google Assistant (#63152) Co-authored-by: Franck Nijhof --- homeassistant/components/google_assistant/trait.py | 4 ++-- tests/components/google_assistant/test_trait.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 843ebfee87a..c3a7d403a2f 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2296,8 +2296,8 @@ class SensorStateTrait(_Trait): sensor_types = { sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"), - sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"), - sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"), + sensor.DEVICE_CLASS_CO: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"), + sensor.DEVICE_CLASS_CO2: ("CarbonDioxideLevel", "PARTS_PER_MILLION"), sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"), sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"), sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: ( diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a396c1bc91d..278b6dc2ffe 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -3030,8 +3030,8 @@ async def test_sensorstate(hass): """Test SensorState trait support for sensor domain.""" sensor_types = { sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"), - sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"), - sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"), + sensor.DEVICE_CLASS_CO: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"), + sensor.DEVICE_CLASS_CO2: ("CarbonDioxideLevel", "PARTS_PER_MILLION"), sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"), sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"), sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: ( From 60f4521a2d6d93b321bd1bca857c6c51c7972916 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jan 2022 05:32:39 -1000 Subject: [PATCH 306/366] Bump flux_led to 0.27.28 to fix missing white channel on SK6812RGBW strips (#63154) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 9f1eaa5d4a4..583570d4784 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.21"], + "requirements": ["flux_led==0.27.28"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 53229f471ab..851f906ebbc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.21 +flux_led==0.27.28 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e14b0e335b5..5c023211aa5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.21 +flux_led==0.27.28 # homeassistant.components.homekit fnvhash==0.1.0 From 5490a65101051c7f41073b6a6a6d5b0906b00f0f Mon Sep 17 00:00:00 2001 From: trdischat <52774325+trdischat@users.noreply.github.com> Date: Sat, 1 Jan 2022 23:15:27 -0800 Subject: [PATCH 307/366] Add default Fronius logger model for v0 API (#63184) --- homeassistant/components/fronius/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index cf648d3b613..5a130f68987 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -160,7 +160,10 @@ class FroniusSolarNet: ) if self.logger_coordinator: _logger_info = self.logger_coordinator.data[SOLAR_NET_ID_SYSTEM] - solar_net_device[ATTR_MODEL] = _logger_info["product_type"]["value"] + # API v0 doesn't provide product_type + solar_net_device[ATTR_MODEL] = _logger_info.get("product_type", {}).get( + "value", "Datalogger Web" + ) solar_net_device[ATTR_SW_VERSION] = _logger_info["software_version"][ "value" ] From 5910460f0017f4875f27c02ab720a6d6fc9cd8c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jan 2022 01:06:44 -1000 Subject: [PATCH 308/366] Prevent doorbird integration from overloading the device on startup (#63253) --- homeassistant/components/doorbird/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 7497304f9e1..4d9f0091e6c 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -248,8 +248,10 @@ class ConfiguredDoorBird: if self.custom_url is not None: hass_url = self.custom_url + favorites = self.device.favorites() + for event in self.doorstation_events: - self._register_event(hass_url, event) + self._register_event(hass_url, event, favs=favorites) _LOGGER.info("Successfully registered URL for %s on %s", event, self.name) @@ -261,15 +263,15 @@ class ConfiguredDoorBird: def _get_event_name(self, event): return f"{self.slug}_{event}" - def _register_event(self, hass_url, event): + def _register_event(self, hass_url, event, favs=None): """Add a schedule entry in the device for a sensor.""" url = f"{hass_url}{API_URL}/{event}?token={self._token}" # Register HA URL as webhook if not already, then get the ID - if not self.webhook_is_registered(url): + if not self.webhook_is_registered(url, favs=favs): self.device.change_favorite("http", f"Home Assistant ({event})", url) - if not self.get_webhook_id(url): + if not self.get_webhook_id(url, favs=favs): _LOGGER.warning( 'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"', url, From ed4f0a50a5714a08e97bb60718f8477db06dffc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jan 2022 01:05:35 -1000 Subject: [PATCH 309/366] Bump flux_led to 0.27.32 to fix incorrect strip order on A2 devices (#63262) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 583570d4784..6703065476e 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.28"], + "requirements": ["flux_led==0.27.32"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 851f906ebbc..36f39fe9390 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.28 +flux_led==0.27.32 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c023211aa5..9350933db34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.28 +flux_led==0.27.32 # homeassistant.components.homekit fnvhash==0.1.0 From f2f0fba611eabfd0153914ec17c1e58d696ce041 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jan 2022 10:46:56 -0800 Subject: [PATCH 310/366] Sisyphus: Fix bad super call (#63327) Co-authored-by: Franck Nijhof --- homeassistant/components/sisyphus/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index 21e50b19a1b..f255c007582 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -163,7 +163,7 @@ class SisyphusPlayer(MediaPlayerEntity): if self._table.active_track: return self._table.active_track.get_thumbnail_url(Track.ThumbnailSize.LARGE) - return super.media_image_url() + return super().media_image_url async def async_turn_on(self): """Wake up a sleeping table.""" From 39cfc1c8399a4437e6b19e9a1d2e4dc3f7451509 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 4 Jan 2022 19:32:25 +0100 Subject: [PATCH 311/366] Fix status type in Shelly climate platform (#63347) --- homeassistant/components/shelly/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 06140fcba72..777a41a6664 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -219,7 +219,7 @@ class BlockSleepingClimate( return CURRENT_HVAC_OFF return ( - CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT + CURRENT_HVAC_HEAT if bool(self.device_block.status) else CURRENT_HVAC_IDLE ) @property From 4d9d186ddff678041588dd259324f0c168b5c20a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 4 Jan 2022 15:37:33 +0100 Subject: [PATCH 312/366] Bump micloud to 0.5 (#63348) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 8de844cdd44..da2b94f5382 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"], + "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.9.2"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 36f39fe9390..34002e25317 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1006,7 +1006,7 @@ meteofrance-api==1.0.2 mficlient==0.3.0 # homeassistant.components.xiaomi_miio -micloud==0.4 +micloud==0.5 # homeassistant.components.miflora miflora==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9350933db34..ccbcfd7d5e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -612,7 +612,7 @@ meteofrance-api==1.0.2 mficlient==0.3.0 # homeassistant.components.xiaomi_miio -micloud==0.4 +micloud==0.5 # homeassistant.components.mill mill-local==0.1.0 From 9126125e16c5a4f88dd3c579c741c21718e7ef24 Mon Sep 17 00:00:00 2001 From: Christopher Masto Date: Tue, 4 Jan 2022 17:04:43 -0500 Subject: [PATCH 313/366] Work around ingress glitch with 304 responses (#63355) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hassio/ingress.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 6935bbdc7da..5369089d0f0 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -134,7 +134,7 @@ class HassIOIngress(HomeAssistantView): if ( hdrs.CONTENT_LENGTH in result.headers and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000 - ): + ) or result.status in (204, 304): # Return Response body = await result.read() return web.Response( From 082c9c34eab81a2805ebab4a7c07475c6303012f Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 4 Jan 2022 17:15:19 +0100 Subject: [PATCH 314/366] Fix Hue grouped light color_mode calculation (#63374) --- homeassistant/components/hue/v2/group.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index b8fdb0b0b1d..835f90d3f6f 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -298,7 +298,10 @@ class GroupedHueLight(HueBaseEntity, LightEntity): supported_color_modes.add(COLOR_MODE_ONOFF) self._attr_supported_color_modes = supported_color_modes # pick a winner for the current colormode - if lights_in_colortemp_mode == lights_with_color_temp_support: + if ( + lights_with_color_temp_support > 0 + and lights_in_colortemp_mode == lights_with_color_temp_support + ): self._attr_color_mode = COLOR_MODE_COLOR_TEMP elif lights_with_color_support > 0: self._attr_color_mode = COLOR_MODE_XY From 27a74d2720773ff6b9ece7311921e4c806fce078 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 Jan 2022 22:16:35 +0100 Subject: [PATCH 315/366] Fix missing timezone in GTFS timestamp sensor (#63401) --- homeassistant/components/gtfs/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 367a45aa073..beaa37e7429 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -619,7 +619,9 @@ class GTFSDepartureSensor(SensorEntity): if not self._departure: self._state = None else: - self._state = self._departure["departure_time"] + self._state = self._departure["departure_time"].replace( + tzinfo=dt_util.UTC + ) # Fetch trip and route details once, unless updated if not self._departure: From 599c6240f4652f53c4d23d88d86afeefeb18e31e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jan 2022 14:12:39 -0800 Subject: [PATCH 316/366] Bumped version to 2021.12.8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f9131e0a4f3..45104f0d81b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "7" +PATCH_VERSION: Final = "8" __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) From 41b43199a39d81cab2e5eefb99fe0a5d865d2ad2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jan 2022 14:48:13 -0800 Subject: [PATCH 317/366] Fix merge conflicts --- homeassistant/components/knx/translations/en.json | 4 ---- tests/components/shelly/test_device_trigger.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 43aeced6698..beff8cecd00 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -47,13 +47,9 @@ "data": { "connection_type": "KNX Connection Type", "individual_address": "Default individual address", -<<<<<<< HEAD -======= "local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)", ->>>>>>> b9247f3952 (Fix local_ip handling in KNX options flow (#62969)) "multicast_group": "Multicast group used for routing and discovery", "multicast_port": "Multicast port used for routing and discovery", - "local_ip": "Local IP (leave empty if unsure)", "rate_limit": "Maximum outgoing telegrams per second", "state_updater": "Globally enable reading states from the KNX Bus" } diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 73f92ca9640..238fb9b0336 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -198,7 +198,7 @@ async def test_get_triggers_non_initialized_devices(hass): expected_triggers = [] triggers = await async_get_device_automations( - hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id + hass, "trigger", coap_wrapper.device_id ) assert_lists_same(triggers, expected_triggers) From 888abd63a18b541954ca39b065b8cc06ef502848 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 4 Jan 2022 17:16:53 -0600 Subject: [PATCH 318/366] Handle missing monitored users in Plex options (#63411) --- homeassistant/components/plex/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index b2606c6eeaf..82a26cfc237 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -380,6 +380,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow): for user in plex_server.option_monitored_users if plex_server.option_monitored_users[user]["enabled"] } + default_accounts.intersection_update(plex_server.accounts) for user in plex_server.accounts: if user not in known_accounts: available_accounts[user] += " [New]" From 53b3369c2990866309bd600b0cc3f38afb3b7b8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Jan 2022 19:22:28 -1000 Subject: [PATCH 319/366] Handle no enabled ipv4 addresses in the network integration (#63416) --- homeassistant/components/network/__init__.py | 14 ++++++ tests/components/network/test_init.py | 48 ++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 7cc864727d7..b3ef88e7ab2 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -2,8 +2,10 @@ from __future__ import annotations from ipaddress import IPv4Address, IPv6Address, ip_interface +import logging from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -12,6 +14,8 @@ from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP from .models import Adapter from .network import Network, async_get_network +_LOGGER = logging.getLogger(__name__) + @bind_hass async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]: @@ -32,6 +36,16 @@ async def async_get_source_ip( all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s]) source_ip = util.async_get_source_ip(target_ip) + if not all_ipv4s: + _LOGGER.warning( + "Because the system does not have any enabled IPv4 addresses, source address detection may be inaccurate" + ) + if source_ip is None: + raise HomeAssistantError( + "Could not determine source ip because the system does not have any enabled IPv4 addresses and creating a socket failed" + ) + return source_ip + return source_ip if source_ip in all_ipv4s else all_ipv4s[0] diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index 5a6802a14fb..1103c6fa850 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address from unittest.mock import MagicMock, Mock, patch import ifaddr +import pytest from homeassistant.components import network from homeassistant.components.network.const import ( @@ -13,6 +14,7 @@ from homeassistant.components.network.const import ( STORAGE_KEY, STORAGE_VERSION, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component _NO_LOOPBACK_IPADDR = "192.168.1.5" @@ -602,3 +604,49 @@ async def test_async_get_ipv4_broadcast_addresses_multiple(hass, hass_storage): IPv4Address("192.168.1.255"), IPv4Address("169.254.255.255"), } + + +async def test_async_get_source_ip_no_enabled_addresses(hass, hass_storage, caplog): + """Test getting the source ip address when all adapters are disabled.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[], + ), patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket(["192.168.1.5"]), + ): + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" + + assert "source address detection may be inaccurate" in caplog.text + + +async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses( + hass, hass_storage, caplog +): + """Test getting the source ip address when all adapters are disabled and getting it fails.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[], + ), patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([None]), + ): + assert not await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + with pytest.raises(HomeAssistantError): + await network.async_get_source_ip(hass, MDNS_TARGET_IP) From 4d8cf8f14a31627a50048be74cb1a65ff6334ad2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Jan 2022 12:17:54 +0100 Subject: [PATCH 320/366] Bump pychromecast to 10.2.3 (#63429) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b084540bebb..b1a4cd0b358 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.2.2"], + "requirements": ["pychromecast==10.2.3"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 34002e25317..37b484792f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1397,7 +1397,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.2.2 +pychromecast==10.2.3 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccbcfd7d5e2..60471a03fcc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.2.2 +pychromecast==10.2.3 # homeassistant.components.climacell pyclimacell==0.18.2 From 5f3e89d6386592e57d9f22749d68ddab79073651 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jan 2022 01:24:03 -1000 Subject: [PATCH 321/366] Bump flux_led to 0.27.40 to fix SK6812RGBW white level reporting (#63435) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 6703065476e..ca28d014c96 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.32"], + "requirements": ["flux_led==0.27.40"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 37b484792f2..67b07d0b203 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.32 +flux_led==0.27.40 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60471a03fcc..617cca5cbdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.32 +flux_led==0.27.40 # homeassistant.components.homekit fnvhash==0.1.0 From 178b63f3b4bc21e1d347e56e9bf7d1f6e1e34fa3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 Jan 2022 12:21:24 +0100 Subject: [PATCH 322/366] Fix incorrect access to entity registry in Xiaomi Miio (#63446) --- homeassistant/components/xiaomi_miio/number.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 94b3412d44a..eacc39db560 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -7,6 +7,7 @@ from homeassistant.components.number import NumberEntity, NumberEntityDescriptio from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er from .const import ( CONF_DEVICE, @@ -251,7 +252,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for feature, description in NUMBER_TYPES.items(): if feature == FEATURE_SET_LED_BRIGHTNESS and model != MODEL_FAN_ZA5: # Delete LED bightness entity created by mistake if it exists - entity_reg = hass.helpers.entity_registry.async_get() + entity_reg = er.async_get(hass) entity_id = entity_reg.async_get_entity_id( PLATFORM_DOMAIN, DOMAIN, f"{description.key}_{config_entry.unique_id}" ) From 2d7defbdb1b42d9feb584816570739a3bea9ad66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:12:37 -1000 Subject: [PATCH 323/366] Pickup screenlogic codeowner (#61477) - I am using this in production and already doing some work on it --- CODEOWNERS | 2 +- homeassistant/components/screenlogic/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e1dcf3d7dc9..29a22efa45f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -452,7 +452,7 @@ homeassistant/components/samsungtv/* @escoand @chemelli74 homeassistant/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps homeassistant/components/scrape/* @fabaff -homeassistant/components/screenlogic/* @dieselrabbit +homeassistant/components/screenlogic/* @dieselrabbit @bdraco homeassistant/components/script/* @home-assistant/core homeassistant/components/search/* @home-assistant/core homeassistant/components/select/* @home-assistant/core diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index b6134216049..92f9e774183 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", "requirements": ["screenlogicpy==0.5.3"], - "codeowners": ["@dieselrabbit"], + "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ { "hostname": "pentair: *", From 4318bec5d45a24b4e77bd65f8d0865bd3bf7bc1a Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 6 Jan 2022 00:32:55 -0800 Subject: [PATCH 324/366] Bump screenlogicpy (#63533) --- homeassistant/components/screenlogic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 92f9e774183..09313dab0dd 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,7 +3,7 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.5.3"], + "requirements": ["screenlogicpy==0.5.4"], "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 67b07d0b203..c00fffe27a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2113,7 +2113,7 @@ scapy==2.4.5 schiene==0.23 # homeassistant.components.screenlogic -screenlogicpy==0.5.3 +screenlogicpy==0.5.4 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 617cca5cbdb..9fe99876050 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1257,7 +1257,7 @@ samsungtvws==1.6.0 scapy==2.4.5 # homeassistant.components.screenlogic -screenlogicpy==0.5.3 +screenlogicpy==0.5.4 # homeassistant.components.emulated_kasa # homeassistant.components.sense From 4ad77a758c3fdf1b1624d1e341a2a07b8952a43e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 6 Jan 2022 08:26:03 -0600 Subject: [PATCH 325/366] Bump soco to 0.25.3 (#63548) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index d4fce01fd78..b943013d4bc 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.25.2"], + "requirements": ["soco==0.25.3"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index c00fffe27a3..4b8742f899d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2185,7 +2185,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.25.2 +soco==0.25.3 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fe99876050..57cf7d37eaa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1291,7 +1291,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.25.2 +soco==0.25.3 # homeassistant.components.solaredge solaredge==0.0.2 From f487f2ee31accf1c4416ff3bae8806bc0df08963 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jan 2022 08:47:14 -1000 Subject: [PATCH 326/366] Fix KeyError during call to homekit.unpair (#63627) --- homeassistant/components/homekit/__init__.py | 7 ++++++- tests/components/homekit/test_homekit.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index cfa734559fc..6f2f09f3974 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -732,7 +732,12 @@ class HomeKit: """Remove all pairings for an accessory so it can be repaired.""" state = self.driver.state for client_uuid in list(state.paired_clients): - state.remove_paired_client(client_uuid) + # We need to check again since removing a single client + # can result in removing all the clients that the client + # granted access to if it was an admin, otherwise + # remove_paired_client can generate a KeyError + if client_uuid in state.paired_clients: + state.remove_paired_client(client_uuid) self.driver.async_persist() self.driver.async_update_advertisement() self._async_show_setup_message() diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 0b1d2cc8535..bd637572191 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -683,6 +683,11 @@ async def test_homekit_unpair(hass, device_reg, mock_async_zeroconf): state = homekit.driver.state state.add_paired_client("client1", "any", b"1") + state.add_paired_client("client2", "any", b"0") + state.add_paired_client("client3", "any", b"1") + state.add_paired_client("client4", "any", b"0") + state.add_paired_client("client5", "any", b"0") + formatted_mac = device_registry.format_mac(state.mac) hk_bridge_dev = device_reg.async_get_device( {}, {(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)} From 20362867ff07d00cac46800b153b8b47d61e44fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jan 2022 15:46:39 -1000 Subject: [PATCH 327/366] Ensure selected entity is pre-selected in homekit options flow (#63628) * Ensure selected entity is pre-selected in homekit options flow - We recently adjusted the flow to exclude entities that had been deleted from breaking the UI validation. We need to include single entities in the set of all supported entities since accessory mode has no domain filter * tweak * Additional fixes * small tweak to speed up building the set * merged fixed version for test branch --- .../components/homekit/config_flow.py | 25 +++++---- tests/components/homekit/test_config_flow.py | 53 +++++++++++++++++-- 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 0d8bf967c5b..34c62b31d2a 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -446,15 +446,25 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_advanced() entity_filter = self.hk_options.get(CONF_FILTER, {}) + entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) + all_supported_entities = _async_get_matching_entities( self.hass, domains=self.hk_options[CONF_DOMAINS], ) - data_schema = {} - entity_schema = vol.In - entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - if self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_ACCESSORY: + # Strip out entities that no longer exist to prevent error in the UI + valid_entities = [ + entity_id for entity_id in entities if entity_id in all_supported_entities + ] + if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY: + # In accessory mode we can only have one + default_value = valid_entities[0] if valid_entities else None + entity_schema = vol.In + entities_schema_required = vol.Required + else: + # Bridge mode + entities_schema_required = vol.Optional include_exclude_mode = MODE_INCLUDE if not entities: include_exclude_mode = MODE_EXCLUDE @@ -463,13 +473,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): vol.Required(CONF_INCLUDE_EXCLUDE_MODE, default=include_exclude_mode) ] = vol.In(INCLUDE_EXCLUDE_MODES) entity_schema = cv.multi_select + default_value = valid_entities - # Strip out entities that no longer exist to prevent error in the UI - valid_entities = [ - entity_id for entity_id in entities if entity_id in all_supported_entities - ] data_schema[ - vol.Optional(CONF_ENTITIES, default=valid_entities) + entities_schema_required(CONF_ENTITIES, default=default_value) ] = entity_schema(all_supported_entities) return self.async_show_form( diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index f076d8e00ae..d190dec04b8 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -7,6 +7,8 @@ from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.setup import async_setup_component +from .util import PATH_HOMEKIT, async_init_entry + from tests.common import MockConfigEntry @@ -1065,11 +1067,13 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY -async def test_options_flow_include_mode_basic_accessory(hass, mock_get_source_ip): +@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +async def test_options_flow_include_mode_basic_accessory( + port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf +): """Test config flow options in include mode with a single accessory.""" - config_entry = _mock_config_entry_with_options_populated() - config_entry.add_to_hass(hass) + await async_init_entry(hass, config_entry) hass.states.async_set("media_player.tv", "off") hass.states.async_set("media_player.sonos", "off") @@ -1101,7 +1105,48 @@ async def test_options_flow_include_mode_basic_accessory(hass, mock_get_source_i assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "include_exclude" - assert _get_schema_default(result2["data_schema"].schema, "entities") == [] + assert _get_schema_default(result2["data_schema"].schema, "entities") is None + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": "media_player.tv"}, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "accessory", + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["media_player.tv"], + }, + } + + # Now we check again to make sure the single entity is still + # preselected + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": ["media_player"], + "mode": "accessory", + } + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"domains": ["media_player"], "mode": "accessory"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "include_exclude" + assert ( + _get_schema_default(result2["data_schema"].schema, "entities") + == "media_player.tv" + ) result3 = await hass.config_entries.options.async_configure( result2["flow_id"], From 1923f86b75a4b555505163ab7e5e6915ea1c02fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jan 2022 11:43:42 -1000 Subject: [PATCH 328/366] Bump flux_led to 0.27.41 (#63638) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index ca28d014c96..23b754368a0 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.40"], + "requirements": ["flux_led==0.27.41"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 4b8742f899d..f2c357dd7f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.40 +flux_led==0.27.41 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57cf7d37eaa..2666df9ddc0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.40 +flux_led==0.27.41 # homeassistant.components.homekit fnvhash==0.1.0 From b4391fd114f18befded3ee820052b5e320081364 Mon Sep 17 00:00:00 2001 From: Leah Oswald Date: Sat, 8 Jan 2022 21:01:54 +0100 Subject: [PATCH 329/366] Add default mode 'auto' for tradfri starkvind air purifier on turn on (#63641) * add default mode 'auto' for tradfri starkvind air purifier on turn on This commits fixes the behaviour described in #60122 (purifier couldn't be started via toggle switch). It adds 'auto' as default mode/fallback for turning on the starkvind air purifier. This is now the same behaviour the original app provides. * Refactor code that set 'auto' as default value for tradfri starkvind on turn on --- homeassistant/components/tradfri/fan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index 845d5e6d9c3..dab39f598f2 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -142,7 +142,7 @@ class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity): preset_mode: str | None = None, **kwargs: Any, ) -> None: - """Turn on the fan.""" + """Turn on the fan. Auto-mode if no argument is given.""" if not self._device_control: return @@ -150,8 +150,8 @@ class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity): await self._api(self._device_control.set_mode(_from_percentage(percentage))) return - if preset_mode: - await self.async_set_preset_mode(preset_mode) + preset_mode = preset_mode or ATTR_AUTO + await self.async_set_preset_mode(preset_mode) async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" From 79d789c610b9fe30d7dac0bd9b3993087d1f22dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jan 2022 15:45:39 -1000 Subject: [PATCH 330/366] Bump flux_led to 0.27.42 (#63651) - Fixes RGB/WW/CW controller operating mode not being detected after a factory reset. - When the device is factory reset or setup for the first time the operating mode is reported as 0 and its expected the user will set it in the Magic Home app. If they skip that step Home Assistant will never be able to figure out which operating mode the device is using and the CW and WW channels will not work. - Changelog: https://github.com/Danielhiversen/flux_led/compare/0.27.41...0.27.42 --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 23b754368a0..521463a8ec9 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.41"], + "requirements": ["flux_led==0.27.42"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index f2c357dd7f8..98552c6df7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.41 +flux_led==0.27.42 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2666df9ddc0..6a392fd9b5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.41 +flux_led==0.27.42 # homeassistant.components.homekit fnvhash==0.1.0 From ea5b18c1ef16b64cd7916f2540692ab5de2d2edf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 00:25:30 -0600 Subject: [PATCH 331/366] Split august motion and image capture binary sensors (#62154) --- .../components/august/binary_sensor.py | 18 +++++ homeassistant/components/august/camera.py | 3 +- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/test_binary_sensor.py | 66 ++++++++++++++++--- 6 files changed, 80 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index cf34952309b..541b5e94276 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -61,6 +61,17 @@ def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool: return _activity_time_based_state(latest) +def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> bool: + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_IMAGE_CAPTURE} + ) + + if latest is None: + return False + + return _activity_time_based_state(latest) + + def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool: latest = data.activity_stream.get_latest_device_activity( detail.device_id, {ActivityType.DOORBELL_DING} @@ -126,6 +137,13 @@ SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( value_fn=_retrieve_motion_state, is_time_based=True, ), + AugustBinarySensorEntityDescription( + key="doorbell_image_capture", + name="Image Capture", + icon="mdi:file-image", + value_fn=_retrieve_image_capture_state, + is_time_based=True, + ), AugustBinarySensorEntityDescription( key="doorbell_online", name="Online", diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 6f9ecf1b182..6c1f31c4b9c 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -63,7 +63,8 @@ class AugustCamera(AugustEntityMixin, Camera): def _update_from_data(self): """Get the latest state of the sensor.""" doorbell_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, {ActivityType.DOORBELL_MOTION} + self._device_id, + {ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_IMAGE_CAPTURE}, ) if doorbell_activity is not None: diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fc365102926..f89be2915fb 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.13"], + "requirements": ["yalexs==1.1.15"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 98552c6df7a..e920a88c6cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2466,7 +2466,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.13 +yalexs==1.1.15 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a392fd9b5c..1179255c888 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1464,7 +1464,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.13 +yalexs==1.1.15 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 26c824e5842..e2ff4a6771a 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -1,5 +1,6 @@ """The binary_sensor tests for the august platform.""" import datetime +import time from unittest.mock import Mock, patch from yalexs.pubnub_async import AugustPubNub @@ -26,6 +27,10 @@ from tests.components.august.mocks import ( ) +def _timetoken(): + return str(time.time_ns())[:-2] + + async def test_doorsense(hass): """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_lock_from_fixture( @@ -85,6 +90,10 @@ async def test_create_doorbell(hass): "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF binary_sensor_k98gidt45gul_name_online = hass.states.get( "binary_sensor.k98gidt45gul_name_online" ) @@ -97,6 +106,10 @@ async def test_create_doorbell(hass): "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF async def test_create_doorbell_offline(hass): @@ -171,7 +184,7 @@ async def test_doorbell_update_via_pubnub(hass): pubnub, Mock( channel=doorbell_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={ "status": "imagecapture", "data": { @@ -186,10 +199,46 @@ async def test_doorbell_update_via_pubnub(hass): await hass.async_block_till_done() + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_ON + + pubnub.message( + pubnub, + Mock( + channel=doorbell_one.pubsub_channel, + timetoken=_timetoken(), + message={ + "status": "doorbell_motion_detected", + "data": { + "event": "doorbell_motion_detected", + "image": { + "height": 640, + "width": 480, + "format": "jpg", + "created_at": "2021-03-16T02:36:26.886Z", + "bytes": 14061, + "secure_url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", + "url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", + "etag": "09e839331c4ea59eef28081f2caa0e90", + }, + "doorbellName": "Front Door", + "callID": None, + "origin": "mars-api", + "mutableContent": True, + }, + }, + ), + ) + + await hass.async_block_till_done() + binary_sensor_k98gidt45gul_name_motion = hass.states.get( "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON + binary_sensor_k98gidt45gul_name_ding = hass.states.get( "binary_sensor.k98gidt45gul_name_ding" ) @@ -204,16 +253,16 @@ async def test_doorbell_update_via_pubnub(hass): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF pubnub.message( pubnub, Mock( channel=doorbell_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={ "status": "buttonpush", }, @@ -274,7 +323,7 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Unlocking", "doorState": "closed"}, ), ) @@ -289,11 +338,10 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Locking", "doorState": "open"}, ), ) - await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( "binary_sensor.online_with_doorsense_name_open" @@ -327,7 +375,7 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Unlocking", "doorState": "open"}, ), ) From 028169c076df93d20a4b0de003cd38f425f9c8d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Dec 2021 10:08:38 -1000 Subject: [PATCH 332/366] Bump yalexs to 1.1.16 (#62700) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index f89be2915fb..201c9ce89aa 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.15"], + "requirements": ["yalexs==1.1.16"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index e920a88c6cd..ef9292bc3a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2466,7 +2466,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.15 +yalexs==1.1.16 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1179255c888..45f5d057fdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1464,7 +1464,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.15 +yalexs==1.1.16 # homeassistant.components.yeelight yeelight==0.7.8 From 0828eb5bc5e29f034ed2cd78eb92b8de971ef07c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jan 2022 19:27:27 -1000 Subject: [PATCH 333/366] Fix august locks failing to lock/unlock (#63652) --- homeassistant/components/august/__init__.py | 18 +++++ homeassistant/components/august/lock.py | 6 ++ homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/mocks.py | 4 +- tests/components/august/test_lock.py | 81 +++++++++++++++++++ 7 files changed, 110 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 474e69db435..ff6a4f5adb6 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -245,6 +245,15 @@ class AugustData(AugustSubscriberMixin): device_id, ) + async def async_lock_async(self, device_id): + """Lock the device but do not wait for a response since it will come via pubnub.""" + return await self._async_call_api_op_requires_bridge( + device_id, + self._api.async_lock_async, + self._august_gateway.access_token, + device_id, + ) + async def async_unlock(self, device_id): """Unlock the device.""" return await self._async_call_api_op_requires_bridge( @@ -254,6 +263,15 @@ class AugustData(AugustSubscriberMixin): device_id, ) + async def async_unlock_async(self, device_id): + """Unlock the device but do not wait for a response since it will come via pubnub.""" + return await self._async_call_api_op_requires_bridge( + device_id, + self._api.async_unlock_async, + self._august_gateway.access_token, + device_id, + ) + async def _async_call_api_op_requires_bridge( self, device_id, func, *args, **kwargs ): diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 665b0036557..ea977a3c2d0 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -41,10 +41,16 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): async def async_lock(self, **kwargs): """Lock the device.""" + if self._data.activity_stream.pubnub.connected: + await self._data.async_lock_async(self._device_id) + return await self._call_lock_operation(self._data.async_lock) async def async_unlock(self, **kwargs): """Unlock the device.""" + if self._data.activity_stream.pubnub.connected: + await self._data.async_unlock_async(self._device_id) + return await self._call_lock_operation(self._data.async_unlock) async def _call_lock_operation(self, lock_operation): diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 201c9ce89aa..c08f25177cc 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.16"], + "requirements": ["yalexs==1.1.17"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index ef9292bc3a9..7a279e1586d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2466,7 +2466,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.16 +yalexs==1.1.17 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45f5d057fdd..111e12d0b64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1464,7 +1464,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.16 +yalexs==1.1.17 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 13d8f18d0d9..7075eb84d72 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -2,8 +2,6 @@ import json import os import time - -# from unittest.mock import AsyncMock from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from yalexs.activity import ( @@ -207,6 +205,8 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, side_effect=api_call_side_effects["unlock_return_activities"] ) + api_instance.async_unlock_async = AsyncMock() + api_instance.async_lock_async = AsyncMock() api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"}) return await _mock_setup_august(hass, api_instance, pubnub) diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 9d1c34d917a..56f55138e36 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -154,6 +154,86 @@ async def test_one_lock_operation(hass): ) +async def test_one_lock_operation_pubnub_connected(hass): + """Test lock and unlock operations are async when pubnub is connected.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + assert lock_one.pubsub_channel == "pubsub" + + pubnub = AugustPubNub() + await _create_august_with_devices(hass, [lock_one], pubnub=pubnub) + pubnub.connected = True + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 + assert ( + lock_online_with_doorsense_name.attributes.get("friendly_name") + == "online_with_doorsense Name" + ) + + data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} + assert await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True + ) + await hass.async_block_till_done() + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=(dt_util.utcnow().timestamp() + 1) * 10000000, + message={ + "status": "kAugLockState_Unlocked", + }, + ), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + + assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 + assert ( + lock_online_with_doorsense_name.attributes.get("friendly_name") + == "online_with_doorsense Name" + ) + + assert await hass.services.async_call( + LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True + ) + await hass.async_block_till_done() + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000, + message={ + "status": "kAugLockState_Locked", + }, + ), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + # No activity means it will be unavailable until the activity feed has data + entity_registry = er.async_get(hass) + lock_operator_sensor = entity_registry.async_get( + "sensor.online_with_doorsense_name_operator" + ) + assert lock_operator_sensor + assert ( + hass.states.get("sensor.online_with_doorsense_name_operator").state + == STATE_UNKNOWN + ) + + async def test_lock_jammed(hass): """Test lock gets jammed on unlock.""" @@ -273,6 +353,7 @@ async def test_lock_update_via_pubnub(hass): config_entry = await _create_august_with_devices( hass, [lock_one], activities=activities, pubnub=pubnub ) + pubnub.connected = True lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") From 70d21bf3c09ad0cc0c3ab1c0143fabfbbb2a84ed Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Sat, 8 Jan 2022 13:58:31 +0200 Subject: [PATCH 334/366] Switchbot Set initial state for switch (#63654) --- homeassistant/components/switchbot/switch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index a09255d0392..48dbd8d8cac 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -113,6 +113,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): super().__init__(coordinator, idx, mac, name) self._attr_unique_id = idx self._device = device + self._attr_is_on = False async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" @@ -132,6 +133,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): ) if self._last_run_success: self._attr_is_on = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" @@ -143,6 +145,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): ) if self._last_run_success: self._attr_is_on = False + self.async_write_ha_state() @property def assumed_state(self) -> bool: From 1444a3cce0eed413878495bce6cb512946c2f194 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Jan 2022 06:26:28 +0100 Subject: [PATCH 335/366] Fix Tuya climate c_f DP not being a string (#63680) --- homeassistant/components/tuya/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 35e6f5814f3..a56f64a8aad 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -148,8 +148,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): self._attr_temperature_unit = TEMP_CELSIUS if any( - "f" in device.status.get(dpcode, "").lower() + "f" in device.status[dpcode].lower() for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT) + if isinstance(device.status.get(dpcode), str) ): self._attr_temperature_unit = TEMP_FAHRENHEIT From 50d690544ab63151724e324da96a61b9938348fd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 9 Jan 2022 11:27:42 +0100 Subject: [PATCH 336/366] Fix Netgear used method version (#63686) * Netgear fix used method version * add error message * fix imports * fix black * fix error mess * rename const --- .../components/netgear/config_flow.py | 8 +++---- homeassistant/components/netgear/const.py | 24 ++++++++++++++++--- homeassistant/components/netgear/router.py | 8 +++++++ tests/components/netgear/test_config_flow.py | 4 ++-- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 29a0bc40ed6..bcf51f1966d 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -22,8 +22,8 @@ from .const import ( DEFAULT_CONSIDER_HOME, DEFAULT_NAME, DOMAIN, - MODELS_V2, - ORBI_PORT, + MODELS_PORT_80, + PORT_80, ) from .errors import CannotLoginException from .router import get_api @@ -141,13 +141,13 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured(updates=updated_data) updated_data[CONF_PORT] = DEFAULT_PORT - for model in MODELS_V2: + for model in MODELS_PORT_80: if discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith( model ) or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "").startswith( model ): - updated_data[CONF_PORT] = ORBI_PORT + updated_data[CONF_PORT] = PORT_80 self.placeholders.update(updated_data) self.discovered = True diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index cba2d7ff875..81fdf1d59e2 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -10,8 +10,8 @@ CONF_CONSIDER_HOME = "consider_home" DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_NAME = "Netgear router" -# update method V2 models -MODELS_V2 = [ +# models using port 80 instead of 5000 +MODELS_PORT_80 = [ "Orbi", "RBK", "RBR", @@ -29,7 +29,25 @@ MODELS_V2 = [ "SXR", "SXS", ] -ORBI_PORT = 80 +PORT_80 = 80 +# update method V2 models +MODELS_V2 = [ + "Orbi", + "RBK", + "RBR", + "RBS", + "RBW", + "LBK", + "LBR", + "CBK", + "CBR", + "SRC", + "SRK", + "SRS", + "SXK", + "SXR", + "SXS", +] # Icons DEVICE_ICONS = { diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 40e26128d8d..dbfd0439a85 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -149,6 +149,14 @@ class NetgearRouter: if self.model.startswith(model): self.method_version = 2 + if self.method_version == 2: + if not self._api.get_attached_devices_2(): + _LOGGER.error( + "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", + self.model, + ) + self.method_version = 1 + async def async_setup(self) -> None: """Set up a Netgear router.""" await self.hass.async_add_executor_job(self._setup) diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index b26dce8d936..bdb68d79ab2 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components import ssdp -from homeassistant.components.netgear.const import CONF_CONSIDER_HOME, DOMAIN, ORBI_PORT +from homeassistant.components.netgear.const import CONF_CONSIDER_HOME, DOMAIN, PORT_80 from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_HOST, @@ -252,7 +252,7 @@ async def test_ssdp(hass, service): assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST - assert result["data"].get(CONF_PORT) == ORBI_PORT + assert result["data"].get(CONF_PORT) == PORT_80 assert result["data"].get(CONF_SSL) == SSL assert result["data"].get(CONF_USERNAME) == DEFAULT_USER assert result["data"][CONF_PASSWORD] == PASSWORD From 3e02fffdb6f0c2338a1c18e7de8b60e3bc64668d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Jan 2022 18:27:31 -1000 Subject: [PATCH 337/366] Bump flux_led to 0.27.44 to fix CCT ceiling lights (#63712) - Changelog: https://github.com/Danielhiversen/flux_led/compare/0.27.42...0.27.44 - The library was incorrectly speaking the older protocol to these devices when it needed to speak the newer CCT one --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 521463a8ec9..d2c759ea81a 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.42"], + "requirements": ["flux_led==0.27.44"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 7a279e1586d..66ea0c3ff87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.42 +flux_led==0.27.44 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 111e12d0b64..5b7b6119f97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.42 +flux_led==0.27.44 # homeassistant.components.homekit fnvhash==0.1.0 From c7257934ccec918a78f01c78e76f1f6c46977a95 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Mon, 10 Jan 2022 08:19:04 +0200 Subject: [PATCH 338/366] Bump PySwitchbot to 0.13.2 (#63713) * Bump api * gen requirements * Bump api to 0.13.2 to remove print statement. --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 69f9eddc6cd..59415d31c1e 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.13.0"], + "requirements": ["PySwitchbot==0.13.2"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 66ea0c3ff87..e62fa6c4449 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.13.0 +# PySwitchbot==0.13.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b7b6119f97..07977bc0324 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,7 +24,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -# PySwitchbot==0.13.0 +# PySwitchbot==0.13.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From e2c7c7f5822a2827dfd3324c196be846bd695f19 Mon Sep 17 00:00:00 2001 From: Korn <20152583+k-korn@users.noreply.github.com> Date: Sun, 9 Jan 2022 16:29:06 +0200 Subject: [PATCH 339/366] Bump WazeRouteCalculator to 0.14 (#63718) --- homeassistant/components/waze_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 832ec8a12e3..7991cbccbb4 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -2,7 +2,7 @@ "domain": "waze_travel_time", "name": "Waze Travel Time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", - "requirements": ["WazeRouteCalculator==0.13"], + "requirements": ["WazeRouteCalculator==0.14"], "codeowners": [], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index e62fa6c4449..52242fd5de6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -83,7 +83,7 @@ TwitterAPI==2.7.5 WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time -WazeRouteCalculator==0.13 +WazeRouteCalculator==0.14 # homeassistant.components.abode abodepy==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07977bc0324..abc82042af3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ RtmAPI==0.7.2 WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time -WazeRouteCalculator==0.13 +WazeRouteCalculator==0.14 # homeassistant.components.abode abodepy==1.2.0 From 4a3d1bcb233ccc659f62f05270b06f809136cca2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 10 Jan 2022 00:28:36 -0500 Subject: [PATCH 340/366] Fix zwave_js device actions (#63769) --- .../components/zwave_js/device_action.py | 31 +- ...ate_radio_thermostat_ct100_plus_state.json | 123 +++++++- .../components/zwave_js/test_device_action.py | 292 +++++++++++++----- 3 files changed, 360 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 14d64f87eb7..f819a33f1d4 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -14,7 +14,14 @@ from zwave_js_server.util.command_class.meter import get_meter_type from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_DOMAIN, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, +) from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry @@ -227,7 +234,22 @@ async def async_call_action_from_config( if action_type not in ACTION_TYPES: raise HomeAssistantError(f"Unhandled action type {action_type}") - service_data = {k: v for k, v in config.items() if v not in (None, "")} + # Don't include domain, subtype or any null/empty values in the service call + service_data = { + k: v + for k, v in config.items() + if k not in (ATTR_DOMAIN, CONF_SUBTYPE) and v not in (None, "") + } + + # Entity services (including refresh value which is a fake entity service) expects + # just an entity ID + if action_type in ( + SERVICE_REFRESH_VALUE, + SERVICE_SET_LOCK_USERCODE, + SERVICE_CLEAR_LOCK_USERCODE, + SERVICE_RESET_METER, + ): + service_data.pop(ATTR_DEVICE_ID) await hass.services.async_call( DOMAIN, service, service_data, blocking=True, context=context ) @@ -283,7 +305,10 @@ async def async_get_action_capabilities( "extra_fields": vol.Schema( { vol.Required(ATTR_COMMAND_CLASS): vol.In( - {cc.value: cc.name for cc in CommandClass} + { + CommandClass(cc.id).value: cc.name + for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return] + } ), vol.Required(ATTR_PROPERTY): cv.string, vol.Optional(ATTR_PROPERTY_KEY): cv.string, diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json index 34df415301e..cd5a6bd4abe 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json @@ -57,7 +57,128 @@ }, { "nodeId": 13, "index": 2 } ], - "commandClasses": [], + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 65bc8e4bddb..0980b414a09 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -1,4 +1,6 @@ """The tests for Z-Wave JS device actions.""" +from unittest.mock import patch + import pytest import voluptuous_serialize from zwave_js_server.client import Client @@ -14,7 +16,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component -from tests.common import async_get_device_automations, async_mock_service +from tests.common import async_get_device_automations async def test_get_actions( @@ -87,8 +89,130 @@ async def test_get_actions_meter( assert len(filtered_actions) > 0 -async def test_action(hass: HomeAssistant) -> None: - """Test for turn_on and turn_off actions.""" +async def test_actions( + hass: HomeAssistant, + client: Client, + climate_radio_thermostat_ct100_plus: Node, + integration: ConfigEntry, +) -> None: + """Test actions.""" + node = climate_radio_thermostat_ct100_plus + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_refresh_value", + }, + "action": { + "domain": DOMAIN, + "type": "refresh_value", + "device_id": device.id, + "entity_id": "climate.z_wave_thermostat", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_ping", + }, + "action": { + "domain": DOMAIN, + "type": "ping", + "device_id": device.id, + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_value", + }, + "action": { + "domain": DOMAIN, + "type": "set_value", + "device_id": device.id, + "command_class": 112, + "property": 1, + "value": 1, + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_config_parameter", + }, + "action": { + "domain": DOMAIN, + "type": "set_config_parameter", + "device_id": device.id, + "parameter": 1, + "bitmask": None, + "subtype": "2-112-0-3 (Beeper)", + "value": 1, + }, + }, + ] + }, + ) + + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + with patch("zwave_js_server.model.node.Node.async_ping") as mock_call: + hass.bus.async_fire("test_event_ping") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 0 + + with patch("zwave_js_server.model.node.Node.async_set_value") as mock_call: + hass.bus.async_fire("test_event_set_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0] == "13-112-0-1" + assert args[1] == 1 + + with patch( + "homeassistant.components.zwave_js.services.async_set_config_parameter" + ) as mock_call: + hass.bus.async_fire("test_event_set_config_parameter") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 3 + assert args[0].node_id == 13 + assert args[1] == 1 + assert args[2] == 1 + + +async def test_lock_actions( + hass: HomeAssistant, + client: Client, + lock_schlage_be469: Node, + integration: ConfigEntry, +) -> None: + """Test actions for locks.""" + node = lock_schlage_be469 + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device + assert await async_setup_component( hass, automation.DOMAIN, @@ -102,7 +226,7 @@ async def test_action(hass: HomeAssistant) -> None: "action": { "domain": DOMAIN, "type": "clear_lock_usercode", - "device_id": "fake", + "device_id": device.id, "entity_id": "lock.touchscreen_deadbolt", "code_slot": 1, }, @@ -115,97 +239,80 @@ async def test_action(hass: HomeAssistant) -> None: "action": { "domain": DOMAIN, "type": "set_lock_usercode", - "device_id": "fake", + "device_id": device.id, "entity_id": "lock.touchscreen_deadbolt", "code_slot": 1, "usercode": "1234", }, }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_refresh_value", - }, - "action": { - "domain": DOMAIN, - "type": "refresh_value", - "device_id": "fake", - "entity_id": "lock.touchscreen_deadbolt", - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_ping", - }, - "action": { - "domain": DOMAIN, - "type": "ping", - "device_id": "fake", - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_set_value", - }, - "action": { - "domain": DOMAIN, - "type": "set_value", - "device_id": "fake", - "command_class": 112, - "property": "test", - "value": 1, - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_set_config_parameter", - }, - "action": { - "domain": DOMAIN, - "type": "set_config_parameter", - "device_id": "fake", - "parameter": 3, - "bitmask": None, - "subtype": "2-112-0-3 (Beeper)", - "value": 255, - }, - }, ] }, ) - clear_lock_usercode = async_mock_service(hass, "zwave_js", "clear_lock_usercode") - hass.bus.async_fire("test_event_clear_lock_usercode") - await hass.async_block_till_done() - assert len(clear_lock_usercode) == 1 + with patch("homeassistant.components.zwave_js.lock.clear_usercode") as mock_call: + hass.bus.async_fire("test_event_clear_lock_usercode") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0].node_id == node.node_id + assert args[1] == 1 - set_lock_usercode = async_mock_service(hass, "zwave_js", "set_lock_usercode") - hass.bus.async_fire("test_event_set_lock_usercode") - await hass.async_block_till_done() - assert len(set_lock_usercode) == 1 + with patch("homeassistant.components.zwave_js.lock.set_usercode") as mock_call: + hass.bus.async_fire("test_event_set_lock_usercode") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 3 + assert args[0].node_id == node.node_id + assert args[1] == 1 + assert args[2] == "1234" - refresh_value = async_mock_service(hass, "zwave_js", "refresh_value") - hass.bus.async_fire("test_event_refresh_value") - await hass.async_block_till_done() - assert len(refresh_value) == 1 - ping = async_mock_service(hass, "zwave_js", "ping") - hass.bus.async_fire("test_event_ping") - await hass.async_block_till_done() - assert len(ping) == 1 +async def test_reset_meter_action( + hass: HomeAssistant, + client: Client, + aeon_smart_switch_6: Node, + integration: ConfigEntry, +) -> None: + """Test reset_meter action.""" + node = aeon_smart_switch_6 + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device - set_value = async_mock_service(hass, "zwave_js", "set_value") - hass.bus.async_fire("test_event_set_value") - await hass.async_block_till_done() - assert len(set_value) == 1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_reset_meter", + }, + "action": { + "domain": DOMAIN, + "type": "reset_meter", + "device_id": device.id, + "entity_id": "sensor.smart_switch_6_electric_consumed_kwh", + }, + }, + ] + }, + ) - set_config_parameter = async_mock_service(hass, "zwave_js", "set_config_parameter") - hass.bus.async_fire("test_event_set_config_parameter") - await hass.async_block_till_done() - assert len(set_config_parameter) == 1 + with patch( + "zwave_js_server.model.endpoint.Endpoint.async_invoke_cc_api" + ) as mock_call: + hass.bus.async_fire("test_event_reset_meter") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0] == CommandClass.METER + assert args[1] == "reset" async def test_get_action_capabilities( @@ -261,7 +368,28 @@ async def test_get_action_capabilities( ) assert capabilities and "extra_fields" in capabilities - cc_options = [(cc.value, cc.name) for cc in CommandClass] + cc_options = [ + (133, "Association"), + (89, "Association Group Information"), + (128, "Battery"), + (129, "Clock"), + (112, "Configuration"), + (90, "Device Reset Locally"), + (122, "Firmware Update Meta Data"), + (135, "Indicator"), + (114, "Manufacturer Specific"), + (96, "Multi Channel"), + (142, "Multi Channel Association"), + (49, "Multilevel Sensor"), + (115, "Powerlevel"), + (68, "Thermostat Fan Mode"), + (69, "Thermostat Fan State"), + (64, "Thermostat Mode"), + (66, "Thermostat Operating State"), + (67, "Thermostat Setpoint"), + (134, "Version"), + (94, "Z-Wave Plus Info"), + ] assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer From e0ba71e6cbbeecaf148982c476e832a3ca756f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 10 Jan 2022 14:49:25 +0100 Subject: [PATCH 341/366] Add client metadata to cloud register (#63794) --- homeassistant/components/cloud/http_api.py | 18 ++++++- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_http_api.py | 57 +++++++++++++++++--- 6 files changed, 72 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index cd682057266..2fbb1c447ae 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -21,6 +21,7 @@ from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.websocket_api import const as ws_const +from homeassistant.util.location import async_detect_location_info from .const import ( DOMAIN, @@ -220,8 +221,23 @@ class CloudRegisterView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] + client_metadata = None + + if location_info := await async_detect_location_info( + hass.helpers.aiohttp_client.async_get_clientsession() + ): + client_metadata = { + "NC_COUNTRY_CODE": location_info.country_code, + "NC_REGION_CODE": location_info.region_code, + "NC_ZIP_CODE": location_info.zip_code, + } + async with async_timeout.timeout(REQUEST_TIMEOUT): - await cloud.auth.async_register(data["email"], data["password"]) + await cloud.auth.async_register( + data["email"], + data["password"], + client_metadata=client_metadata, + ) return self.json_message("ok") diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 517aa887a30..0bb00cd5ced 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.50.0"], + "requirements": ["hass-nabucasa==0.51.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e90fdf9db12..4753e819ead 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 -hass-nabucasa==0.50.0 +hass-nabucasa==0.51.0 home-assistant-frontend==20211229.0 httpx==0.21.0 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 52242fd5de6..3815daa18fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -787,7 +787,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.50.0 +hass-nabucasa==0.51.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abc82042af3..c2250c84479 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.50.0 +hass-nabucasa==0.51.0 # homeassistant.components.tasmota hatasmota==0.3.1 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 42a498528ce..ac8cd49802e 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -15,6 +15,7 @@ from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.cloud.const import DOMAIN, RequireRelink from homeassistant.components.google_assistant.helpers import GoogleEntity from homeassistant.core import State +from homeassistant.util.location import LocationInfo from . import mock_cloud, mock_cloud_prefs @@ -203,16 +204,60 @@ async def test_logout_view_unknown_error(hass, cloud_client): assert req.status == HTTPStatus.BAD_GATEWAY -async def test_register_view(mock_cognito, cloud_client): - """Test logging out.""" - req = await cloud_client.post( - "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} - ) +async def test_register_view_no_location(mock_cognito, cloud_client): + """Test register without location.""" + with patch( + "homeassistant.components.cloud.http_api.async_detect_location_info", + return_value=None, + ): + req = await cloud_client.post( + "/api/cloud/register", + json={"email": "hello@bla.com", "password": "falcon42"}, + ) assert req.status == HTTPStatus.OK assert len(mock_cognito.register.mock_calls) == 1 - result_email, result_pass = mock_cognito.register.mock_calls[0][1] + call = mock_cognito.register.mock_calls[0] + result_email, result_pass = call.args assert result_email == "hello@bla.com" assert result_pass == "falcon42" + assert call.kwargs["client_metadata"] is None + + +async def test_register_view_with_location(mock_cognito, cloud_client): + """Test register with location.""" + with patch( + "homeassistant.components.cloud.http_api.async_detect_location_info", + return_value=LocationInfo( + **{ + "country_code": "XX", + "zip_code": "12345", + "region_code": "GH", + "ip": "1.2.3.4", + "city": "Gotham", + "region_name": "Gotham", + "time_zone": "Earth/Gotham", + "currency": "XXX", + "latitude": "12.34567", + "longitude": "12.34567", + "use_metric": True, + } + ), + ): + req = await cloud_client.post( + "/api/cloud/register", + json={"email": "hello@bla.com", "password": "falcon42"}, + ) + assert req.status == HTTPStatus.OK + assert len(mock_cognito.register.mock_calls) == 1 + call = mock_cognito.register.mock_calls[0] + result_email, result_pass = call.args + assert result_email == "hello@bla.com" + assert result_pass == "falcon42" + assert call.kwargs["client_metadata"] == { + "NC_COUNTRY_CODE": "XX", + "NC_REGION_CODE": "GH", + "NC_ZIP_CODE": "12345", + } async def test_register_view_bad_data(mock_cognito, cloud_client): From 50bd5d62aa747cdc7001772b210ffc2e00088e60 Mon Sep 17 00:00:00 2001 From: Knodd Date: Mon, 10 Jan 2022 15:21:03 +0100 Subject: [PATCH 342/366] Support Tuya strip lights with correct values for saturation and brightness (#63812) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/light.py | 48 ++++++++++++++------------ 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 0669dee86c4..c6640378de5 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -30,6 +30,28 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, WorkMode from .util import remap_value +@dataclass +class ColorTypeData: + """Color Type Data.""" + + h_type: IntegerTypeData + s_type: IntegerTypeData + v_type: IntegerTypeData + + +DEFAULT_COLOR_TYPE_DATA = ColorTypeData( + h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), + s_type=IntegerTypeData(min=1, scale=0, max=255, step=1), + v_type=IntegerTypeData(min=1, scale=0, max=255, step=1), +) + +DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData( + h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), + s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), + v_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), +) + + @dataclass class TuyaLightEntityDescription(LightEntityDescription): """Describe an Tuya light entity.""" @@ -40,6 +62,7 @@ class TuyaLightEntityDescription(LightEntityDescription): color_data: DPCode | tuple[DPCode, ...] | None = None color_mode: DPCode | None = None color_temp: DPCode | tuple[DPCode, ...] | None = None + default_color_type: ColorTypeData = DEFAULT_COLOR_TYPE_DATA LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { @@ -63,6 +86,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, color_data=DPCode.COLOUR_DATA, + default_color_type=DEFAULT_COLOR_TYPE_DATA_V2, ), ), # Light @@ -242,28 +266,6 @@ LIGHTS["cz"] = LIGHTS["kg"] LIGHTS["pc"] = LIGHTS["kg"] -@dataclass -class ColorTypeData: - """Color Type Data.""" - - h_type: IntegerTypeData - s_type: IntegerTypeData - v_type: IntegerTypeData - - -DEFAULT_COLOR_TYPE_DATA = ColorTypeData( - h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), - s_type=IntegerTypeData(min=1, scale=0, max=255, step=1), - v_type=IntegerTypeData(min=1, scale=0, max=255, step=1), -) - -DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData( - h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), - s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), - v_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), -) - - @dataclass class ColorData: """Color Data.""" @@ -443,7 +445,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): ) else: # If no type is found, use a default one - self._color_data_type = DEFAULT_COLOR_TYPE_DATA + self._color_data_type = self.entity_description.default_color_type if self._color_data_dpcode == DPCode.COLOUR_DATA_V2 or ( self._brightness_type and self._brightness_type.max > 255 ): From f6d21a04e0711e46ea9a83913591bc1df7a9f4e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jan 2022 13:40:45 -1000 Subject: [PATCH 343/366] Bump flux_led to 0.27.45 to fix missing controls on ZJ21410 models (#63854) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index d2c759ea81a..e1815b1d145 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.44"], + "requirements": ["flux_led==0.27.45"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3815daa18fd..0acd5f12565 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.44 +flux_led==0.27.45 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2250c84479..7d7c714e5a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.44 +flux_led==0.27.45 # homeassistant.components.homekit fnvhash==0.1.0 From ab4aa5dda43da46cd1f20decb6f8ac9254c0f9da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jan 2022 13:40:57 -1000 Subject: [PATCH 344/366] Bump aioharmony to 0.2.9 (#63858) --- homeassistant/components/harmony/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index d1b1073ebad..94817890160 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -2,7 +2,7 @@ "domain": "harmony", "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", - "requirements": ["aioharmony==0.2.8"], + "requirements": ["aioharmony==0.2.9"], "codeowners": [ "@ehendrix23", "@bramkragten", diff --git a/requirements_all.txt b/requirements_all.txt index 0acd5f12565..154d4e882d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -177,7 +177,7 @@ aiogithubapi==21.11.0 aioguardian==2021.11.0 # homeassistant.components.harmony -aioharmony==0.2.8 +aioharmony==0.2.9 # homeassistant.components.homekit_controller aiohomekit==0.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d7c714e5a7..29c08f517ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -121,7 +121,7 @@ aioflo==2021.11.0 aioguardian==2021.11.0 # homeassistant.components.harmony -aioharmony==0.2.8 +aioharmony==0.2.9 # homeassistant.components.homekit_controller aiohomekit==0.6.4 From a4f717cb5a0b722b4b670eb3381367d60e735a7a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Jan 2022 16:13:08 -0800 Subject: [PATCH 345/366] Bump frontend to 20211229.1 (#63866) --- 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 c1d833ac169..b1f44c79414 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==20211229.0" + "home-assistant-frontend==20211229.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4753e819ead..1152007c417 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.51.0 -home-assistant-frontend==20211229.0 +home-assistant-frontend==20211229.1 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 154d4e882d3..41c1908f511 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -820,7 +820,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211229.0 +home-assistant-frontend==20211229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29c08f517ec..8beca5013ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211229.0 +home-assistant-frontend==20211229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 25b07b07d464cf2c6965d401674ec06afe7bb48e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Jan 2022 16:22:00 -0800 Subject: [PATCH 346/366] Bumped version to 2021.12.9 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 45104f0d81b..16ee024024c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "8" +PATCH_VERSION: Final = "9" __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) From cb89c23c0ffd7beba1ecc0cb84d80e8842f9a571 Mon Sep 17 00:00:00 2001 From: kpine Date: Mon, 27 Dec 2021 03:31:31 -0800 Subject: [PATCH 347/366] Avoid removing zwave_js devices for non-ready nodes (#59964) * Only replace a node if the mfgr id / prod id / prod type differ * Prefer original device name for unready node * move register_node_in_dev_reg into async_setup_entry * simplify get_device_id_ext * Don't need hex ids * Revert "move register_node_in_dev_reg into async_setup_entry" This reverts commit f900e5fb0c67cc81657a1452b51c313bccb6f9e1. * Revert Callable change * Revert device backup name * Add test fixtures * Update existing not ready test with new fixture data * Check device properties after node added event * Add entity check * Check for extended device id * better device info checks * Use receive_event to properly setup components * Cleanup tests * improve test_replace_different_node * improve test_replace_same_node * add test test_node_model_change * Clean up long comments and strings * Format * Reload integration to detect node device config changes * update assertions * Disable entities on "value removed" event * Disable node status sensor on node replacement * Add test for disabling entities on remove value event * Add test for disabling node status sensor on node replacement * disable entity -> remove entity Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 42 +- homeassistant/components/zwave_js/entity.py | 26 +- homeassistant/components/zwave_js/helpers.py | 13 + homeassistant/components/zwave_js/sensor.py | 7 + tests/components/zwave_js/conftest.py | 34 + .../fixtures/zp3111-5_not_ready_state.json | 68 ++ .../zwave_js/fixtures/zp3111-5_state.json | 706 ++++++++++++++++++ tests/components/zwave_js/test_init.py | 692 ++++++++++++++--- 8 files changed, 1486 insertions(+), 102 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json create mode 100644 tests/components/zwave_js/fixtures/zp3111-5_state.json diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 7d2af4af126..10088f62414 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -88,7 +88,12 @@ from .discovery import ( async_discover_node_values, async_discover_single_value, ) -from .helpers import async_enable_statistics, get_device_id, get_unique_id +from .helpers import ( + async_enable_statistics, + get_device_id, + get_device_id_ext, + get_unique_id, +) from .migrate import async_migrate_discovered_value from .services import ZWaveServices @@ -116,17 +121,27 @@ def register_node_in_dev_reg( ) -> device_registry.DeviceEntry: """Register node in dev reg.""" device_id = get_device_id(client, node) - # If a device already exists but it doesn't match the new node, it means the node - # was replaced with a different device and the device needs to be removeed so the - # new device can be created. Otherwise if the device exists and the node is the same, - # the node was replaced with the same device model and we can reuse the device. - if (device := dev_reg.async_get_device({device_id})) and ( - device.model != node.device_config.label - or device.manufacturer != node.device_config.manufacturer + device_id_ext = get_device_id_ext(client, node) + device = dev_reg.async_get_device({device_id}) + + # Replace the device if it can be determined that this node is not the + # same product as it was previously. + if ( + device_id_ext + and device + and len(device.identifiers) == 2 + and device_id_ext not in device.identifiers ): remove_device_func(device) + device = None + + if device_id_ext: + ids = {device_id, device_id_ext} + else: + ids = {device_id} + params = { - ATTR_IDENTIFIERS: {device_id}, + ATTR_IDENTIFIERS: ids, ATTR_SW_VERSION: node.firmware_version, ATTR_NAME: node.name or node.device_config.description @@ -338,7 +353,14 @@ async def async_setup_entry( # noqa: C901 device = dev_reg.async_get_device({dev_id}) # We assert because we know the device exists assert device - if not replaced: + if replaced: + discovered_value_ids.pop(device.id, None) + + async_dispatcher_send( + hass, + f"{DOMAIN}_{client.driver.controller.home_id}.{node.node_id}.node_status_remove_entity", + ) + else: remove_device(device) @callback diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index cf15f32932b..87e9f3adbbd 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -20,6 +20,7 @@ from .migrate import async_add_migration_entity_value LOGGER = logging.getLogger(__name__) EVENT_VALUE_UPDATED = "value updated" +EVENT_VALUE_REMOVED = "value removed" EVENT_DEAD = "dead" EVENT_ALIVE = "alive" @@ -99,6 +100,10 @@ class ZWaveBaseEntity(Entity): self.async_on_remove( self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) ) + self.async_on_remove( + self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed) + ) + for status_event in (EVENT_ALIVE, EVENT_DEAD): self.async_on_remove( self.info.node.on(status_event, self._node_status_alive_or_dead) @@ -171,7 +176,7 @@ class ZWaveBaseEntity(Entity): @callback def _value_changed(self, event_data: dict) -> None: - """Call when (one of) our watched values changes. + """Call when a value associated with our node changes. Should not be overridden by subclasses. """ @@ -193,6 +198,25 @@ class ZWaveBaseEntity(Entity): self.on_value_update() self.async_write_ha_state() + @callback + def _value_removed(self, event_data: dict) -> None: + """Call when a value associated with our node is removed. + + Should not be overridden by subclasses. + """ + value_id = event_data["value"].value_id + + if value_id != self.info.primary_value.value_id: + return + + LOGGER.debug( + "[%s] Primary value %s is being removed", + self.entity_id, + value_id, + ) + + self.hass.async_create_task(self.async_remove()) + @callback def get_zwave_value( self, diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index aa6db532616..363762ac72b 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -66,6 +66,19 @@ def get_device_id(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str]: return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") +@callback +def get_device_id_ext(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str] | None: + """Get extended device registry identifier for Z-Wave node.""" + if None in (node.manufacturer_id, node.product_type, node.product_id): + return None + + domain, dev_id = get_device_id(client, node) + return ( + domain, + f"{dev_id}-{node.manufacturer_id}:{node.product_type}:{node.product_id}", + ) + + @callback def get_home_and_node_id_from_device_id(device_id: tuple[str, ...]) -> list[str]: """ diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 549df9f6264..70528c28427 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -520,4 +520,11 @@ class ZWaveNodeStatusSensor(SensorEntity): self.async_poll_value, ) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.unique_id}_remove_entity", + self.async_remove, + ) + ) self.async_write_ha_state() diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e2db9fe7a6b..4ae1d509c69 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -473,6 +473,24 @@ def fortrezz_ssa1_siren_state_fixture(): return json.loads(load_fixture("zwave_js/fortrezz_ssa1_siren_state.json")) +@pytest.fixture(name="fortrezz_ssa3_siren_state", scope="session") +def fortrezz_ssa3_siren_state_fixture(): + """Load the fortrezz ssa3 siren node state fixture data.""" + return json.loads(load_fixture("zwave_js/fortrezz_ssa3_siren_state.json")) + + +@pytest.fixture(name="zp3111_not_ready_state", scope="session") +def zp3111_not_ready_state_fixture(): + """Load the zp3111 4-in-1 sensor not-ready node state fixture data.""" + return json.loads(load_fixture("zwave_js/zp3111-5_not_ready_state.json")) + + +@pytest.fixture(name="zp3111_state", scope="session") +def zp3111_state_fixture(): + """Load the zp3111 4-in-1 sensor node state fixture data.""" + return json.loads(load_fixture("zwave_js/zp3111-5_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -905,3 +923,19 @@ def fortrezz_ssa1_siren_fixture(client, fortrezz_ssa1_siren_state): def firmware_file_fixture(): """Return mock firmware file stream.""" return io.BytesIO(bytes(10)) + + +@pytest.fixture(name="zp3111_not_ready") +def zp3111_not_ready_fixture(client, zp3111_not_ready_state): + """Mock a zp3111 4-in-1 sensor node in a not-ready state.""" + node = Node(client, copy.deepcopy(zp3111_not_ready_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="zp3111") +def zp3111_fixture(client, zp3111_state): + """Mock a zp3111 4-in-1 sensor node.""" + node = Node(client, copy.deepcopy(zp3111_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json new file mode 100644 index 00000000000..f892eb5570e --- /dev/null +++ b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json @@ -0,0 +1,68 @@ +{ + "nodeId": 22, + "index": 0, + "status": 1, + "ready": false, + "isListening": false, + "isRouting": true, + "isSecure": "unknown", + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 22, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + } + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [ + 40000, + 100000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [], + "interviewStage": "ProtocolInfo", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + } +} diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json new file mode 100644 index 00000000000..8de7dd2b713 --- /dev/null +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -0,0 +1,706 @@ +{ + "nodeId": 22, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "status": 2, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": false, + "manufacturerId": 265, + "productId": 8449, + "productType": 8225, + "firmwareVersion": "5.1", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/cache/db/devices/0x0109/zp3111-5.json", + "isEmbedded": true, + "manufacturer": "Vision Security", + "manufacturerId": 265, + "label": "ZP3111-5", + "description": "4-in-1 Sensor", + "devices": [ + { + "productType": 8225, + "productId": 8449 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave primary controller into inclusion mode. Press the Program Switch of ZP3111 for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, otherwise, ZP3111 will go to sleep after 20 seconds.", + "exclusion": "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave primary controller into “exclusion” mode, and following its instruction to delete the ZP3111 to the controller. Press the Program Switch of ZP3111 once to be excluded.", + "reset": "Remove cover to trigged tamper switch, LED flash once & send out Alarm Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send the “Device Reset Locally Notification” command and reset to the factory default. (Remark: This is to be used only in the case of primary controller being inoperable or otherwise unavailable.)", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf" + } + }, + "label": "ZP3111-5", + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 22, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.5" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "5.1", + "10.1" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 265 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 8225 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 8449 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Cover status", + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Cover status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "3": "Tampering, product cover removed" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Motion sensor status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "8": "Motion detection" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "°C" + }, + "value": 21.98 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Illuminance", + "propertyName": "Illuminance", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Illuminance", + "ccSpecific": { + "sensorType": 3, + "scale": 0 + }, + "unit": "%" + }, + "value": 7.31 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Humidity", + "propertyName": "Humidity", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + }, + "unit": "%" + }, + "value": 51.98 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Temperature Scale", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Scale", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Celsius", + "1": "Fahrenheit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Temperature offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature offset", + "default": 1, + "min": 0, + "max": 50, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Humidity", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Configure Relative Humidity", + "label": "Humidity", + "default": 10, + "min": 1, + "max": 50, + "unit": "percent", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Light Sensor", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Sensor", + "default": 10, + "min": 1, + "max": 50, + "unit": "percent", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Trigger Interval", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Set the trigger interval for motion sensor re-activation.", + "label": "Trigger Interval", + "default": 180, + "min": 1, + "max": 255, + "unit": "seconds", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Motion Sensor Sensitivity", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Adjust sensitivity of the motion sensor.", + "label": "Motion Sensor Sensitivity", + "default": 4, + "min": 1, + "max": 7, + "states": { + "1": "highest", + "2": "higher", + "3": "high", + "4": "normal", + "5": "low", + "6": "lower", + "7": "lowest" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "LED indicator mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LED indicator mode", + "default": 3, + "min": 1, + "max": 3, + "states": { + "1": "Off", + "2": "Pulsing Temperature, Flashing Motion", + "3": "Flashing Temperature and Motion" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "ccVersion": 2, + "metadata": { + "type": "number", + "default": 3600, + "readable": false, + "writeable": true, + "label": "Wake Up interval", + "min": 600, + "max": 604800, + "steps": 600 + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [ + 40000, + 100000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 6, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 7, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0109:0x2021:0x2101:5.1", + "statistics": { + "commandsTX": 39, + "commandsRX": 38, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "highestSecurityClass": -1 +} diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index b2cb7bc808e..ab29dfde23f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -12,7 +12,11 @@ from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import DISABLED_USER, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY @@ -159,7 +163,7 @@ async def test_new_entity_on_value_added(hass, multisensor_6, client, integratio async def test_on_node_added_ready(hass, multisensor_6_state, client, integration): - """Test we handle a ready node added event.""" + """Test we handle a node added event with a ready node.""" dev_reg = dr.async_get(hass) node = Node(client, deepcopy(multisensor_6_state)) event = {"node": node} @@ -182,38 +186,34 @@ async def test_on_node_added_ready(hass, multisensor_6_state, client, integratio assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) -async def test_on_node_added_not_ready(hass, multisensor_6_state, client, integration): - """Test we handle a non ready node added event.""" +async def test_on_node_added_not_ready( + hass, zp3111_not_ready_state, client, integration +): + """Test we handle a node added event with a non-ready node.""" dev_reg = dr.async_get(hass) - node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests. - node = Node(client, node_data) - node.data["ready"] = False - event = {"node": node} - air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + device_id = f"{client.driver.controller.home_id}-{zp3111_not_ready_state['nodeId']}" - state = hass.states.get(AIR_TEMPERATURE_SENSOR) + assert len(hass.states.async_all()) == 0 + assert not dev_reg.devices - assert not state # entity and device not yet added - assert not dev_reg.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} + event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": deepcopy(zp3111_not_ready_state), + }, ) - - client.driver.controller.emit("node added", event) + client.driver.receive_event(event) await hass.async_block_till_done() - state = hass.states.get(AIR_TEMPERATURE_SENSOR) + # the only entity is the node status sensor + assert len(hass.states.async_all()) == 1 - assert not state # entity not yet added but device added in registry - assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) - - node.data["ready"] = True - node.emit("ready", event) - await hass.async_block_till_done() - - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert state # entity added - assert state.state != STATE_UNAVAILABLE + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + # no extended device identifier yet + assert len(device.identifiers) == 1 async def test_existing_node_ready(hass, client, multisensor_6, integration): @@ -221,12 +221,157 @@ async def test_existing_node_ready(hass, client, multisensor_6, integration): dev_reg = dr.async_get(hass) node = multisensor_6 air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + air_temperature_device_id_ext = ( + f"{air_temperature_device_id}-{node.manufacturer_id}:" + f"{node.product_type}:{node.product_id}" + ) state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert state # entity and device added assert state.state != STATE_UNAVAILABLE - assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) + + device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) + assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id_ext)} + ) + + +async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integration): + """Test we handle a non-ready node that exists during integration setup.""" + dev_reg = dr.async_get(hass) + node = zp3111_not_ready + device_id = f"{client.driver.controller.home_id}-{node.node_id}" + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device.name == f"Node {node.node_id}" + assert not device.manufacturer + assert not device.model + assert not device.sw_version + + # the only entity is the node status sensor + assert len(hass.states.async_all()) == 1 + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + # no extended device identifier yet + assert len(device.identifiers) == 1 + + +async def test_existing_node_not_replaced_when_not_ready( + hass, zp3111, zp3111_not_ready_state, zp3111_state, client, integration +): + """Test when a node added event with a non-ready node is received. + + The existing node should not be replaced, and no customization should be lost. + """ + dev_reg = dr.async_get(hass) + er_reg = er.async_get(hass) + kitchen_area = ar.async_get(hass).async_create("Kitchen") + + device_id = f"{client.driver.controller.home_id}-{zp3111.node_id}" + device_id_ext = ( + f"{device_id}-{zp3111.manufacturer_id}:" + f"{zp3111.product_type}:{zp3111.product_id}" + ) + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device.name == "4-in-1 Sensor" + assert not device.name_by_user + assert device.manufacturer == "Vision Security" + assert device.model == "ZP3111-5" + assert device.sw_version == "5.1" + assert not device.area_id + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + + motion_entity = "binary_sensor.4_in_1_sensor_home_security_motion_detection" + state = hass.states.get(motion_entity) + assert state + assert state.name == "4-in-1 Sensor: Home Security - Motion detection" + + dev_reg.async_update_device( + device.id, name_by_user="Custom Device Name", area_id=kitchen_area.id + ) + + custom_device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert custom_device + assert custom_device.name == "4-in-1 Sensor" + assert custom_device.name_by_user == "Custom Device Name" + assert custom_device.manufacturer == "Vision Security" + assert custom_device.model == "ZP3111-5" + assert device.sw_version == "5.1" + assert custom_device.area_id == kitchen_area.id + assert custom_device == dev_reg.async_get_device( + identifiers={(DOMAIN, device_id_ext)} + ) + + custom_entity = "binary_sensor.custom_motion_sensor" + er_reg.async_update_entity( + motion_entity, new_entity_id=custom_entity, name="Custom Entity Name" + ) + await hass.async_block_till_done() + state = hass.states.get(custom_entity) + assert state + assert state.name == "Custom Entity Name" + assert not hass.states.get(motion_entity) + + event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": deepcopy(zp3111_not_ready_state), + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + assert device.id == custom_device.id + assert device.identifiers == custom_device.identifiers + assert device.name == f"Node {zp3111.node_id}" + assert device.name_by_user == "Custom Device Name" + assert not device.manufacturer + assert not device.model + assert not device.sw_version + assert device.area_id == kitchen_area.id + + state = hass.states.get(custom_entity) + assert state + assert state.name == "Custom Entity Name" + + event = Event( + type="ready", + data={ + "source": "node", + "event": "ready", + "nodeId": zp3111_state["nodeId"], + "nodeState": deepcopy(zp3111_state), + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + assert device.id == custom_device.id + assert device.identifiers == custom_device.identifiers + assert device.name == "4-in-1 Sensor" + assert device.name_by_user == "Custom Device Name" + assert device.manufacturer == "Vision Security" + assert device.model == "ZP3111-5" + assert device.area_id == kitchen_area.id + assert device.sw_version == "5.1" + + state = hass.states.get(custom_entity) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.name == "Custom Entity Name" async def test_null_name(hass, client, null_name_check, integration): @@ -235,38 +380,6 @@ async def test_null_name(hass, client, null_name_check, integration): assert hass.states.get(f"switch.node_{node.node_id}") -async def test_existing_node_not_ready(hass, client, multisensor_6): - """Test we handle a non ready node that exists during integration setup.""" - dev_reg = dr.async_get(hass) - node = multisensor_6 - node.data = deepcopy(node.data) # Copy to allow modification in tests. - node.data["ready"] = False - event = {"node": node} - air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" - entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert not state # entity not yet added - assert dev_reg.async_get_device( # device should be added - identifiers={(DOMAIN, air_temperature_device_id)} - ) - - node.data["ready"] = True - node.emit("ready", event) - await hass.async_block_till_done() - - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert state # entity and device added - assert state.state != STATE_UNAVAILABLE - assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) - - async def test_start_addon( hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon ): @@ -738,63 +851,460 @@ async def test_node_removed(hass, multisensor_6_state, client, integration): assert not dev_reg.async_get(old_device.id) -async def test_replace_same_node(hass, multisensor_6_state, client, integration): +async def test_replace_same_node( + hass, multisensor_6, multisensor_6_state, client, integration +): """Test when a node is replaced with itself that the device remains.""" dev_reg = dr.async_get(hass) - node = Node(client, deepcopy(multisensor_6_state)) - device_id = f"{client.driver.controller.home_id}-{node.node_id}" - event = {"node": node} + node_id = multisensor_6.node_id + multisensor_6_state = deepcopy(multisensor_6_state) - client.driver.controller.emit("node added", event) + device_id = f"{client.driver.controller.home_id}-{node_id}" + multisensor_6_device_id = ( + f"{device_id}-{multisensor_6.manufacturer_id}:" + f"{multisensor_6.product_type}:{multisensor_6.product_id}" + ) + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, multisensor_6_device_id)} + ) + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + dev_id = device.id + + assert hass.states.get(AIR_TEMPERATURE_SENSOR) + + # A replace node event has the extra field "replaced" set to True + # to distinguish it from an exclusion + event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "replaced": True, + "node": multisensor_6_state, + }, + ) + client.driver.receive_event(event) await hass.async_block_till_done() - old_device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) - assert old_device.id - event = {"node": node, "replaced": True} + # Device should still be there after the node was removed + device = dev_reg.async_get(dev_id) + assert device - client.driver.controller.emit("node removed", event) + # When the node is replaced, a non-ready node added event is emitted + event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": { + "nodeId": node_id, + "index": 0, + "status": 4, + "ready": False, + "isSecure": "unknown", + "interviewAttempts": 1, + "endpoints": [{"nodeId": node_id, "index": 0, "deviceClass": None}], + "values": [], + "deviceClass": None, + "commandClasses": [], + "interviewStage": "None", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + }, + }, + }, + ) + + # Device is still not removed + client.driver.receive_event(event) await hass.async_block_till_done() - # Assert device has remained - assert dev_reg.async_get(old_device.id) - event = {"node": node} + device = dev_reg.async_get(dev_id) + assert device - client.driver.controller.emit("node added", event) + event = Event( + type="ready", + data={ + "source": "node", + "event": "ready", + "nodeId": node_id, + "nodeState": multisensor_6_state, + }, + ) + client.driver.receive_event(event) await hass.async_block_till_done() - # Assert device has remained - assert dev_reg.async_get(old_device.id) + + # Device is the same + device = dev_reg.async_get(dev_id) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, multisensor_6_device_id)} + ) + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + + assert hass.states.get(AIR_TEMPERATURE_SENSOR) async def test_replace_different_node( - hass, multisensor_6_state, hank_binary_switch_state, client, integration + hass, + multisensor_6, + multisensor_6_state, + hank_binary_switch_state, + client, + integration, ): """Test when a node is replaced with a different node.""" - hank_binary_switch_state = deepcopy(hank_binary_switch_state) - multisensor_6_state = deepcopy(multisensor_6_state) - hank_binary_switch_state["nodeId"] = multisensor_6_state["nodeId"] dev_reg = dr.async_get(hass) - old_node = Node(client, multisensor_6_state) - device_id = f"{client.driver.controller.home_id}-{old_node.node_id}" - new_node = Node(client, hank_binary_switch_state) - event = {"node": old_node} + node_id = multisensor_6.node_id + hank_binary_switch_state = deepcopy(hank_binary_switch_state) + hank_binary_switch_state["nodeId"] = node_id + + device_id = f"{client.driver.controller.home_id}-{node_id}" + multisensor_6_device_id = ( + f"{device_id}-{multisensor_6.manufacturer_id}:" + f"{multisensor_6.product_type}:{multisensor_6.product_id}" + ) + hank_device_id = ( + f"{device_id}-{hank_binary_switch_state['manufacturerId']}:" + f"{hank_binary_switch_state['productType']}:" + f"{hank_binary_switch_state['productId']}" + ) - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, multisensor_6_device_id)} + ) + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + dev_id = device.id - event = {"node": old_node, "replaced": True} + assert hass.states.get(AIR_TEMPERATURE_SENSOR) - client.driver.controller.emit("node removed", event) + # A replace node event has the extra field "replaced" set to True + # to distinguish it from an exclusion + event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "replaced": True, + "node": multisensor_6_state, + }, + ) + client.driver.receive_event(event) await hass.async_block_till_done() + # Device should still be there after the node was removed + device = dev_reg.async_get(dev_id) assert device - event = {"node": new_node} + # When the node is replaced, a non-ready node added event is emitted + event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": { + "nodeId": multisensor_6.node_id, + "index": 0, + "status": 4, + "ready": False, + "isSecure": "unknown", + "interviewAttempts": 1, + "endpoints": [ + {"nodeId": multisensor_6.node_id, "index": 0, "deviceClass": None} + ], + "values": [], + "deviceClass": None, + "commandClasses": [], + "interviewStage": "None", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + }, + }, + }, + ) - client.driver.controller.emit("node added", event) + # Device is still not removed + client.driver.receive_event(event) await hass.async_block_till_done() - device = dev_reg.async_get(device.id) - # assert device is new + + device = dev_reg.async_get(dev_id) assert device + + event = Event( + type="ready", + data={ + "source": "node", + "event": "ready", + "nodeId": node_id, + "nodeState": hank_binary_switch_state, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + # Old device and entities were removed, but the ID is re-used + device = dev_reg.async_get(dev_id) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, hank_device_id)}) + assert not dev_reg.async_get_device(identifiers={(DOMAIN, multisensor_6_device_id)}) assert device.manufacturer == "HANK Electronics Ltd." + assert device.model == "HKZW-SO01" + + assert not hass.states.get(AIR_TEMPERATURE_SENSOR) + assert hass.states.get("switch.smart_plug_with_two_usb_ports") + + +async def test_node_model_change(hass, zp3111, client, integration): + """Test when a node's model is changed due to an updated device config file. + + The device and entities should not be removed. + """ + dev_reg = dr.async_get(hass) + er_reg = er.async_get(hass) + + device_id = f"{client.driver.controller.home_id}-{zp3111.node_id}" + device_id_ext = ( + f"{device_id}-{zp3111.manufacturer_id}:" + f"{zp3111.product_type}:{zp3111.product_id}" + ) + + # Verify device and entities have default names/ids + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + assert device.manufacturer == "Vision Security" + assert device.model == "ZP3111-5" + assert device.name == "4-in-1 Sensor" + assert not device.name_by_user + + dev_id = device.id + + motion_entity = "binary_sensor.4_in_1_sensor_home_security_motion_detection" + state = hass.states.get(motion_entity) + assert state + assert state.name == "4-in-1 Sensor: Home Security - Motion detection" + + # Customize device and entity names/ids + dev_reg.async_update_device(device.id, name_by_user="Custom Device Name") + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device.id == dev_id + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + assert device.manufacturer == "Vision Security" + assert device.model == "ZP3111-5" + assert device.name == "4-in-1 Sensor" + assert device.name_by_user == "Custom Device Name" + + custom_entity = "binary_sensor.custom_motion_sensor" + er_reg.async_update_entity( + motion_entity, new_entity_id=custom_entity, name="Custom Entity Name" + ) + await hass.async_block_till_done() + assert not hass.states.get(motion_entity) + state = hass.states.get(custom_entity) + assert state + assert state.name == "Custom Entity Name" + + # Unload the integration + assert await hass.config_entries.async_unload(integration.entry_id) + await hass.async_block_till_done() + assert integration.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + # Simulate changes to the node labels + zp3111.device_config.data["description"] = "New Device Name" + zp3111.device_config.data["label"] = "New Device Model" + zp3111.device_config.data["manufacturer"] = "New Device Manufacturer" + + # Reload integration, it will re-add the nodes + integration.add_to_hass(hass) + await hass.config_entries.async_setup(integration.entry_id) + await hass.async_block_till_done() + + # Device name changes, but the customization is the same + device = dev_reg.async_get(dev_id) + assert device + assert device.id == dev_id + assert device.manufacturer == "New Device Manufacturer" + assert device.model == "New Device Model" + assert device.name == "New Device Name" + assert device.name_by_user == "Custom Device Name" + + assert not hass.states.get(motion_entity) + state = hass.states.get(custom_entity) + assert state + assert state.name == "Custom Entity Name" + + +async def test_disabled_node_status_entity_on_node_replaced( + hass, zp3111_state, zp3111, client, integration +): + """Test that when a node replacement event is received the node status sensor is removed.""" + node_status_entity = "sensor.4_in_1_sensor_node_status" + state = hass.states.get(node_status_entity) + assert state + assert state.state != STATE_UNAVAILABLE + + event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "replaced": True, + "node": zp3111_state, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(node_status_entity) + assert state + assert state.state == STATE_UNAVAILABLE + + +async def test_disabled_entity_on_value_removed(hass, zp3111, client, integration): + """Test that when entity primary values are removed the entity is removed.""" + er_reg = er.async_get(hass) + + # re-enable this default-disabled entity + sensor_cover_entity = "sensor.4_in_1_sensor_home_security_cover_status" + er_reg.async_update_entity(entity_id=sensor_cover_entity, disabled_by=None) + await hass.async_block_till_done() + + # must reload the integration when enabling an entity + await hass.config_entries.async_unload(integration.entry_id) + await hass.async_block_till_done() + assert integration.state is ConfigEntryState.NOT_LOADED + integration.add_to_hass(hass) + await hass.config_entries.async_setup(integration.entry_id) + await hass.async_block_till_done() + assert integration.state is ConfigEntryState.LOADED + + state = hass.states.get(sensor_cover_entity) + assert state + assert state.state != STATE_UNAVAILABLE + + # check for expected entities + binary_cover_entity = ( + "binary_sensor.4_in_1_sensor_home_security_tampering_product_cover_removed" + ) + state = hass.states.get(binary_cover_entity) + assert state + assert state.state != STATE_UNAVAILABLE + + battery_level_entity = "sensor.4_in_1_sensor_battery_level" + state = hass.states.get(battery_level_entity) + assert state + assert state.state != STATE_UNAVAILABLE + + unavailable_entities = { + state.entity_id + for state in hass.states.async_all() + if state.state == STATE_UNAVAILABLE + } + + # This value ID removal does not remove any entity + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": zp3111.node_id, + "args": { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "wakeUpInterval", + "prevValue": 3600, + "propertyName": "wakeUpInterval", + }, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + assert all(state != STATE_UNAVAILABLE for state in hass.states.async_all()) + + # This value ID removal only affects the battery level entity + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": zp3111.node_id, + "args": { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "prevValue": 100, + "propertyName": "level", + }, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(battery_level_entity) + assert state + assert state.state == STATE_UNAVAILABLE + + # This value ID removal affects its multiple notification sensors + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": zp3111.node_id, + "args": { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Cover status", + "prevValue": 0, + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + }, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(binary_cover_entity) + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get(sensor_cover_entity) + assert state + assert state.state == STATE_UNAVAILABLE + + # existing entities and the entities with removed values should be unavailable + new_unavailable_entities = { + state.entity_id + for state in hass.states.async_all() + if state.state == STATE_UNAVAILABLE + } + assert ( + unavailable_entities + | {battery_level_entity, binary_cover_entity, sensor_cover_entity} + == new_unavailable_entities + ) From df2b6fd5d47a12891b6562b552b6a72a19f118e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jan 2022 13:06:09 -1000 Subject: [PATCH 348/366] Convert siri requests for target heating cooling state auto to a valid mode (#60220) --- .../components/homekit/manifest.json | 2 +- .../components/homekit/type_thermostats.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit/test_type_thermostats.py | 24 +++++++++++++++++++ 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index d23aa11b4ea..4b54468e092 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.3.0", + "HAP-python==4.4.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 804f0b86167..5f925e2b01d 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -174,6 +174,7 @@ class Thermostat(HomeAccessory): self.char_target_heat_cool.override_properties( valid_values=self.hc_hass_to_homekit ) + self.char_target_heat_cool.allow_invalid_client_values = True # Current and target temperature characteristics self.char_current_temp = serv_thermostat.configure_char( @@ -252,7 +253,6 @@ class Thermostat(HomeAccessory): hvac_mode = state.state homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode] - # Homekit will reset the mode when VIEWING the temp # Ignore it if its the same mode if ( @@ -282,7 +282,7 @@ class Thermostat(HomeAccessory): target_hc, hc_fallback, ) - target_hc = hc_fallback + self.char_target_heat_cool.value = target_hc = hc_fallback break params[ATTR_HVAC_MODE] = self.hc_homekit_to_hass[target_hc] diff --git a/requirements_all.txt b/requirements_all.txt index 41c1908f511..ad1b5b55650 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.3.0 +HAP-python==4.4.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8beca5013ab..d6f998a9c34 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.3.0 +HAP-python==4.4.0 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 20ed225552c..a11aa9d6cb7 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1266,6 +1266,7 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): await hass.async_block_till_done() hap = acc.char_target_heat_cool.to_HAP() assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_HEAT] + assert acc.char_target_heat_cool.allow_invalid_client_values is True assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT acc.char_target_heat_cool.set_value(HC_HEAT_COOL_HEAT) @@ -1303,6 +1304,29 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + acc.char_target_heat_cool.client_update_value(HC_HEAT_COOL_OFF) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_OFF + hass.states.async_set( + entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + ) + await hass.async_block_till_done() + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver): """Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to cool.""" From 7320904f48c2b71527d2a226341360cf282a24ab Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 11 Jan 2022 23:36:46 +1100 Subject: [PATCH 349/366] dlna_dmr: Allow for upnp attributes with None contents (#63892) --- homeassistant/components/dlna_dmr/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 69437d99e3d..c1d5369f474 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -474,8 +474,8 @@ def _is_ignored_device(discovery_info: ssdp.SsdpServiceInfo) -> bool: # Special cases for devices with other discovery methods (e.g. mDNS), or # that advertise multiple unrelated (sent in separate discovery packets) # UPnP devices. - manufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER, "").lower() - model = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "").lower() + manufacturer = (discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER) or "").lower() + model = (discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "").lower() if manufacturer.startswith("xbmc") or model == "kodi": # kodi From 2fb1e406cfcf0db79ad3e46493d83e137824438f Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 12 Jan 2022 05:50:32 +0100 Subject: [PATCH 350/366] Convert disabled_by to DeviceEntryDisabler on load (#63944) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/device_registry.py | 4 +++- tests/helpers/test_device_registry.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index e31b77d3ae2..eb812f15b48 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -584,7 +584,9 @@ class DeviceRegistry: configuration_url=device["configuration_url"], # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] - disabled_by=device["disabled_by"], + disabled_by=DeviceEntryDisabler(device["disabled_by"]) + if device["disabled_by"] + else None, entry_type=DeviceEntryType(device["entry_type"]) if device["entry_type"] else None, diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 455c90b8f65..ca58c014c75 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -640,6 +640,7 @@ async def test_loading_saving_data(hass, registry, area_registry): identifiers={("hue", "abc")}, manufacturer="manufacturer", model="light", + entry_type=device_registry.DeviceEntryType.SERVICE, ) assert orig_light4.id == orig_light3.id @@ -679,6 +680,15 @@ async def test_loading_saving_data(hass, registry, area_registry): assert orig_light == new_light assert orig_light4 == new_light4 + # Ensure enums converted + for (old, new) in ( + (orig_via, new_via), + (orig_light, new_light), + (orig_light4, new_light4), + ): + assert old.disabled_by is new.disabled_by + assert old.entry_type is new.entry_type + # Ensure a save/load cycle does not keep suggested area new_kitchen_light = registry2.async_get_device({("hue", "999")}) assert orig_kitchen_light.suggested_area == "Kitchen" From ba961b7fddadaa22c834eeb783c56485dd3ca100 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jan 2022 12:56:24 -1000 Subject: [PATCH 351/366] Fix homekit options being mutated during config_flow/migration (#64003) --- homeassistant/components/homekit/__init__.py | 5 +++-- homeassistant/components/homekit/config_flow.py | 8 +++++--- tests/components/homekit/test_config_flow.py | 11 ++++++++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 6f2f09f3974..473705494fd 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from copy import deepcopy import ipaddress import logging import os @@ -352,8 +353,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry): @callback def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): - options = dict(entry.options) - data = dict(entry.data) + options = deepcopy(dict(entry.options)) + data = deepcopy(dict(entry.data)) modified = False for importable_option in CONFIG_OPTIONS: if importable_option not in entry.options and importable_option in entry.data: diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 34c62b31d2a..303682f3335 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -2,9 +2,11 @@ from __future__ import annotations import asyncio +from copy import deepcopy import random import re import string +from typing import Final import voluptuous as vol @@ -116,7 +118,7 @@ DEFAULT_DOMAINS = [ "water_heater", ] -_EMPTY_ENTITY_FILTER = { +_EMPTY_ENTITY_FILTER: Final = { CONF_INCLUDE_DOMAINS: [], CONF_EXCLUDE_DOMAINS: [], CONF_INCLUDE_ENTITIES: [], @@ -151,7 +153,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Choose specific domains in bridge mode.""" if user_input is not None: - entity_filter = _EMPTY_ENTITY_FILTER.copy() + entity_filter = deepcopy(_EMPTY_ENTITY_FILTER) entity_filter[CONF_INCLUDE_DOMAINS] = user_input[CONF_INCLUDE_DOMAINS] self.hk_data[CONF_FILTER] = entity_filter return await self.async_step_pairing() @@ -492,7 +494,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.hk_options.update(user_input) return await self.async_step_include_exclude() - self.hk_options = dict(self.config_entry.options) + self.hk_options = deepcopy(dict(self.config_entry.options)) entity_filter = self.hk_options.get(CONF_FILTER, {}) homekit_mode = self.hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) domains = entity_filter.get(CONF_INCLUDE_DOMAINS, []) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index d190dec04b8..ffd223d1d2a 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -2,9 +2,14 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow -from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME +from homeassistant.components.homekit.const import ( + CONF_FILTER, + DOMAIN, + SHORT_BRIDGE_NAME, +) from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS from homeassistant.setup import async_setup_component from .util import PATH_HOMEKIT, async_init_entry @@ -347,6 +352,10 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "include_exclude" + # Inject garbage to ensure the options data + # is being deep copied and we cannot mutate it in flight + config_entry.options[CONF_FILTER][CONF_INCLUDE_DOMAINS].append("garbage") + result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.old"], "include_exclude_mode": "exclude"}, From 7bcf3e81d2d57c0aadab6aa3f3df3be640f57f30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jan 2022 16:12:30 -1000 Subject: [PATCH 352/366] Bump nexia to 0.9.13 to fix setting emergency heat (#64020) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 624eee41db7..f605b32528e 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==0.9.12"], + "requirements": ["nexia==0.9.13"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index ad1b5b55650..96a9d71f469 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1066,7 +1066,7 @@ nettigo-air-monitor==1.2.1 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.12 +nexia==0.9.13 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6f998a9c34..7846809270c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -654,7 +654,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.1 # homeassistant.components.nexia -nexia==0.9.12 +nexia==0.9.13 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.3 From 7a886efec50d82f603746dbb368de4d6327e7d81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jan 2022 13:43:39 -1000 Subject: [PATCH 353/366] Bump flux_led to 0.28.3 to fix setting colorloop on older models (#64094) Fixes #64087 --- .../components/flux_led/manifest.json | 65 +++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index e1815b1d145..682a4fb5f68 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,42 +3,41 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.45"], + "requirements": ["flux_led==0.28.3"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", "dhcp": [ - { - "macaddress": "18B905*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "249494*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "7CB94C*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "B4E842*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "F0FE6B*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "8CCE4E*", - "hostname": "lwip*" - }, - { - "hostname": "zengge_[0-9a-f][0-9a-f]_*" - }, - { - "macaddress": "C82E47*", - "hostname": "sta*" - } + { + "macaddress": "18B905*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "249494*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "7CB94C*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "B4E842*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "F0FE6B*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "8CCE4E*", + "hostname": "lwip*" + }, + { + "hostname": "zengge_[0-9a-f][0-9a-f]_*" + }, + { + "macaddress": "C82E47*", + "hostname": "sta*" + } ] } - diff --git a/requirements_all.txt b/requirements_all.txt index 96a9d71f469..ab05bdc3cdc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.45 +flux_led==0.28.3 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7846809270c..694b772d519 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.45 +flux_led==0.28.3 # homeassistant.components.homekit fnvhash==0.1.0 From 405c4f685cc99b597ed1b0f24db192a5048c62a3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 14 Jan 2022 17:24:54 +0100 Subject: [PATCH 354/366] Fix changing the preset mode (#64119) --- homeassistant/components/shelly/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 777a41a6664..21d90adb74b 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -281,10 +281,10 @@ class BlockSleepingClimate( async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" - if not self._attr_preset_modes: + if not self._preset_modes: return - preset_index = self._attr_preset_modes.index(preset_mode) + preset_index = self._preset_modes.index(preset_mode) if preset_index == 0: await self.set_state_full_path(schedule=0) From 4f09439c8ca9dfb1a225f60ef78cba7b8f25782f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jan 2022 13:14:02 -1000 Subject: [PATCH 355/366] Fix flux_led turn on when brightness is zero on newer devices (#64129) --- homeassistant/components/flux_led/light.py | 42 ++-- homeassistant/components/flux_led/util.py | 23 ++ tests/components/flux_led/test_light.py | 265 ++++++++++++++++++++- 3 files changed, 308 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index d364d8b9581..4c93c678ad9 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -71,7 +71,14 @@ from .const import ( TRANSITION_STROBE, ) from .entity import FluxOnOffEntity -from .util import _effect_brightness, _flux_color_mode_to_hass, _hass_color_modes +from .util import ( + _effect_brightness, + _flux_color_mode_to_hass, + _hass_color_modes, + _min_rgb_brightness, + _min_rgbw_brightness, + _min_rgbwc_brightness, +) _LOGGER = logging.getLogger(__name__) @@ -244,7 +251,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return self._device.brightness + return self._device.brightness # type: ignore @property def color_temp(self) -> int: @@ -254,17 +261,17 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def rgb_color(self) -> tuple[int, int, int]: """Return the rgb color value.""" - return self._device.rgb_unscaled + return self._device.rgb_unscaled # type: ignore @property def rgbw_color(self) -> tuple[int, int, int, int]: """Return the rgbw color value.""" - return self._device.rgbw + return self._device.rgbw # type: ignore @property def rgbww_color(self) -> tuple[int, int, int, int, int]: """Return the rgbww aka rgbcw color value.""" - return self._device.rgbcw + return self._device.rgbcw # type: ignore @property def color_mode(self) -> str: @@ -276,7 +283,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def effect(self) -> str | None: """Return the current effect.""" - return self._device.effect + return self._device.effect # type: ignore async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" @@ -313,13 +320,13 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): """Determine brightness from kwargs or current value.""" if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: brightness = self.brightness - if not brightness: - # If the brightness was previously 0, the light - # will not turn on unless brightness is at least 1 - # If the device was on and brightness was not - # set, it means it was masked by an effect - brightness = 255 if self.is_on else 1 - return brightness + # If the brightness was previously 0, the light + # will not turn on unless brightness is at least 1 + # + # We previously had a problem with the brightness + # sometimes reporting as 0 when an effect was in progress, + # however this has since been resolved in the upstream library + return max(1, brightness) async def _async_set_mode(self, **kwargs: Any) -> None: """Set an effect or color mode.""" @@ -348,6 +355,8 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): return # Handle switch to RGB Color Mode if rgb := kwargs.get(ATTR_RGB_COLOR): + if not self._device.requires_turn_on: + rgb = _min_rgb_brightness(rgb) red, green, blue = rgb await self._device.async_set_levels(red, green, blue, brightness=brightness) return @@ -355,13 +364,18 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): if rgbw := kwargs.get(ATTR_RGBW_COLOR): if ATTR_BRIGHTNESS in kwargs: rgbw = rgbw_brightness(rgbw, brightness) + if not self._device.requires_turn_on: + rgbw = _min_rgbw_brightness(rgbw) await self._device.async_set_levels(*rgbw) return # Handle switch to RGBWW Color Mode if rgbcw := kwargs.get(ATTR_RGBWW_COLOR): if ATTR_BRIGHTNESS in kwargs: rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness) - await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw)) + rgbwc = rgbcw_to_rgbwc(rgbcw) + if not self._device.requires_turn_on: + rgbwc = _min_rgbwc_brightness(rgbwc) + await self._device.async_set_levels(*rgbwc) return if (white := kwargs.get(ATTR_WHITE)) is not None: await self._device.async_set_levels(w=white) diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 774ae1aaa53..1376e78b019 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -34,3 +34,26 @@ def _flux_color_mode_to_hass( def _effect_brightness(brightness: int) -> int: """Convert hass brightness to effect brightness.""" return round(brightness / 255 * 100) + + +def _min_rgb_brightness(rgb: tuple[int, int, int]) -> tuple[int, int, int]: + """Ensure the RGB value will not turn off the device from a turn on command.""" + if all(byte == 0 for byte in rgb): + return (1, 1, 1) + return rgb + + +def _min_rgbw_brightness(rgbw: tuple[int, int, int, int]) -> tuple[int, int, int, int]: + """Ensure the RGBW value will not turn off the device from a turn on command.""" + if all(byte == 0 for byte in rgbw): + return (1, 1, 1, 0) + return rgbw + + +def _min_rgbwc_brightness( + rgbwc: tuple[int, int, int, int, int] +) -> tuple[int, int, int, int, int]: + """Ensure the RGBWC value will not turn off the device from a turn on command.""" + if all(byte == 0 for byte in rgbwc): + return (1, 1, 1, 0, 0) + return rgbwc diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 92ea0fd8d39..a719d297378 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -39,6 +39,9 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( @@ -247,9 +250,11 @@ async def test_rgb_light(hass: HomeAssistant) -> None: blocking=True, ) # If the bulb is on and we are using existing brightness - # and brightness was 0 it means we could not read it because - # an effect is in progress so we use 255 - bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + # and brightness was 0 older devices will not be able to turn on + # so we need to make sure its at least 1 and that we + # call it before the turn on command since the device + # does not support auto on + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) bulb.async_set_levels.reset_mock() bulb.brightness = 128 @@ -304,9 +309,9 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "rgb" + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGB assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGB] assert attributes[ATTR_HS_COLOR] == (0, 100) await hass.services.async_call( @@ -331,6 +336,19 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: bulb.async_set_levels.reset_mock() bulb.async_turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (0, 0, 0)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(1, 1, 1, brightness=1) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + # Should still be called with no kwargs await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -357,10 +375,11 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: blocking=True, ) # If the bulb is on and we are using existing brightness - # and brightness was 0 it means we could not read it because - # an effect is in progress so we use 255 + # and brightness was 0 we need to set it to at least 1 + # or the device may not turn on bulb.async_turn_on.assert_not_called() - bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + bulb.async_set_brightness.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) bulb.async_set_levels.reset_mock() bulb.brightness = 128 @@ -395,6 +414,236 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: bulb.async_set_effect.reset_mock() +async def test_rgbw_light_auto_on(hass: HomeAssistant) -> None: + """Test an rgbw light that does not need the turn on command sent.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.requires_turn_on = False + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_RGBW} + bulb.color_mode = FLUX_COLOR_MODE_RGBW + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGBW + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGBW] + assert attributes[ATTR_HS_COLOR] == (0.0, 83.529) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + + await async_mock_device_turn_off(hass, bulb) + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (10, 10, 30, 0)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, 0) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + + # Should still be called with no kwargs + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON + bulb.async_turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (0, 0, 0, 0)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 we need to set it to at least 1 + # or the device may not turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_not_called() + bulb.async_set_levels.assert_called_with(1, 1, 1, 0) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(110, 19, 0, 255) + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) + bulb.async_set_effect.reset_mock() + + +async def test_rgbww_light_auto_on(hass: HomeAssistant) -> None: + """Test an rgbww light that does not need the turn on command sent.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.requires_turn_on = False + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_RGBWW} + bulb.color_mode = FLUX_COLOR_MODE_RGBWW + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGBWW + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGBWW] + assert attributes[ATTR_HS_COLOR] == (3.237, 94.51) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + + await async_mock_device_turn_off(hass, bulb) + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (10, 10, 30, 0, 0)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, 0, 0) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + + # Should still be called with no kwargs + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON + bulb.async_turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (0, 0, 0, 0, 0)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 we need to set it to at least 1 + # or the device may not turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_not_called() + bulb.async_set_levels.assert_called_with(1, 1, 1, 0, 0) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(14, 0, 30, 255, 255) + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) + bulb.async_set_effect.reset_mock() + + async def test_rgb_cct_light(hass: HomeAssistant) -> None: """Test an rgb cct light.""" config_entry = MockConfigEntry( From 9f281e026f0e2eb861e002625ab111bc435f4fb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jan 2022 07:26:47 -1000 Subject: [PATCH 356/366] Ensure august status is current when integration loads (#64027) --- homeassistant/components/august/__init__.py | 14 ++++++++++++++ homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/mocks.py | 12 ++++++++++-- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index ff6a4f5adb6..e8b4a15b65a 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -138,6 +138,11 @@ class AugustData(AugustSubscriberMixin): pubnub.subscribe(self.async_pubnub_message) self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub) + if self._locks_by_id: + await asyncio.gather( + *[self.async_status_async(lock_id) for lock_id in self._locks_by_id] + ) + @callback def async_pubnub_message(self, device_id, date_time, message): """Process a pubnub message.""" @@ -245,6 +250,15 @@ class AugustData(AugustSubscriberMixin): device_id, ) + async def async_status_async(self, device_id): + """Request status of the the device but do not wait for a response since it will come via pubnub.""" + return await self._async_call_api_op_requires_bridge( + device_id, + self._api.async_status_async, + self._august_gateway.access_token, + device_id, + ) + async def async_lock_async(self, device_id): """Lock the device but do not wait for a response since it will come via pubnub.""" return await self._async_call_api_op_requires_bridge( diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index c08f25177cc..15b31edcce4 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.17"], + "requirements": ["yalexs==1.1.18"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index ab05bdc3cdc..89285ae67f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2466,7 +2466,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.17 +yalexs==1.1.18 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 694b772d519..2e62c9f62d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1464,7 +1464,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.17 +yalexs==1.1.18 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 7075eb84d72..2d572b886f3 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -162,10 +162,17 @@ async def _create_august_with_devices( # noqa: C901 "unlock_return_activities" ] = unlock_return_activities_side_effect - return await _mock_setup_august_with_api_side_effects( + api_instance, entry = await _mock_setup_august_with_api_side_effects( hass, api_call_side_effects, pubnub ) + if device_data["locks"]: + # Ensure we sync status when the integration is loaded if there + # are any locks + assert api_instance.async_status_async.mock_calls + + return entry + async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, pubnub): api_instance = MagicMock(name="Api") @@ -207,9 +214,10 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, api_instance.async_unlock_async = AsyncMock() api_instance.async_lock_async = AsyncMock() + api_instance.async_status_async = AsyncMock() api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"}) - return await _mock_setup_august(hass, api_instance, pubnub) + return api_instance, await _mock_setup_august(hass, api_instance, pubnub) def _mock_august_authentication(token_text, token_timestamp, state): From 8f842c780782c4ee23a03aaa239f2591b0b07d2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jan 2022 13:13:30 -1000 Subject: [PATCH 357/366] Fix august lock/unlock with older bridges (#64143) --- homeassistant/components/august/__init__.py | 6 ++++-- homeassistant/components/august/lock.py | 9 +++++++-- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index e8b4a15b65a..53ea583050b 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -259,13 +259,14 @@ class AugustData(AugustSubscriberMixin): device_id, ) - async def async_lock_async(self, device_id): + async def async_lock_async(self, device_id, hyper_bridge): """Lock the device but do not wait for a response since it will come via pubnub.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_lock_async, self._august_gateway.access_token, device_id, + hyper_bridge, ) async def async_unlock(self, device_id): @@ -277,13 +278,14 @@ class AugustData(AugustSubscriberMixin): device_id, ) - async def async_unlock_async(self, device_id): + async def async_unlock_async(self, device_id, hyper_bridge): """Unlock the device but do not wait for a response since it will come via pubnub.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_unlock_async, self._august_gateway.access_token, device_id, + hyper_bridge, ) async def _async_call_api_op_requires_bridge( diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index ea977a3c2d0..3c5af4c94c9 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -39,17 +39,22 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): self._attr_unique_id = f"{self._device_id:s}_lock" self._update_from_data() + @property + def _hyper_bridge(self): + """Check if the lock has a paired hyper bridge.""" + return bool(self._detail.bridge and self._detail.bridge.hyper_bridge) + async def async_lock(self, **kwargs): """Lock the device.""" if self._data.activity_stream.pubnub.connected: - await self._data.async_lock_async(self._device_id) + await self._data.async_lock_async(self._device_id, self._hyper_bridge) return await self._call_lock_operation(self._data.async_lock) async def async_unlock(self, **kwargs): """Unlock the device.""" if self._data.activity_stream.pubnub.connected: - await self._data.async_unlock_async(self._device_id) + await self._data.async_unlock_async(self._device_id, self._hyper_bridge) return await self._call_lock_operation(self._data.async_unlock) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 15b31edcce4..db537287b05 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.18"], + "requirements": ["yalexs==1.1.19"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 89285ae67f0..86e575f4cb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2466,7 +2466,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.18 +yalexs==1.1.19 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e62c9f62d9..9827fb3f7b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1464,7 +1464,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.18 +yalexs==1.1.19 # homeassistant.components.yeelight yeelight==0.7.8 From da5b4735da4021ed03205999eb9534b087e58890 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 16 Jan 2022 07:22:18 -0500 Subject: [PATCH 358/366] Ignore unavailable entities when creating zwave_js device actions list (#64184) * Fix bug with zwave-js device actions * outdent * Add test and fix bug * fix --- .../components/zwave_js/device_action.py | 18 +++++++++++---- .../components/zwave_js/test_device_action.py | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index f819a33f1d4..9f6fa7fc35c 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE, + STATE_UNAVAILABLE, ) from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -172,7 +173,17 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: meter_endpoints: dict[int, dict[str, Any]] = defaultdict(dict) - for entry in entity_registry.async_entries_for_device(registry, device_id): + for entry in entity_registry.async_entries_for_device( + registry, device_id, include_disabled_entities=False + ): + # If an entry is unavailable, it is possible that the underlying value + # is no longer valid. Additionally, if an entry is disabled, its + # underlying value is not being monitored by HA so we shouldn't allow + # actions against it. + if ( + state := hass.states.get(entry.entity_id) + ) and state.state == STATE_UNAVAILABLE: + continue entity_action = {**base_action, CONF_ENTITY_ID: entry.entity_id} actions.append({**entity_action, CONF_TYPE: SERVICE_REFRESH_VALUE}) if entry.domain == LOCK_DOMAIN: @@ -187,10 +198,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: value_id = entry.unique_id.split(".")[1] # If this unique ID doesn't have a value ID, we know it is the node status # sensor which doesn't have any relevant actions - if re.match(VALUE_ID_REGEX, value_id): - value = node.values[value_id] - else: + if not re.match(VALUE_ID_REGEX, value_id): continue + value = node.values[value_id] # If the value has the meterType CC specific value, we can add a reset_meter # action for it if CC_SPECIFIC_METER_TYPE in value.metadata.cc_specific: diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 0980b414a09..ff511bc5f0e 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -11,6 +11,7 @@ from homeassistant.components import automation from homeassistant.components.zwave_js import DOMAIN, device_action from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry @@ -583,3 +584,25 @@ async def test_failure_scenarios( ) == {} ) + + +async def test_unavailable_entity_actions( + hass: HomeAssistant, + client: Client, + lock_schlage_be469: Node, + integration: ConfigEntry, +) -> None: + """Test unavailable entities are not included in actions list.""" + entity_id_unavailable = "binary_sensor.touchscreen_deadbolt_home_security_intrusion" + hass.states.async_set(entity_id_unavailable, STATE_UNAVAILABLE, force_update=True) + await hass.async_block_till_done() + node = lock_schlage_be469 + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({get_device_id(client, node)}) + assert device + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device.id + ) + assert not any( + action.get("entity_id") == entity_id_unavailable for action in actions + ) From ec993b12e92843aa40adcf5eeb5dd743256070ce Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 17 Jan 2022 05:44:21 +0100 Subject: [PATCH 359/366] Fix KNX onboarding when there is no yaml config defined yet (#64216) --- homeassistant/components/knx/__init__.py | 27 ++++++------ homeassistant/components/knx/config_flow.py | 48 +++++++++------------ 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 61d49243430..943baf47549 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -206,7 +206,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return bool(hass.config_entries.async_entries(DOMAIN)) conf = dict(conf) - hass.data[DATA_KNX_CONFIG] = conf # Only import if we haven't before. @@ -223,19 +222,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" conf = hass.data.get(DATA_KNX_CONFIG) - - # When reloading + # `conf` is None when reloading the integration or no `knx` key in configuration.yaml if conf is None: - conf = await async_integration_yaml_config(hass, DOMAIN) - if not conf or DOMAIN not in conf: - return False - - conf = conf[DOMAIN] - - # If user didn't have configuration.yaml config, generate defaults - if conf is None: - conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] - + _conf = await async_integration_yaml_config(hass, DOMAIN) + if not _conf or DOMAIN not in _conf: + _LOGGER.warning( + "No `knx:` key found in configuration.yaml. See " + "https://www.home-assistant.io/integrations/knx/ " + "for KNX entity configuration documentation" + ) + # generate defaults + conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] + else: + conf = _conf[DOMAIN] config = {**conf, **entry.data} try: @@ -363,7 +362,6 @@ class KNXModule: self.entry.async_on_unload( self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) ) - self.entry.async_on_unload(self.entry.add_update_listener(async_update_entry)) def init_xknx(self) -> None: @@ -403,7 +401,6 @@ class KNXModule: route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False), auto_reconnect=True, ) - return ConnectionConfig(auto_reconnect=True) async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 01e71eb37af..99cdc4807c6 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -44,7 +44,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - _tunnels: list + _tunnels: list[GatewayDescriptor] _gateway_ip: str = "" _gateway_port: int = DEFAULT_MCAST_PORT @@ -64,25 +64,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_type(self, user_input: dict | None = None) -> FlowResult: """Handle connection type configuration.""" - errors: dict = {} - supported_connection_types = CONF_KNX_INITIAL_CONNECTION_TYPES.copy() - fields = {} - - if user_input is None: - gateways = await scan_for_gateways() - - if gateways: - supported_connection_types.insert(0, CONF_KNX_AUTOMATIC) - self._tunnels = [ - gateway for gateway in gateways if gateway.supports_tunnelling - ] - - fields = { - vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In( - supported_connection_types - ) - } - if user_input is not None: connection_type = user_input[CONF_KNX_CONNECTION_TYPE] if connection_type == CONF_KNX_AUTOMATIC: @@ -99,6 +80,22 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_manual_tunnel() + errors: dict = {} + supported_connection_types = CONF_KNX_INITIAL_CONNECTION_TYPES.copy() + fields = {} + gateways = await scan_for_gateways() + + if gateways: + # add automatic only if a gateway responded + supported_connection_types.insert(0, CONF_KNX_AUTOMATIC) + self._tunnels = [ + gateway for gateway in gateways if gateway.supports_tunnelling + ] + + fields = { + vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(supported_connection_types) + } + return self.async_show_form( step_id="type", data_schema=vol.Schema(fields), errors=errors ) @@ -107,8 +104,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict | None = None ) -> FlowResult: """General setup.""" - errors: dict = {} - if user_input is not None: return self.async_create_entry( title=f"{CONF_KNX_TUNNELING.capitalize()} @ {user_input[CONF_HOST]}", @@ -129,6 +124,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) + errors: dict = {} fields = { vol.Required(CONF_HOST, default=self._gateway_ip): str, vol.Required(CONF_PORT, default=self._gateway_port): vol.Coerce(int), @@ -149,8 +145,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult: """Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found.""" - errors: dict = {} - if user_input is not None: gateway: GatewayDescriptor = next( gateway @@ -163,6 +157,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_manual_tunnel() + errors: dict = {} tunnel_repr = { str(tunnel) for tunnel in self._tunnels if tunnel.supports_tunnelling } @@ -182,8 +177,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_routing(self, user_input: dict | None = None) -> FlowResult: """Routing setup.""" - errors: dict = {} - if user_input is not None: return self.async_create_entry( title=CONF_KNX_ROUTING.capitalize(), @@ -205,6 +198,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) + errors: dict = {} fields = { vol.Required( CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS @@ -434,7 +428,7 @@ class KNXOptionsFlowHandler(OptionsFlow): ) -async def scan_for_gateways(stop_on_found: int = 0) -> list: +async def scan_for_gateways(stop_on_found: int = 0) -> list[GatewayDescriptor]: """Scan for gateways within the network.""" xknx = XKNX() gatewayscanner = GatewayScanner( From 8c531b4c17becf59da257ab99a633411a34032c2 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 17 Jan 2022 19:30:35 +0100 Subject: [PATCH 360/366] Bump bimmer_connected to 0.8.10 (#64314) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 63046d9d441..9698679a6d6 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.8.7"], + "requirements": ["bimmer_connected==0.8.10"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 86e575f4cb5..864a1d1960d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -388,7 +388,7 @@ beautifulsoup4==4.10.0 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.7 +bimmer_connected==0.8.10 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9827fb3f7b7..d9da69ac50d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ base36==0.1.1 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.7 +bimmer_connected==0.8.10 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 1c82a56618d07df3b8504a9ebf35504aac3f340e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Jan 2022 10:39:56 -0800 Subject: [PATCH 361/366] Bumped version to 2021.12.10 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 16ee024024c..3f2f57c489e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "9" +PATCH_VERSION: Final = "10" __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) From 69c5b04de9c1b36f459f86dbd0a97d787bb6cd2a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Jan 2022 11:21:55 -0800 Subject: [PATCH 362/366] lint --- tests/components/zwave_js/test_device_action.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index ff511bc5f0e..965c7207fcf 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -600,9 +600,7 @@ async def test_unavailable_entity_actions( dev_reg = device_registry.async_get(hass) device = dev_reg.async_get_device({get_device_id(client, node)}) assert device - actions = await async_get_device_automations( - hass, DeviceAutomationType.ACTION, device.id - ) + actions = await async_get_device_automations(hass, "action", device.id) assert not any( action.get("entity_id") == entity_id_unavailable for action in actions ) From 777c5b77171a617753ac5cf5e24b71d7f5d0fdec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jan 2022 21:00:44 -1000 Subject: [PATCH 363/366] Ensure status is correct at start for older august bridges (#64144) --- homeassistant/components/august/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 53ea583050b..c198cf8a3b3 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -139,9 +139,15 @@ class AugustData(AugustSubscriberMixin): self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub) if self._locks_by_id: - await asyncio.gather( - *[self.async_status_async(lock_id) for lock_id in self._locks_by_id] - ) + tasks = [] + for lock_id in self._locks_by_id: + detail = self._device_detail_by_id[lock_id] + tasks.append( + self.async_status_async( + lock_id, bool(detail.bridge and detail.bridge.hyper_bridge) + ) + ) + await asyncio.gather(*tasks) @callback def async_pubnub_message(self, device_id, date_time, message): @@ -250,13 +256,14 @@ class AugustData(AugustSubscriberMixin): device_id, ) - async def async_status_async(self, device_id): + async def async_status_async(self, device_id, hyper_bridge): """Request status of the the device but do not wait for a response since it will come via pubnub.""" return await self._async_call_api_op_requires_bridge( device_id, self._api.async_status_async, self._august_gateway.access_token, device_id, + hyper_bridge, ) async def async_lock_async(self, device_id, hyper_bridge): From bfe657ac966369c9213ce45bc6b610b0c32c30d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jan 2022 12:35:36 -1000 Subject: [PATCH 364/366] Bump flux_led to 0.28.4 to fix setting white temp on 0x35 devices (#64326) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 682a4fb5f68..a5d1482adfe 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.3"], + "requirements": ["flux_led==0.28.4"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 864a1d1960d..c97ab17efc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.3 +flux_led==0.28.4 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9da69ac50d..36f119e77d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.3 +flux_led==0.28.4 # homeassistant.components.homekit fnvhash==0.1.0 From 959498d8df9c9f9ae461017e7505e70f7a176258 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 17 Jan 2022 22:37:33 +0100 Subject: [PATCH 365/366] Orphaned MAC addresses breaks UniFi options flow (#64327) --- homeassistant/components/unifi/config_flow.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 4ab566eb5b4..fda03cbd3d9 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -316,6 +316,10 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): ) ssid_filter = {ssid: ssid for ssid in sorted(ssids)} + selected_ssids_to_filter = [ + ssid for ssid in self.controller.option_ssid_filter if ssid in ssid_filter + ] + return self.async_show_form( step_id="device_tracker", data_schema=vol.Schema( @@ -333,7 +337,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): default=self.controller.option_track_devices, ): bool, vol.Optional( - CONF_SSID_FILTER, default=self.controller.option_ssid_filter + CONF_SSID_FILTER, default=selected_ssids_to_filter ): cv.multi_select(ssid_filter), vol.Optional( CONF_DETECTION_TIME, @@ -365,12 +369,18 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): client.mac ] = f"{client.name or client.hostname} ({client.mac})" + selected_clients_to_block = [ + client + for client in self.options.get(CONF_BLOCK_CLIENT, []) + if client in clients_to_block + ] + return self.async_show_form( step_id="client_control", data_schema=vol.Schema( { vol.Optional( - CONF_BLOCK_CLIENT, default=self.options[CONF_BLOCK_CLIENT] + CONF_BLOCK_CLIENT, default=selected_clients_to_block ): cv.multi_select(clients_to_block), vol.Optional( CONF_POE_CLIENTS, From c3126543b83844b6ac7b4ef8d2509d65c5237bbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Jan 2022 15:47:05 -0800 Subject: [PATCH 366/366] remove typing --- homeassistant/components/flux_led/light.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 4c93c678ad9..58d497d029e 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -251,7 +251,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return self._device.brightness # type: ignore + return self._device.brightness @property def color_temp(self) -> int: @@ -261,17 +261,17 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def rgb_color(self) -> tuple[int, int, int]: """Return the rgb color value.""" - return self._device.rgb_unscaled # type: ignore + return self._device.rgb_unscaled @property def rgbw_color(self) -> tuple[int, int, int, int]: """Return the rgbw color value.""" - return self._device.rgbw # type: ignore + return self._device.rgbw @property def rgbww_color(self) -> tuple[int, int, int, int, int]: """Return the rgbww aka rgbcw color value.""" - return self._device.rgbcw # type: ignore + return self._device.rgbcw @property def color_mode(self) -> str: @@ -283,7 +283,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): @property def effect(self) -> str | None: """Return the current effect.""" - return self._device.effect # type: ignore + return self._device.effect async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on."""