From e7fbd3b54bb4745d2abe73e2e6ec4d5bd49a47ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Sep 2023 18:14:30 +0200 Subject: [PATCH 01/95] Bumped version to 2023.10.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5585413e97b..0865e105f7a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index e4d3876d9f7..c620af3200f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0.dev0" +version = "2023.10.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 10e8173d4e68e505df7581cb1f4782d29239bb9e Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 27 Sep 2023 18:28:27 +0200 Subject: [PATCH 02/95] Restore state of trend sensor (#100332) * Restoring state of trend sensor * Handle unknown state & parametrize tests --- .../components/trend/binary_sensor.py | 10 ++++++- tests/components/trend/test_binary_sensor.py | 30 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 089e82b0f07..2d00f35202c 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_SENSORS, + STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -37,6 +38,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from homeassistant.util.dt import utcnow @@ -116,7 +118,7 @@ async def async_setup_platform( async_add_entities(sensors) -class SensorTrend(BinarySensorEntity): +class SensorTrend(BinarySensorEntity, RestoreEntity): """Representation of a trend Sensor.""" _attr_should_poll = False @@ -194,6 +196,12 @@ class SensorTrend(BinarySensorEntity): ) ) + if not (state := await self.async_get_last_state()): + return + if state.state == STATE_UNKNOWN: + return + self._state = state.state == STATE_ON + async def async_update(self) -> None: """Get the latest data and update the states.""" # Remove outdated samples diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index c477b9a11fe..cccf1add61b 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -2,16 +2,19 @@ from datetime import timedelta from unittest.mock import patch +import pytest + from homeassistant import config as hass_config, setup from homeassistant.components.trend.const import DOMAIN from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, get_fixture_path, get_test_home_assistant, + mock_restore_cache, ) @@ -413,3 +416,28 @@ async def test_reload(hass: HomeAssistant) -> None: assert hass.states.get("binary_sensor.test_trend_sensor") is None assert hass.states.get("binary_sensor.second_test_trend_sensor") + + +@pytest.mark.parametrize( + ("saved_state", "restored_state"), + [("on", "on"), ("off", "off"), ("unknown", "unknown")], +) +async def test_restore_state( + hass: HomeAssistant, saved_state: str, restored_state: str +) -> None: + """Test we restore the trend state.""" + mock_restore_cache(hass, (State("binary_sensor.test_trend_sensor", saved_state),)) + + assert await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "trend", + "sensors": {"test_trend_sensor": {"entity_id": "sensor.test_state"}}, + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_trend_sensor").state == restored_state From dde4b07c29c5cef2e6787c8ab354f5a5197c13cc Mon Sep 17 00:00:00 2001 From: steffenrapp <88974099+steffenrapp@users.noreply.github.com> Date: Wed, 27 Sep 2023 20:16:00 +0200 Subject: [PATCH 03/95] Add homeassistant reload_all translatable service name and description (#100437) * Update services.yaml * Update strings.json * Update strings.json --- homeassistant/components/homeassistant/services.yaml | 2 ++ homeassistant/components/homeassistant/strings.json | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 2b5fd3fc686..892e577490d 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -60,3 +60,5 @@ reload_config_entry: text: save_persistent_states: + +reload_all: diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 53510a94f01..a3435a8d1f5 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -125,6 +125,10 @@ "save_persistent_states": { "name": "Save persistent states", "description": "Saves the persistent states immediately. Maintains the normal periodic saving interval." + }, + "reload_all": { + "name": "Reload all", + "description": "Reload all YAML configuration that can be reloaded without restarting Home Assistant." } } } From 415042f356cb77f136d7abbcc93c8bec9fb85c1d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 27 Sep 2023 23:36:12 +0200 Subject: [PATCH 04/95] Adopt Hue integration to latest changes in Hue firmware (#101001) --- homeassistant/components/hue/manifest.json | 2 +- .../components/hue/v2/binary_sensor.py | 61 ++++++++-- homeassistant/components/hue/v2/sensor.py | 14 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/hue/fixtures/v2_resources.json | 108 ++++++++++++++++++ tests/components/hue/test_binary_sensor.py | 50 +++++++- tests/components/hue/test_sensor_v2.py | 2 - tests/components/hue/test_switch.py | 4 +- 9 files changed, 216 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index e55bd2782df..4cd6ca143cb 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -11,6 +11,6 @@ "iot_class": "local_push", "loggers": ["aiohue"], "quality_scale": "platinum", - "requirements": ["aiohue==4.6.2"], + "requirements": ["aiohue==4.7.0"], "zeroconf": ["_hue._tcp.local."] } diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index 0a8f50b8b7a..1eded0429b8 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Hue binary sensors.""" from __future__ import annotations -from typing import Any, TypeAlias +from typing import TypeAlias from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.config import ( @@ -9,9 +9,17 @@ from aiohue.v2.controllers.config import ( EntertainmentConfigurationController, ) from aiohue.v2.controllers.events import EventType -from aiohue.v2.controllers.sensors import MotionController +from aiohue.v2.controllers.sensors import ( + CameraMotionController, + ContactController, + MotionController, + TamperController, +) +from aiohue.v2.models.camera_motion import CameraMotion +from aiohue.v2.models.contact import Contact, ContactState from aiohue.v2.models.entertainment_configuration import EntertainmentStatus from aiohue.v2.models.motion import Motion +from aiohue.v2.models.tamper import Tamper, TamperState from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -25,8 +33,16 @@ from ..bridge import HueBridge from ..const import DOMAIN from .entity import HueBaseEntity -SensorType: TypeAlias = Motion | EntertainmentConfiguration -ControllerType: TypeAlias = MotionController | EntertainmentConfigurationController +SensorType: TypeAlias = ( + CameraMotion | Contact | Motion | EntertainmentConfiguration | Tamper +) +ControllerType: TypeAlias = ( + CameraMotionController + | ContactController + | MotionController + | EntertainmentConfigurationController + | TamperController +) async def async_setup_entry( @@ -57,8 +73,11 @@ async def async_setup_entry( ) # setup for each binary-sensor-type hue resource + register_items(api.sensors.camera_motion, HueMotionSensor) register_items(api.sensors.motion, HueMotionSensor) register_items(api.config.entertainment_configuration, HueEntertainmentActiveSensor) + register_items(api.sensors.contact, HueContactSensor) + register_items(api.sensors.tamper, HueTamperSensor) class HueBinarySensorBase(HueBaseEntity, BinarySensorEntity): @@ -87,12 +106,7 @@ class HueMotionSensor(HueBinarySensorBase): if not self.resource.enabled: # Force None (unknown) if the sensor is set to disabled in Hue return None - return self.resource.motion.motion - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the optional state attributes.""" - return {"motion_valid": self.resource.motion.motion_valid} + return self.resource.motion.value class HueEntertainmentActiveSensor(HueBinarySensorBase): @@ -110,3 +124,30 @@ class HueEntertainmentActiveSensor(HueBinarySensorBase): """Return sensor name.""" type_title = self.resource.type.value.replace("_", " ").title() return f"{self.resource.metadata.name}: {type_title}" + + +class HueContactSensor(HueBinarySensorBase): + """Representation of a Hue Contact sensor.""" + + _attr_device_class = BinarySensorDeviceClass.OPENING + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + if not self.resource.enabled: + # Force None (unknown) if the sensor is set to disabled in Hue + return None + return self.resource.contact_report.state != ContactState.CONTACT + + +class HueTamperSensor(HueBinarySensorBase): + """Representation of a Hue Tamper sensor.""" + + _attr_device_class = BinarySensorDeviceClass.TAMPER + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + if not self.resource.tamper_reports: + return False + return self.resource.tamper_reports[0].state == TamperState.TAMPERED diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index cc36edb88b2..4bfb727b917 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -100,12 +100,7 @@ class HueTemperatureSensor(HueSensorBase): @property def native_value(self) -> float: """Return the value reported by the sensor.""" - return round(self.resource.temperature.temperature, 1) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the optional state attributes.""" - return {"temperature_valid": self.resource.temperature.temperature_valid} + return round(self.resource.temperature.value, 1) class HueLightLevelSensor(HueSensorBase): @@ -122,14 +117,13 @@ class HueLightLevelSensor(HueSensorBase): # scale used because the human eye adjusts to light levels and small # changes at low lux levels are more noticeable than at high lux # levels. - return int(10 ** ((self.resource.light.light_level - 1) / 10000)) + return int(10 ** ((self.resource.light.value - 1) / 10000)) @property def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" return { - "light_level": self.resource.light.light_level, - "light_level_valid": self.resource.light.light_level_valid, + "light_level": self.resource.light.value, } @@ -149,6 +143,8 @@ class HueBatterySensor(HueSensorBase): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" + if self.resource.power_state.battery_state is None: + return {} return {"battery_state": self.resource.power_state.battery_state.value} diff --git a/requirements_all.txt b/requirements_all.txt index 4bb5909b09c..d9dc9f6439b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -256,7 +256,7 @@ aiohomekit==3.0.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.6.2 +aiohue==4.7.0 # homeassistant.components.imap aioimaplib==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4829c59a61..76b6ca777ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -234,7 +234,7 @@ aiohomekit==3.0.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.6.2 +aiohue==4.7.0 # homeassistant.components.imap aioimaplib==1.0.1 diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index 371975e12a5..24f433f539c 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -2221,5 +2221,113 @@ "id": "52612630-841e-4d39-9763-60346a0da759", "is_configured": true, "type": "geolocation" + }, + { + "id": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75", + "product_data": { + "model_id": "SOC001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue secure contact sensor", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "2.67.9", + "hardware_platform_type": "100b-125" + }, + "metadata": { + "name": "Test contact sensor", + "archetype": "unknown_archetype" + }, + "identify": {}, + "services": [ + { + "rid": "18802b4a-b2f6-45dc-8813-99cde47f3a4a", + "rtype": "contact" + }, + { + "rid": "d7fcfab0-69e1-4afb-99df-6ed505211db4", + "rtype": "tamper" + } + ], + "type": "device" + }, + { + "id": "18802b4a-b2f6-45dc-8813-99cde47f3a4a", + "owner": { + "rid": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75", + "rtype": "device" + }, + "enabled": true, + "contact_report": { + "changed": "2023-09-27T10:01:36.968Z", + "state": "contact" + }, + "type": "contact" + }, + { + "id": "d7fcfab0-69e1-4afb-99df-6ed505211db4", + "owner": { + "rid": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75", + "rtype": "device" + }, + "tamper_reports": [ + { + "changed": "2023-09-25T10:02:08.774Z", + "source": "battery_door", + "state": "not_tampered" + } + ], + "type": "tamper" + }, + { + "id": "1cbda90c-b675-46b0-9e97-278e7e7857ed", + "id_v1": "/sensors/249", + "product_data": { + "model_id": "CAMERA", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Fake Hue Test Camera", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "0.0.0", + "hardware_platform_type": "0" + }, + "metadata": { + "name": "Test Camera", + "archetype": "unknown_archetype" + }, + "identify": {}, + "usertest": { + "status": "set", + "usertest": false + }, + "services": [ + { + "rid": "d9f2cfee-5879-426b-aa1f-553af8f38176", + "rtype": "camera_motion" + } + ], + "type": "device" + }, + { + "id": "d9f2cfee-5879-426b-aa1f-553af8f38176", + "id_v1": "/sensors/249", + "owner": { + "rid": "1cbda90c-b675-46b0-9e97-278e7e7857ed", + "rtype": "device" + }, + "enabled": true, + "motion": { + "motion": true, + "motion_valid": true, + "motion_report": { + "changed": "2023-09-27T10:06:41.822Z", + "motion": true + } + }, + "sensitivity": { + "status": "set", + "sensitivity": 2, + "sensitivity_max": 4 + }, + "type": "motion" } ] diff --git a/tests/components/hue/test_binary_sensor.py b/tests/components/hue/test_binary_sensor.py index 7750f4a6795..3846f17aa76 100644 --- a/tests/components/hue/test_binary_sensor.py +++ b/tests/components/hue/test_binary_sensor.py @@ -14,8 +14,8 @@ async def test_binary_sensors( await setup_platform(hass, mock_bridge_v2, "binary_sensor") # there shouldn't have been any requests at this point assert len(mock_bridge_v2.mock_requests) == 0 - # 2 binary_sensors should be created from test data - assert len(hass.states.async_all()) == 2 + # 5 binary_sensors should be created from test data + assert len(hass.states.async_all()) == 5 # test motion sensor sensor = hass.states.get("binary_sensor.hue_motion_sensor_motion") @@ -23,7 +23,6 @@ async def test_binary_sensors( assert sensor.state == "off" assert sensor.name == "Hue motion sensor Motion" assert sensor.attributes["device_class"] == "motion" - assert sensor.attributes["motion_valid"] is True # test entertainment room active sensor sensor = hass.states.get( @@ -34,6 +33,51 @@ async def test_binary_sensors( assert sensor.name == "Entertainmentroom 1: Entertainment Configuration" assert sensor.attributes["device_class"] == "running" + # test contact sensor + sensor = hass.states.get("binary_sensor.test_contact_sensor_contact") + assert sensor is not None + assert sensor.state == "off" + assert sensor.name == "Test contact sensor Contact" + assert sensor.attributes["device_class"] == "opening" + # test contact sensor disabled == state unknown + mock_bridge_v2.api.emit_event( + "update", + { + "enabled": False, + "id": "18802b4a-b2f6-45dc-8813-99cde47f3a4a", + "type": "contact", + }, + ) + await hass.async_block_till_done() + sensor = hass.states.get("binary_sensor.test_contact_sensor_contact") + assert sensor.state == "unknown" + + # test tamper sensor + sensor = hass.states.get("binary_sensor.test_contact_sensor_tamper") + assert sensor is not None + assert sensor.state == "off" + assert sensor.name == "Test contact sensor Tamper" + assert sensor.attributes["device_class"] == "tamper" + # test tamper sensor when no tamper reports exist + mock_bridge_v2.api.emit_event( + "update", + { + "id": "d7fcfab0-69e1-4afb-99df-6ed505211db4", + "tamper_reports": [], + "type": "tamper", + }, + ) + await hass.async_block_till_done() + sensor = hass.states.get("binary_sensor.test_contact_sensor_tamper") + assert sensor.state == "off" + + # test camera_motion sensor + sensor = hass.states.get("binary_sensor.test_camera_motion") + assert sensor is not None + assert sensor.state == "on" + assert sensor.name == "Test Camera Motion" + assert sensor.attributes["device_class"] == "motion" + async def test_binary_sensor_add_update(hass: HomeAssistant, mock_bridge_v2) -> None: """Test if binary_sensor get added/updated from events.""" diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index 91eccc2c984..45e39e94119 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -28,7 +28,6 @@ async def test_sensors( assert sensor.attributes["device_class"] == "temperature" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "°C" - assert sensor.attributes["temperature_valid"] is True # test illuminance sensor sensor = hass.states.get("sensor.hue_motion_sensor_illuminance") @@ -39,7 +38,6 @@ async def test_sensors( assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "lx" assert sensor.attributes["light_level"] == 18027 - assert sensor.attributes["light_level_valid"] is True # test battery sensor sensor = hass.states.get("sensor.wall_switch_with_2_controls_battery") diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index c8fa417b12c..a576b88a7c3 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -14,8 +14,8 @@ async def test_switch( await setup_platform(hass, mock_bridge_v2, "switch") # there shouldn't have been any requests at this point assert len(mock_bridge_v2.mock_requests) == 0 - # 2 entities should be created from test data - assert len(hass.states.async_all()) == 2 + # 3 entities should be created from test data + assert len(hass.states.async_all()) == 3 # test config switch to enable/disable motion sensor test_entity = hass.states.get("switch.hue_motion_sensor_motion") From 115c3d6e49f4574f5f99ae319ed60ef804415bd7 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 28 Sep 2023 02:59:19 +0200 Subject: [PATCH 05/95] Fix handling reload with invalid mqtt config (#101015) Fix handling reload whith invalid mqtt config --- homeassistant/components/mqtt/__init__.py | 13 +++++-- tests/components/mqtt/test_init.py | 41 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 5b5c39e6831..7caeb2b51f7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -24,7 +24,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import TemplateError, Unauthorized +from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event as ev, template from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -364,8 +364,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _reload_config(call: ServiceCall) -> None: """Reload the platforms.""" - # Fetch updated manual configured items and validate - config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} + # Fetch updated manually configured items and validate + if ( + config_yaml := await async_integration_yaml_config(hass, DOMAIN) + ) is None: + # Raise in case we have an invalid configuration + raise HomeAssistantError( + "Error reloading manually configured MQTT items, " + "check your configuration.yaml" + ) mqtt_data.config = config_yaml.get(DOMAIN, {}) # Reload the modern yaml platforms diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index e3a12a2c24e..48d949ae927 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3898,3 +3898,44 @@ async def test_reload_config_entry( assert state.state == "manual2_update_after_reload" assert (state := hass.states.get("sensor.test_manual3")) is not None assert state.state is STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + "hass_config", + [ + { + "mqtt": [ + { + "sensor": { + "name": "test", + "state_topic": "test-topic", + } + }, + ] + } + ], +) +async def test_reload_with_invalid_config( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test reloading yaml config fails.""" + await mqtt_mock_entry() + assert hass.states.get("sensor.test") is not None + + # Reload with an invalid config and assert again + invalid_config = {"mqtt": "some_invalid_config"} + with patch( + "homeassistant.config.load_yaml_config_file", return_value=invalid_config + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "mqtt", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + # Test nothing changed as loading the config failed + assert hass.states.get("sensor.test") is not None From c287564e68013c4f84b94c7df052f4fbab99e8f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Sep 2023 16:34:25 -0500 Subject: [PATCH 06/95] Fix HomeKit handling of unavailable state (#101021) --- .../components/homekit/accessories.py | 4 ++- tests/components/homekit/test_type_covers.py | 6 +++-- tests/components/homekit/test_type_locks.py | 25 ++++++++++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 88422b5c957..5a1e9bc1ea2 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -465,7 +465,9 @@ class HomeAccessory(Accessory): # type: ignore[misc] def async_update_state_callback(self, new_state: State | None) -> None: """Handle state change listener callback.""" _LOGGER.debug("New_state: %s", new_state) - if new_state is None: + # HomeKit handles unavailable state via the available property + # so we should not propagate it here + if new_state is None or new_state.state == STATE_UNAVAILABLE: return battery_state = None battery_charging_state = None diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 9da576b6a0e..b8841289611 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -74,17 +74,19 @@ async def test_garage_door_open_close(hass: HomeAssistant, hk_driver, events) -> assert acc.char_obstruction_detected.value is True hass.states.async_set( - entity_id, STATE_UNAVAILABLE, {ATTR_OBSTRUCTION_DETECTED: False} + entity_id, STATE_UNAVAILABLE, {ATTR_OBSTRUCTION_DETECTED: True} ) await hass.async_block_till_done() assert acc.char_current_state.value == HK_DOOR_OPEN assert acc.char_target_state.value == HK_DOOR_OPEN - assert acc.char_obstruction_detected.value is False + assert acc.char_obstruction_detected.value is True + assert acc.available is False hass.states.async_set(entity_id, STATE_UNKNOWN) await hass.async_block_till_done() assert acc.char_current_state.value == HK_DOOR_OPEN assert acc.char_target_state.value == HK_DOOR_OPEN + assert acc.available is True # Set from HomeKit call_close_cover = async_mock_service(hass, DOMAIN, "close_cover") diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 32f1561644e..dc614ee54c4 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, STATE_LOCKED, + STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNLOCKED, ) @@ -68,10 +69,32 @@ async def test_lock_unlock(hass: HomeAssistant, hk_driver, events) -> None: assert acc.char_current_state.value == 3 assert acc.char_target_state.value == 0 - hass.states.async_remove(entity_id) + # Unavailable should keep last state + # but set the accessory to not available + hass.states.async_set(entity_id, STATE_UNAVAILABLE) await hass.async_block_till_done() assert acc.char_current_state.value == 3 assert acc.char_target_state.value == 0 + assert acc.available is False + + hass.states.async_set(entity_id, STATE_UNLOCKED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 + assert acc.available is True + + # Unavailable should keep last state + # but set the accessory to not available + hass.states.async_set(entity_id, STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 + assert acc.available is False + + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 # Set from HomeKit call_lock = async_mock_service(hass, DOMAIN, "lock") From be93793db9f5b9c8d7a95994f39d54e6f5f4d927 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Sep 2023 00:58:30 +0200 Subject: [PATCH 07/95] Update pyweatherflowudp to 1.4.3 (#101022) --- homeassistant/components/weatherflow/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherflow/manifest.json b/homeassistant/components/weatherflow/manifest.json index e2671d74cda..3c34250652d 100644 --- a/homeassistant/components/weatherflow/manifest.json +++ b/homeassistant/components/weatherflow/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyweatherflowudp"], - "requirements": ["pyweatherflowudp==1.4.2"] + "requirements": ["pyweatherflowudp==1.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d9dc9f6439b..5fcf0dc6fd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2249,7 +2249,7 @@ pyvolumio==0.1.5 pywaze==0.5.0 # homeassistant.components.weatherflow -pyweatherflowudp==1.4.2 +pyweatherflowudp==1.4.3 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76b6ca777ac..75446892ece 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1675,7 +1675,7 @@ pyvolumio==0.1.5 pywaze==0.5.0 # homeassistant.components.weatherflow -pyweatherflowudp==1.4.2 +pyweatherflowudp==1.4.3 # homeassistant.components.html5 pywebpush==1.9.2 From af37de46bd46102b5eb089e4474f406133563e9b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 27 Sep 2023 19:55:26 -0500 Subject: [PATCH 08/95] Use webrtc-noise-gain without AVX2 (#101028) --- homeassistant/components/assist_pipeline/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/assist_pipeline/manifest.json b/homeassistant/components/assist_pipeline/manifest.json index 138f880526d..db6c517a81a 100644 --- a/homeassistant/components/assist_pipeline/manifest.json +++ b/homeassistant/components/assist_pipeline/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/assist_pipeline", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["webrtc-noise-gain==1.2.1"] + "requirements": ["webrtc-noise-gain==1.2.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 698960095ba..bf287f564cc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -50,7 +50,7 @@ typing-extensions>=4.8.0,<5.0 ulid-transform==0.8.1 voluptuous-serialize==2.6.0 voluptuous==0.13.1 -webrtc-noise-gain==1.2.1 +webrtc-noise-gain==1.2.2 yarl==1.9.2 zeroconf==0.115.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5fcf0dc6fd6..098e57ea5ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2700,7 +2700,7 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.assist_pipeline -webrtc-noise-gain==1.2.1 +webrtc-noise-gain==1.2.2 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75446892ece..bd708f0c767 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2006,7 +2006,7 @@ wallbox==0.4.12 watchdog==2.3.1 # homeassistant.components.assist_pipeline -webrtc-noise-gain==1.2.1 +webrtc-noise-gain==1.2.2 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.4 From b02f64196b59a5c061ceeb047b7dd87e90310fb9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Sep 2023 21:00:57 -0400 Subject: [PATCH 09/95] Bumped version to 2023.10.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0865e105f7a..0e659a58980 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index c620af3200f..8ed01169f89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b0" +version = "2023.10.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9ab340047d3024f19d91dd675de30c24d4274133 Mon Sep 17 00:00:00 2001 From: lennart24 <18117505+lennart24@users.noreply.github.com> Date: Thu, 28 Sep 2023 12:59:02 +0200 Subject: [PATCH 10/95] Add shutter_tilt support for Fibaro FGR 223 (#96283) * add support for shutter_tilt for Fibaro FGR 223 add tests for fgr 223 * Adjust comments and docstring --------- Co-authored-by: Lennart <18117505+Ced4@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- .../components/zwave_js/discovery.py | 60 + tests/components/zwave_js/conftest.py | 14 + .../fixtures/cover_fibaro_fgr223_state.json | 2325 +++++++++++++++++ tests/components/zwave_js/test_cover.py | 138 +- 4 files changed, 2530 insertions(+), 7 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/cover_fibaro_fgr223_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index d54dc659be1..0a3f61fd824 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -160,6 +160,8 @@ class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne): writeable: bool | None = None # [optional] the value's states map must include ANY of these key/value pairs any_available_states: set[tuple[int, str]] | None = None + # [optional] the value's value must match this value + value: Any | None = None @dataclass @@ -378,6 +380,61 @@ DISCOVERY_SCHEMAS = [ ) ], ), + # Fibaro Shutter Fibaro FGR223 + # Combine both switch_multilevel endpoints into shutter_tilt + # if operating mode (151) is set to venetian blind (2) + ZWaveDiscoverySchema( + platform=Platform.COVER, + hint="shutter_tilt", + manufacturer_id={0x010F}, + product_id={0x1000, 0x1001}, + product_type={0x0303}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={CURRENT_VALUE_PROPERTY}, + endpoint={1}, + type={ValueType.NUMBER}, + ), + data_template=CoverTiltDataTemplate( + current_tilt_value_id=ZwaveValueID( + property_=CURRENT_VALUE_PROPERTY, + command_class=CommandClass.SWITCH_MULTILEVEL, + endpoint=2, + ), + target_tilt_value_id=ZwaveValueID( + property_=TARGET_VALUE_PROPERTY, + command_class=CommandClass.SWITCH_MULTILEVEL, + endpoint=2, + ), + ), + required_values=[ + ZWaveValueDiscoverySchema( + command_class={CommandClass.CONFIGURATION}, + property={151}, + endpoint={0}, + value={2}, + ) + ], + ), + # Fibaro Shutter Fibaro FGR223 + # Disable endpoint 2 (slat), + # as these are either combined with endpoint one as shutter_tilt + # or it has no practical function. + # CC: Switch_Multilevel + ZWaveDiscoverySchema( + platform=Platform.COVER, + hint="shutter", + manufacturer_id={0x010F}, + product_id={0x1000, 0x1001}, + product_type={0x0303}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={CURRENT_VALUE_PROPERTY}, + endpoint={2}, + type={ValueType.NUMBER}, + ), + entity_registry_enabled_default=False, + ), # Fibaro Nice BiDi-ZWave (IBT4ZWAVE) ZWaveDiscoverySchema( platform=Platform.COVER, @@ -1236,6 +1293,9 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool: ) ): return False + # check value + if schema.value is not None and value.value not in schema.value: + return False return True diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e950ff0402c..bbc836488c2 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -483,6 +483,12 @@ def fibaro_fgr222_shutter_state_fixture(): return json.loads(load_fixture("zwave_js/cover_fibaro_fgr222_state.json")) +@pytest.fixture(name="fibaro_fgr223_shutter_state", scope="session") +def fibaro_fgr223_shutter_state_fixture(): + """Load the Fibaro FGR223 node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_fibaro_fgr223_state.json")) + + @pytest.fixture(name="merten_507801_state", scope="session") def merten_507801_state_fixture(): """Load the Merten 507801 Shutter node state fixture data.""" @@ -1054,6 +1060,14 @@ def fibaro_fgr222_shutter_cover_fixture(client, fibaro_fgr222_shutter_state): return node +@pytest.fixture(name="fibaro_fgr223_shutter") +def fibaro_fgr223_shutter_cover_fixture(client, fibaro_fgr223_shutter_state): + """Mock a Fibaro FGR223 Shutter node.""" + node = Node(client, copy.deepcopy(fibaro_fgr223_shutter_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="merten_507801") def merten_507801_cover_fixture(client, merten_507801_state): """Mock a Merten 507801 Shutter node.""" diff --git a/tests/components/zwave_js/fixtures/cover_fibaro_fgr223_state.json b/tests/components/zwave_js/fixtures/cover_fibaro_fgr223_state.json new file mode 100644 index 00000000000..b0f4992e319 --- /dev/null +++ b/tests/components/zwave_js/fixtures/cover_fibaro_fgr223_state.json @@ -0,0 +1,2325 @@ +{ + "nodeId": 10, + "index": 0, + "installerIcon": 6400, + "userIcon": 6400, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": true, + "manufacturerId": 271, + "productId": 4096, + "productType": 771, + "firmwareVersion": "5.1", + "zwavePlusVersion": 1, + "name": "fgr 223 test cover", + "location": "test location", + "deviceConfig": { + "filename": "/data/db/devices/0x010f/fgr223.json", + "isEmbedded": true, + "manufacturer": "Fibargroup", + "manufacturerId": 271, + "label": "FGR223", + "description": "Roller Shutter 3", + "devices": [ + { + "productType": 771, + "productId": 4096 + }, + { + "productType": 771, + "productId": 12288 + }, + { + "productType": 771, + "productId": 16384 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "paramInformation": { + "_map": {} + }, + "proprietary": { + "fibaroCCs": [38] + } + }, + "label": "FGR223", + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 2, + "aggregatedEndpointCount": 0, + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 10, + "index": 0, + "installerIcon": 6400, + "userIcon": 6400, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 6, + "label": "Motor Control Class B" + }, + "mandatorySupportedCCs": [32, 38, 37, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 86, + "name": "CRC-16 Encapsulation", + "version": 1, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": true + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": true + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": true + } + ] + }, + { + "nodeId": 10, + "index": 1, + "installerIcon": 6400, + "userIcon": 6400, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 6, + "label": "Motor Control Class B" + }, + "mandatorySupportedCCs": [32, 38, 37, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": true + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 10, + "index": 2, + "installerIcon": 6400, + "userIcon": 6400, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 6, + "label": "Motor Control Class B" + }, + "mandatorySupportedCCs": [32, 38, 37, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": true + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "001", + "propertyName": "scene", + "propertyKeyName": "001", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Scene 001", + "min": 0, + "max": 255, + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + "3": "KeyPressed2x", + "4": "KeyPressed3x" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "002", + "propertyName": "scene", + "propertyKeyName": "002", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Scene 002", + "min": 0, + "max": 255, + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + "3": "KeyPressed2x", + "4": "KeyPressed3x" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "slowRefresh", + "propertyName": "slowRefresh", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms.", + "label": "Send held down notifications at a slow rate", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 108, + "commandClassName": "Supervision", + "property": "ccSupported", + "propertyKey": 91, + "propertyName": "ccSupported", + "propertyKeyName": "91", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Switch type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Switch type", + "default": 2, + "min": 0, + "max": 2, + "states": { + "0": "Momentary switches", + "1": "Toggle switches", + "2": "Single momentary switch (S1)" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Switch type" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 24, + "propertyName": "Inputs orientation", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Inputs orientation", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "default", + "1": "reversed" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Inputs orientation" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 25, + "propertyName": "Outputs orientation", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Outputs orientation", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "default", + "1": "reversed" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Outputs orientation" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyKey": 1, + "propertyName": "S1 scenes: Pressed 1 time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Send a Central Scene notification when S1 is pressed 1 time", + "label": "S1 scenes: Pressed 1 time", + "default": 0, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "S1 scenes: Pressed 1 time", + "info": "Send a Central Scene notification when S1 is pressed 1 time" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyKey": 2, + "propertyName": "S1 scenes: Pressed 2 times", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Send a Central Scene notification when S1 is pressed 2 times", + "label": "S1 scenes: Pressed 2 times", + "default": 0, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "S1 scenes: Pressed 2 times", + "info": "Send a Central Scene notification when S1 is pressed 2 times" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyKey": 4, + "propertyName": "S1 scenes: Pressed 3 time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Send a Central Scene notification when S1 is pressed 3 times", + "label": "S1 scenes: Pressed 3 time", + "default": 0, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "S1 scenes: Pressed 3 time", + "info": "Send a Central Scene notification when S1 is pressed 3 times" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyKey": 8, + "propertyName": "S1 scenes: Hold down / Release", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Send a Central Scene notification when S1 is held down or released", + "label": "S1 scenes: Hold down / Release", + "default": 0, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "S1 scenes: Hold down / Release", + "info": "Send a Central Scene notification when S1 is held down or released" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 41, + "propertyKey": 1, + "propertyName": "S2 scenes: Pressed 1 time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Send a Central Scene notification when S2 is pressed 1 time", + "label": "S2 scenes: Pressed 1 time", + "default": 0, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "S2 scenes: Pressed 1 time", + "info": "Send a Central Scene notification when S2 is pressed 1 time" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 41, + "propertyKey": 2, + "propertyName": "S2 scenes: Pressed 2 times", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Send a Central Scene notification when S2 is pressed 2 times", + "label": "S2 scenes: Pressed 2 times", + "default": 0, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "S2 scenes: Pressed 2 times", + "info": "Send a Central Scene notification when S2 is pressed 2 times" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 41, + "propertyKey": 4, + "propertyName": "S2 scenes: Pressed 3 time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Send a Central Scene notification when S2 is pressed 3 times", + "label": "S2 scenes: Pressed 3 time", + "default": 0, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "S2 scenes: Pressed 3 time", + "info": "Send a Central Scene notification when S2 is pressed 3 times" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 41, + "propertyKey": 8, + "propertyName": "S2 scenes: Hold down / Release", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Send a Central Scene notification when S2 is held down or released", + "label": "S2 scenes: Hold down / Release", + "default": 0, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "S2 scenes: Hold down / Release", + "info": "Send a Central Scene notification when S2 is held down or released" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 60, + "propertyName": "Measuring power consumed by the device itself", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Measuring power consumed by the device itself", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Function inactive", + "1": "Function active" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Measuring power consumed by the device itself" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 61, + "propertyName": "Power reports - on change", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Power reports - on change", + "default": 15, + "min": 0, + "max": 500, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Power reports - on change" + }, + "value": 15 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 62, + "propertyName": "Power reports - periodic", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Power reports - periodic", + "default": 3600, + "min": 0, + "max": 32400, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Power reports - periodic" + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 65, + "propertyName": "Energy reports - on change", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Energy reports - on change", + "default": 10, + "min": 0, + "max": 500, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Energy reports - on change" + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 66, + "propertyName": "Energy reports - periodic", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Energy reports - periodic", + "default": 3600, + "min": 0, + "max": 32400, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Energy reports - periodic" + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 150, + "propertyName": "Force calibration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Force calibration", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "device is not calibrated", + "1": "device is calibrated", + "2": "force device calibration" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Force calibration" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 151, + "propertyName": "Operating mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Operating mode", + "default": 1, + "min": 1, + "max": 6, + "states": { + "1": "roller blind", + "2": "Venetian blind", + "3": "gate w/o positioning", + "4": "gate with positioning", + "5": "roller blind with built-in driver", + "6": "roller blind with built-in driver (impulse)" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Operating mode" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 152, + "propertyName": "Venetian blind - time of full turn of the slats", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Venetian blind - time of full turn of the slats", + "default": 150, + "min": 0, + "max": 65535, + "unit": "1/100 sec", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Venetian blind - time of full turn of the slats" + }, + "value": 150 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 153, + "propertyName": "Set slats back to previous position", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Set slats back to previous position", + "default": 1, + "min": 0, + "max": 2, + "states": { + "0": "Main controller operation", + "1": "Controller, Momentary Switch, Limit Switch", + "2": "Controller, both Switches, Multilevel Stop" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Set slats back to previous position" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 154, + "propertyName": "Delay motor stop", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Delay motor stop after reaching end switch", + "label": "Delay motor stop", + "default": 10, + "min": 0, + "max": 255, + "unit": "1/10 sec", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Delay motor stop", + "info": "Delay motor stop after reaching end switch" + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 155, + "propertyName": "Motor operation detection", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Power threshold to be interpreted as reaching a limit switch", + "label": "Motor operation detection", + "default": 10, + "min": 0, + "max": 255, + "unit": "W", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Motor operation detection", + "info": "Power threshold to be interpreted as reaching a limit switch" + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 156, + "propertyName": "Time of up movement", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Time of up movement", + "default": 6000, + "min": 0, + "max": 65535, + "unit": "1/100 sec", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Time of up movement" + }, + "value": 5000 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 157, + "propertyName": "Time of down movement", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Time of down movement", + "default": 6000, + "min": 0, + "max": 65535, + "unit": "1/100 sec", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Time of down movement" + }, + "value": 5000 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 255, + "propertyName": "Alarm #1: Action", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which action to perform when Alarm #1 is triggered", + "label": "Alarm #1: Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No action", + "1": "Open blinds", + "2": "Close blinds" + }, + "valueSize": 4, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Alarm #1: Action", + "info": "Which action to perform when Alarm #1 is triggered" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 65280, + "propertyName": "Alarm #1: Event/State Parameters", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which event parameters Alarm #1 should be limited to", + "label": "Alarm #1: Event/State Parameters", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #1: Event/State Parameters", + "info": "Which event parameters Alarm #1 should be limited to" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 16711680, + "propertyName": "Alarm #1: Notification Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification status Alarm #1 should be limited to", + "label": "Alarm #1: Notification Status", + "default": 0, + "min": 0, + "max": 255, + "states": { + "255": "Any" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #1: Notification Status", + "info": "Which notification status Alarm #1 should be limited to" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 4278190080, + "propertyName": "Alarm #1: Notification Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification type should raise Alarm #1", + "label": "Alarm #1: Notification Type", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #1: Notification Type", + "info": "Which notification type should raise Alarm #1" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 255, + "propertyName": "Alarm #2: Action", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which action to perform when Alarm #2 is triggered", + "label": "Alarm #2: Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No action", + "1": "Open blinds", + "2": "Close blinds" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Alarm #2: Action", + "info": "Which action to perform when Alarm #2 is triggered" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 65280, + "propertyName": "Alarm #2: Event/State Parameters", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which event parameters Alarm #2 should be limited to", + "label": "Alarm #2: Event/State Parameters", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #2: Event/State Parameters", + "info": "Which event parameters Alarm #2 should be limited to" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 16711680, + "propertyName": "Alarm #2: Notification Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification status Alarm #2 should be limited to", + "label": "Alarm #2: Notification Status", + "default": 255, + "min": 0, + "max": 255, + "states": { + "255": "Any" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #2: Notification Status", + "info": "Which notification status Alarm #2 should be limited to" + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 4278190080, + "propertyName": "Alarm #2: Notification Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification type should raise Alarm #2", + "label": "Alarm #2: Notification Type", + "default": 5, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #2: Notification Type", + "info": "Which notification type should raise Alarm #2" + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 255, + "propertyName": "Alarm #3: Action", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which action to perform when Alarm #3 is triggered", + "label": "Alarm #3: Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No action", + "1": "Open blinds", + "2": "Close blinds" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Alarm #3: Action", + "info": "Which action to perform when Alarm #3 is triggered" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 65280, + "propertyName": "Alarm #3: Event/State Parameters", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which event parameters Alarm #3 should be limited to", + "label": "Alarm #3: Event/State Parameters", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #3: Event/State Parameters", + "info": "Which event parameters Alarm #3 should be limited to" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 16711680, + "propertyName": "Alarm #3: Notification Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification status Alarm #3 should be limited to", + "label": "Alarm #3: Notification Status", + "default": 255, + "min": 0, + "max": 255, + "states": { + "255": "Any" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #3: Notification Status", + "info": "Which notification status Alarm #3 should be limited to" + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 4278190080, + "propertyName": "Alarm #3: Notification Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification type should raise Alarm #3", + "label": "Alarm #3: Notification Type", + "default": 1, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #3: Notification Type", + "info": "Which notification type should raise Alarm #3" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 255, + "propertyName": "Alarm #4: Action", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which action to perform when Alarm #4 is triggered", + "label": "Alarm #4: Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No action", + "1": "Open blinds", + "2": "Close blinds" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Alarm #4: Action", + "info": "Which action to perform when Alarm #4 is triggered" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 65280, + "propertyName": "Alarm #4: Event/State Parameters", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which event parameters Alarm #4 should be limited to", + "label": "Alarm #4: Event/State Parameters", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #4: Event/State Parameters", + "info": "Which event parameters Alarm #4 should be limited to" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 16711680, + "propertyName": "Alarm #4: Notification Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification status Alarm #4 should be limited to", + "label": "Alarm #4: Notification Status", + "default": 255, + "min": 0, + "max": 255, + "states": { + "255": "Any" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #4: Notification Status", + "info": "Which notification status Alarm #4 should be limited to" + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 4278190080, + "propertyName": "Alarm #4: Notification Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification type should raise Alarm #4", + "label": "Alarm #4: Notification Type", + "default": 2, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #4: Notification Type", + "info": "Which notification type should raise Alarm #4" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyKey": 255, + "propertyName": "Alarm #5: Action", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which action to perform when Alarm #5 is triggered", + "label": "Alarm #5: Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No action", + "1": "Open blinds", + "2": "Close blinds" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Alarm #5: Action", + "info": "Which action to perform when Alarm #5 is triggered" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyKey": 65280, + "propertyName": "Alarm #5: Event/State Parameters", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which event parameters Alarm #5 should be limited to", + "label": "Alarm #5: Event/State Parameters", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #5: Event/State Parameters", + "info": "Which event parameters Alarm #5 should be limited to" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyKey": 16711680, + "propertyName": "Alarm #5: Notification Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification status Alarm #5 should be limited to", + "label": "Alarm #5: Notification Status", + "default": 255, + "min": 0, + "max": 255, + "states": { + "255": "Any" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #5: Notification Status", + "info": "Which notification status Alarm #5 should be limited to" + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyKey": 4278190080, + "propertyName": "Alarm #5: Notification Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which notification type should raise Alarm #5", + "label": "Alarm #5: Notification Type", + "default": 4, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Alarm #5: Notification Type", + "info": "Which notification type should raise Alarm #5" + }, + "value": 4 + }, + { + "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, + "stateful": true, + "secret": false + }, + "value": 271 + }, + { + "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, + "stateful": true, + "secret": false + }, + "value": 771 + }, + { + "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, + "stateful": true, + "secret": false + }, + "value": 4096 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "local", + "propertyName": "local", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "2": "NoOperationPossible" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "rf", + "propertyName": "rf", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "RF protection state", + "states": { + "0": "Unprotected", + "1": "NoControl" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "exclusiveControlNodeId", + "propertyName": "exclusiveControlNodeId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Node ID with exclusive control", + "min": 1, + "max": 232, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "RF protection timeout", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "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" + }, + "stateful": true, + "secret": false + }, + "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", + "stateful": true, + "secret": false + }, + "value": "6.2" + }, + { + "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", + "stateful": true, + "secret": false + }, + "value": ["5.1", "5.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", + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 1, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": "unknown" + }, + { + "endpoint": 1, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 99 + }, + { + "endpoint": 1, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 99 + }, + { + "endpoint": 1, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 1, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 1, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [kWh]", + "ccSpecific": { + "meterType": 1, + "scale": 0, + "rateType": 1 + }, + "unit": "kWh", + "stateful": true, + "secret": false + }, + "value": 0.0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 1 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values", + "states": { + "true": "Reset" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 1, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-current status", + "propertyName": "Power Management", + "propertyKeyName": "Over-current status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-current status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Over-current detected" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyKey": "Hardware status", + "propertyName": "System", + "propertyKeyName": "Hardware status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Hardware status", + "ccSpecific": { + "notificationType": 9 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "3": "System hardware failure (with failure code)" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 1, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 2, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": "unknown" + }, + { + "endpoint": 2, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 2, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 2, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 2, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 2, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 6, + "label": "Motor Control Class B" + }, + "mandatorySupportedCCs": [32, 38, 37, 114, 134], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x010f:0x0303:0x1000:5.1", + "statistics": { + "commandsTX": 8, + "commandsRX": 13, + "commandsDroppedRX": 12, + "commandsDroppedTX": 0, + "timeoutResponse": 1, + "rtt": 155.4, + "rssi": -66, + "lwr": { + "protocolDataRate": 2, + "repeaters": [11], + "rssi": -56, + "repeaterRSSI": [-55] + }, + "nlwr": { + "protocolDataRate": 3, + "repeaters": [], + "rssi": -89, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": 1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index e51b3751ac8..fc593de883b 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -47,7 +47,8 @@ GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" BLIND_COVER_ENTITY = "cover.window_blind_controller" SHUTTER_COVER_ENTITY = "cover.flush_shutter" AEOTEC_SHUTTER_COVER_ENTITY = "cover.nano_shutter_v_3" -FIBARO_SHUTTER_COVER_ENTITY = "cover.fgr_222_test_cover" +FIBARO_FGR_222_SHUTTER_COVER_ENTITY = "cover.fgr_222_test_cover" +FIBARO_FGR_223_SHUTTER_COVER_ENTITY = "cover.fgr_223_test_cover" LOGGER.setLevel(logging.DEBUG) @@ -238,7 +239,7 @@ async def test_fibaro_fgr222_shutter_cover( hass: HomeAssistant, client, fibaro_fgr222_shutter, integration ) -> None: """Test tilt function of the Fibaro Shutter devices.""" - state = hass.states.get(FIBARO_SHUTTER_COVER_ENTITY) + state = hass.states.get(FIBARO_FGR_222_SHUTTER_COVER_ENTITY) assert state assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.SHUTTER @@ -249,7 +250,7 @@ async def test_fibaro_fgr222_shutter_cover( await hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: FIBARO_SHUTTER_COVER_ENTITY}, + {ATTR_ENTITY_ID: FIBARO_FGR_222_SHUTTER_COVER_ENTITY}, blocking=True, ) @@ -271,7 +272,7 @@ async def test_fibaro_fgr222_shutter_cover( await hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: FIBARO_SHUTTER_COVER_ENTITY}, + {ATTR_ENTITY_ID: FIBARO_FGR_222_SHUTTER_COVER_ENTITY}, blocking=True, ) @@ -293,7 +294,7 @@ async def test_fibaro_fgr222_shutter_cover( await hass.services.async_call( DOMAIN, SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: FIBARO_SHUTTER_COVER_ENTITY, ATTR_TILT_POSITION: 12}, + {ATTR_ENTITY_ID: FIBARO_FGR_222_SHUTTER_COVER_ENTITY, ATTR_TILT_POSITION: 12}, blocking=True, ) @@ -330,7 +331,101 @@ async def test_fibaro_fgr222_shutter_cover( }, ) fibaro_fgr222_shutter.receive_event(event) - state = hass.states.get(FIBARO_SHUTTER_COVER_ENTITY) + state = hass.states.get(FIBARO_FGR_222_SHUTTER_COVER_ENTITY) + assert state + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + +async def test_fibaro_fgr223_shutter_cover( + hass: HomeAssistant, client, fibaro_fgr223_shutter, integration +) -> None: + """Test tilt function of the Fibaro Shutter devices.""" + state = hass.states.get(FIBARO_FGR_223_SHUTTER_COVER_ENTITY) + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.SHUTTER + + assert state.state == STATE_OPEN + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + # Test opening tilts + await hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: FIBARO_FGR_223_SHUTTER_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 10 + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 38, + "property": "targetValue", + } + assert args["value"] == 99 + + client.async_send_command.reset_mock() + # Test closing tilts + await hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: FIBARO_FGR_223_SHUTTER_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 10 + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 38, + "property": "targetValue", + } + assert args["value"] == 0 + + client.async_send_command.reset_mock() + # Test setting tilt position + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: FIBARO_FGR_223_SHUTTER_COVER_ENTITY, ATTR_TILT_POSITION: 12}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 10 + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 38, + "property": "targetValue", + } + assert args["value"] == 12 + + # Test some tilt + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 10, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 2, + "property": "currentValue", + "newValue": 99, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + fibaro_fgr223_shutter.receive_event(event) + state = hass.states.get(FIBARO_FGR_223_SHUTTER_COVER_ENTITY) assert state assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 @@ -694,13 +789,42 @@ async def test_fibaro_fgr222_shutter_cover_no_tilt( client.driver.controller.emit("node added", {"node": node}) await hass.async_block_till_done() - state = hass.states.get(FIBARO_SHUTTER_COVER_ENTITY) + state = hass.states.get(FIBARO_FGR_222_SHUTTER_COVER_ENTITY) assert state assert state.state == STATE_UNKNOWN assert ATTR_CURRENT_POSITION not in state.attributes assert ATTR_CURRENT_TILT_POSITION not in state.attributes +async def test_fibaro_fgr223_shutter_cover_no_tilt( + hass: HomeAssistant, client, fibaro_fgr223_shutter_state, integration +) -> None: + """Test absence of tilt function for Fibaro Shutter roller blind. + + Fibaro Shutter devices can have operating mode set to roller blind (1). + """ + node_state = replace_value_of_zwave_value( + fibaro_fgr223_shutter_state, + [ + ZwaveValueMatcher( + property_=151, + command_class=CommandClass.CONFIGURATION, + endpoint=0, + ), + ], + 1, + ) + node = Node(client, node_state) + client.driver.controller.emit("node added", {"node": node}) + await hass.async_block_till_done() + + state = hass.states.get(FIBARO_FGR_223_SHUTTER_COVER_ENTITY) + assert state + assert state.state == STATE_OPEN + assert ATTR_CURRENT_POSITION in state.attributes + assert ATTR_CURRENT_TILT_POSITION not in state.attributes + + async def test_iblinds_v3_cover( hass: HomeAssistant, client, iblinds_v3, integration ) -> None: From 81e8ca130f0644386e5467c8038af883b37f7bc9 Mon Sep 17 00:00:00 2001 From: tyjtyj Date: Thu, 28 Sep 2023 14:08:07 +0800 Subject: [PATCH 11/95] Fix google maps device_tracker same last seen timestamp (#99963) * Update device_tracker.py This fix the google_maps does not show current location when HA started/restarted and also fix unnecessary update when last_seen timestamp is the same. Unnecessary update is causing proximity sensor switching from between stationary and certain direction. * Remove elif * Fix Black check * fix black check * Update device_tracker.py Better patch * Update device_tracker.py * Update device_tracker.py Fix Black * Update device_tracker.py change warning to debug --- .../components/google_maps/device_tracker.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 2ee12f0154c..be776df1751 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -112,12 +112,22 @@ class GoogleMapsScanner: last_seen = dt_util.as_utc(person.datetime) if last_seen < self._prev_seen.get(dev_id, last_seen): - _LOGGER.warning( + _LOGGER.debug( "Ignoring %s update because timestamp is older than last timestamp", person.nickname, ) _LOGGER.debug("%s < %s", last_seen, self._prev_seen[dev_id]) continue + if last_seen == self._prev_seen.get(dev_id, last_seen) and hasattr( + self, "success_init" + ): + _LOGGER.debug( + "Ignoring %s update because timestamp " + "is the same as the last timestamp %s", + person.nickname, + last_seen, + ) + continue self._prev_seen[dev_id] = last_seen attrs = { From 35eaebd1825e16643d0a4b8cdbdcc2d0275cebd6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Sep 2023 10:55:48 +0200 Subject: [PATCH 12/95] Add feature to add measuring station via number in waqi (#99992) * Add feature to add measuring station via number * Add feature to add measuring station via number * Add feature to add measuring station via number --- homeassistant/components/waqi/config_flow.py | 123 ++++++++++-- homeassistant/components/waqi/strings.json | 22 ++- tests/components/waqi/test_config_flow.py | 191 +++++++++++++++++-- 3 files changed, 301 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/waqi/config_flow.py b/homeassistant/components/waqi/config_flow.py index b5f3a18b223..8404b425678 100644 --- a/homeassistant/components/waqi/config_flow.py +++ b/homeassistant/components/waqi/config_flow.py @@ -1,6 +1,7 @@ """Config flow for World Air Quality Index (WAQI) integration.""" from __future__ import annotations +from collections.abc import Awaitable, Callable import logging from typing import Any @@ -18,25 +19,36 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, + CONF_METHOD, CONF_NAME, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.selector import LocationSelector +from homeassistant.helpers.selector import ( + LocationSelector, + SelectSelector, + SelectSelectorConfig, +) from homeassistant.helpers.typing import ConfigType from .const import CONF_STATION_NUMBER, DOMAIN, ISSUE_PLACEHOLDER _LOGGER = logging.getLogger(__name__) +CONF_MAP = "map" + class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for World Air Quality Index (WAQI).""" VERSION = 1 + def __init__(self) -> None: + """Initialize config flow.""" + self.data: dict[str, Any] = {} + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -47,13 +59,8 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): session=async_get_clientsession(self.hass) ) as waqi_client: waqi_client.authenticate(user_input[CONF_API_KEY]) - location = user_input[CONF_LOCATION] try: - measuring_station: WAQIAirQuality = ( - await waqi_client.get_by_coordinates( - location[CONF_LATITUDE], location[CONF_LONGITUDE] - ) - ) + await waqi_client.get_by_ip() except WAQIAuthenticationError: errors["base"] = "invalid_auth" except WAQIConnectionError: @@ -62,36 +69,110 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception(exc) errors["base"] = "unknown" else: - await self.async_set_unique_id(str(measuring_station.station_id)) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=measuring_station.city.name, - data={ - CONF_API_KEY: user_input[CONF_API_KEY], - CONF_STATION_NUMBER: measuring_station.station_id, - }, - ) + self.data = user_input + if user_input[CONF_METHOD] == CONF_MAP: + return await self.async_step_map() + return await self.async_step_station_number() return self.async_show_form( step_id="user", - data_schema=self.add_suggested_values_to_schema( + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_METHOD): SelectSelector( + SelectSelectorConfig( + options=[CONF_MAP, CONF_STATION_NUMBER], + translation_key="method", + ) + ), + } + ), + errors=errors, + ) + + async def _async_base_step( + self, + step_id: str, + method: Callable[[WAQIClient, dict[str, Any]], Awaitable[WAQIAirQuality]], + data_schema: vol.Schema, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + errors: dict[str, str] = {} + if user_input is not None: + async with WAQIClient( + session=async_get_clientsession(self.hass) + ) as waqi_client: + waqi_client.authenticate(self.data[CONF_API_KEY]) + try: + measuring_station = await method(waqi_client, user_input) + except WAQIConnectionError: + errors["base"] = "cannot_connect" + except Exception as exc: # pylint: disable=broad-except + _LOGGER.exception(exc) + errors["base"] = "unknown" + else: + return await self._async_create_entry(measuring_station) + return self.async_show_form( + step_id=step_id, data_schema=data_schema, errors=errors + ) + + async def async_step_map( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Add measuring station via map.""" + return await self._async_base_step( + CONF_MAP, + lambda waqi_client, data: waqi_client.get_by_coordinates( + data[CONF_LOCATION][CONF_LATITUDE], data[CONF_LOCATION][CONF_LONGITUDE] + ), + self.add_suggested_values_to_schema( vol.Schema( { - vol.Required(CONF_API_KEY): str, vol.Required( CONF_LOCATION, ): LocationSelector(), } ), - user_input - or { + { CONF_LOCATION: { CONF_LATITUDE: self.hass.config.latitude, CONF_LONGITUDE: self.hass.config.longitude, } }, ), - errors=errors, + user_input, + ) + + async def async_step_station_number( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Add measuring station via station number.""" + return await self._async_base_step( + CONF_STATION_NUMBER, + lambda waqi_client, data: waqi_client.get_by_station_number( + data[CONF_STATION_NUMBER] + ), + vol.Schema( + { + vol.Required( + CONF_STATION_NUMBER, + ): int, + } + ), + user_input, + ) + + async def _async_create_entry( + self, measuring_station: WAQIAirQuality + ) -> FlowResult: + await self.async_set_unique_id(str(measuring_station.station_id)) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=measuring_station.city.name, + data={ + CONF_API_KEY: self.data[CONF_API_KEY], + CONF_STATION_NUMBER: measuring_station.station_id, + }, ) async def async_step_import(self, import_config: ConfigType) -> FlowResult: diff --git a/homeassistant/components/waqi/strings.json b/homeassistant/components/waqi/strings.json index 4ceb911de9e..46031a3072b 100644 --- a/homeassistant/components/waqi/strings.json +++ b/homeassistant/components/waqi/strings.json @@ -2,10 +2,20 @@ "config": { "step": { "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "method": "How do you want to select a measuring station?" + } + }, + "map": { "description": "Select a location to get the closest measuring station.", "data": { - "location": "[%key:common::config_flow::data::location%]", - "api_key": "[%key:common::config_flow::data::api_key%]" + "location": "[%key:common::config_flow::data::location%]" + } + }, + "station_number": { + "data": { + "station_number": "Measuring station number" } } }, @@ -18,6 +28,14 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "selector": { + "method": { + "options": { + "map": "Select nearest from point on the map", + "station_number": "Enter a station number" + } + } + }, "issues": { "deprecated_yaml_import_issue_invalid_auth": { "title": "The World Air Quality Index YAML configuration import failed", diff --git a/tests/components/waqi/test_config_flow.py b/tests/components/waqi/test_config_flow.py index 3901ffad550..be738a119e5 100644 --- a/tests/components/waqi/test_config_flow.py +++ b/tests/components/waqi/test_config_flow.py @@ -1,17 +1,20 @@ """Test the World Air Quality Index (WAQI) config flow.""" import json +from typing import Any from unittest.mock import AsyncMock, patch from aiowaqi import WAQIAirQuality, WAQIAuthenticationError, WAQIConnectionError import pytest from homeassistant import config_entries +from homeassistant.components.waqi.config_flow import CONF_MAP from homeassistant.components.waqi.const import CONF_STATION_NUMBER, DOMAIN from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, + CONF_METHOD, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -21,7 +24,29 @@ from tests.common import load_fixture pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: +@pytest.mark.parametrize( + ("method", "payload"), + [ + ( + CONF_MAP, + { + CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0}, + }, + ), + ( + CONF_STATION_NUMBER, + { + CONF_STATION_NUMBER: 4584, + }, + ), + ], +) +async def test_full_map_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + method: str, + payload: dict[str, Any], +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -31,17 +56,36 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No with patch( "aiowaqi.WAQIClient.authenticate", ), patch( - "aiowaqi.WAQIClient.get_by_coordinates", + "aiowaqi.WAQIClient.get_by_ip", return_value=WAQIAirQuality.parse_obj( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0}, - CONF_API_KEY: "asd", - }, + {CONF_API_KEY: "asd", CONF_METHOD: method}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == method + + with patch( + "aiowaqi.WAQIClient.authenticate", + ), patch( + "aiowaqi.WAQIClient.get_by_coordinates", + return_value=WAQIAirQuality.parse_obj( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ), patch( + "aiowaqi.WAQIClient.get_by_station_number", + return_value=WAQIAirQuality.parse_obj( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + payload, ) await hass.async_block_till_done() @@ -73,21 +117,35 @@ async def test_flow_errors( with patch( "aiowaqi.WAQIClient.authenticate", ), patch( - "aiowaqi.WAQIClient.get_by_coordinates", + "aiowaqi.WAQIClient.get_by_ip", side_effect=exception, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0}, - CONF_API_KEY: "asd", - }, + {CONF_API_KEY: "asd", CONF_METHOD: CONF_MAP}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} + with patch( + "aiowaqi.WAQIClient.authenticate", + ), patch( + "aiowaqi.WAQIClient.get_by_ip", + return_value=WAQIAirQuality.parse_obj( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "asd", CONF_METHOD: CONF_MAP}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "map" + with patch( "aiowaqi.WAQIClient.authenticate", ), patch( @@ -100,9 +158,118 @@ async def test_flow_errors( result["flow_id"], { CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0}, - CONF_API_KEY: "asd", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize( + ("method", "payload", "exception", "error"), + [ + ( + CONF_MAP, + { + CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0}, + }, + WAQIConnectionError(), + "cannot_connect", + ), + ( + CONF_MAP, + { + CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0}, + }, + Exception(), + "unknown", + ), + ( + CONF_STATION_NUMBER, + { + CONF_STATION_NUMBER: 4584, + }, + WAQIConnectionError(), + "cannot_connect", + ), + ( + CONF_STATION_NUMBER, + { + CONF_STATION_NUMBER: 4584, + }, + Exception(), + "unknown", + ), + ], +) +async def test_error_in_second_step( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + method: str, + payload: dict[str, Any], + exception: Exception, + error: str, +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "aiowaqi.WAQIClient.authenticate", + ), patch( + "aiowaqi.WAQIClient.get_by_ip", + return_value=WAQIAirQuality.parse_obj( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "asd", CONF_METHOD: method}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == method + + with patch( + "aiowaqi.WAQIClient.authenticate", + ), patch( + "aiowaqi.WAQIClient.get_by_coordinates", side_effect=exception + ), patch("aiowaqi.WAQIClient.get_by_station_number", side_effect=exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + payload, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": error} + + with patch( + "aiowaqi.WAQIClient.authenticate", + ), patch( + "aiowaqi.WAQIClient.get_by_coordinates", + return_value=WAQIAirQuality.parse_obj( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ), patch( + "aiowaqi.WAQIClient.get_by_station_number", + return_value=WAQIAirQuality.parse_obj( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + payload, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "de Jongweg, Utrecht" + assert result["data"] == { + CONF_API_KEY: "asd", + CONF_STATION_NUMBER: 4584, + } + assert len(mock_setup_entry.mock_calls) == 1 From d6c42ee8e7ee20067501d2ef2f98a2f94a37bb31 Mon Sep 17 00:00:00 2001 From: Tereza Tomcova Date: Thu, 28 Sep 2023 14:15:22 +0300 Subject: [PATCH 13/95] Bump PySwitchbot to 0.40.0 to support Curtain 3 (#100619) --- 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 49a6af2b179..e685d1de806 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.39.1"] + "requirements": ["PySwitchbot==0.40.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 098e57ea5ee..c4b3c65ebaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -97,7 +97,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.39.1 +PySwitchbot==0.40.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd708f0c767..82d5c52e6a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.39.1 +PySwitchbot==0.40.0 # homeassistant.components.syncthru PySyncThru==0.7.10 From 5bd306392ff58adea7e46cab66e44770c0200a8c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Sep 2023 17:45:10 +0200 Subject: [PATCH 14/95] Add LED control support to Home Assistant Green (#100922) * Add LED control support to Home Assistant Green * Add strings.json * Sort alphabetically * Reorder LED schema * Improve test coverage * Apply suggestions from code review Co-authored-by: Stefan Agner * Sort + fix test * Remove reboot menu --------- Co-authored-by: Stefan Agner --- homeassistant/components/hassio/__init__.py | 2 + homeassistant/components/hassio/handler.py | 21 +++ .../homeassistant_green/config_flow.py | 80 ++++++++- .../homeassistant_green/strings.json | 28 +++ tests/components/hassio/test_handler.py | 42 +++++ .../homeassistant_green/test_config_flow.py | 164 ++++++++++++++++++ 6 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/homeassistant_green/strings.json diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 3303059d824..75b2535bd44 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -88,11 +88,13 @@ from .handler import ( # noqa: F401 async_get_addon_discovery_info, async_get_addon_info, async_get_addon_store_info, + async_get_green_settings, async_get_yellow_settings, async_install_addon, async_reboot_host, async_restart_addon, async_set_addon_options, + async_set_green_settings, async_set_yellow_settings, async_start_addon, async_stop_addon, diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 020a4365ec6..fe9e1ba1d2e 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -263,6 +263,27 @@ async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> b return await hassio.send_command(command, timeout=None) +@api_data +async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]: + """Return settings specific to Home Assistant Green.""" + hassio: HassIO = hass.data[DOMAIN] + return await hassio.send_command("/os/boards/green", method="get") + + +@api_data +async def async_set_green_settings( + hass: HomeAssistant, settings: dict[str, bool] +) -> dict: + """Set settings specific to Home Assistant Green. + + Returns an empty dict. + """ + hassio: HassIO = hass.data[DOMAIN] + return await hassio.send_command( + "/os/boards/green", method="post", payload=settings + ) + + @api_data async def async_get_yellow_settings(hass: HomeAssistant) -> dict[str, bool]: """Return settings specific to Home Assistant Yellow.""" diff --git a/homeassistant/components/homeassistant_green/config_flow.py b/homeassistant/components/homeassistant_green/config_flow.py index 17ba9aacbc5..c3491de430e 100644 --- a/homeassistant/components/homeassistant_green/config_flow.py +++ b/homeassistant/components/homeassistant_green/config_flow.py @@ -1,22 +1,100 @@ """Config flow for the Home Assistant Green integration.""" from __future__ import annotations +import asyncio +import logging from typing import Any -from homeassistant.config_entries import ConfigFlow +import aiohttp +import voluptuous as vol + +from homeassistant.components.hassio import ( + HassioAPIError, + async_get_green_settings, + async_set_green_settings, + is_hassio, +) +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + +STEP_HW_SETTINGS_SCHEMA = vol.Schema( + { + # Sorted to match front panel left to right + vol.Required("power_led"): selector.BooleanSelector(), + vol.Required("activity_led"): selector.BooleanSelector(), + vol.Required("system_health_led"): selector.BooleanSelector(), + } +) + class HomeAssistantGreenConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Home Assistant Green.""" VERSION = 1 + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> HomeAssistantGreenOptionsFlow: + """Return the options flow.""" + return HomeAssistantGreenOptionsFlow() + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return self.async_create_entry(title="Home Assistant Green", data={}) + + +class HomeAssistantGreenOptionsFlow(OptionsFlow): + """Handle an option flow for Home Assistant Green.""" + + _hw_settings: dict[str, bool] | None = None + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if not is_hassio(self.hass): + return self.async_abort(reason="not_hassio") + + return await self.async_step_hardware_settings() + + async def async_step_hardware_settings( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle hardware settings.""" + + if user_input is not None: + if self._hw_settings == user_input: + return self.async_create_entry(data={}) + try: + async with asyncio.timeout(10): + await async_set_green_settings(self.hass, user_input) + except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err: + _LOGGER.warning("Failed to write hardware settings", exc_info=err) + return self.async_abort(reason="write_hw_settings_error") + return self.async_create_entry(data={}) + + try: + async with asyncio.timeout(10): + self._hw_settings: dict[str, bool] = await async_get_green_settings( + self.hass + ) + except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err: + _LOGGER.warning("Failed to read hardware settings", exc_info=err) + return self.async_abort(reason="read_hw_settings_error") + + schema = self.add_suggested_values_to_schema( + STEP_HW_SETTINGS_SCHEMA, self._hw_settings + ) + + return self.async_show_form(step_id="hardware_settings", data_schema=schema) diff --git a/homeassistant/components/homeassistant_green/strings.json b/homeassistant/components/homeassistant_green/strings.json new file mode 100644 index 00000000000..9066ca64e5c --- /dev/null +++ b/homeassistant/components/homeassistant_green/strings.json @@ -0,0 +1,28 @@ +{ + "options": { + "step": { + "hardware_settings": { + "title": "Configure hardware settings", + "data": { + "activity_led": "Green: activity LED", + "power_led": "White: power LED", + "system_health_led": "Yellow: system health LED" + } + }, + "reboot_menu": { + "title": "Reboot required", + "description": "The settings have changed, but the new settings will not take effect until the system is rebooted", + "menu_options": { + "reboot_later": "Reboot manually later", + "reboot_now": "Reboot now" + } + } + }, + "abort": { + "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", + "read_hw_settings_error": "Failed to read hardware settings", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "write_hw_settings_error": "Failed to write hardware settings" + } + } +} diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index d92a5335809..06c726360d9 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -364,6 +364,48 @@ async def test_api_headers( assert received_request.headers[hdrs.CONTENT_TYPE] == "application/octet-stream" +async def test_api_get_green_settings( + hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with API ping.""" + aioclient_mock.get( + "http://127.0.0.1/os/boards/green", + json={ + "result": "ok", + "data": { + "activity_led": True, + "power_led": True, + "system_health_led": True, + }, + }, + ) + + assert await handler.async_get_green_settings(hass) == { + "activity_led": True, + "power_led": True, + "system_health_led": True, + } + assert aioclient_mock.call_count == 1 + + +async def test_api_set_green_settings( + hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with API ping.""" + aioclient_mock.post( + "http://127.0.0.1/os/boards/green", + json={"result": "ok", "data": {}}, + ) + + assert ( + await handler.async_set_green_settings( + hass, {"activity_led": True, "power_led": True, "system_health_led": True} + ) + == {} + ) + assert aioclient_mock.call_count == 1 + + async def test_api_get_yellow_settings( hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/homeassistant_green/test_config_flow.py b/tests/components/homeassistant_green/test_config_flow.py index 2eb7389af55..84af22509f9 100644 --- a/tests/components/homeassistant_green/test_config_flow.py +++ b/tests/components/homeassistant_green/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Home Assistant Green config flow.""" from unittest.mock import patch +import pytest + from homeassistant.components.homeassistant_green.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -8,6 +10,29 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration +@pytest.fixture(name="get_green_settings") +def mock_get_green_settings(): + """Mock getting green settings.""" + with patch( + "homeassistant.components.homeassistant_green.config_flow.async_get_green_settings", + return_value={ + "activity_led": True, + "power_led": True, + "system_health_led": True, + }, + ) as get_green_settings: + yield get_green_settings + + +@pytest.fixture(name="set_green_settings") +def mock_set_green_settings(): + """Mock setting green settings.""" + with patch( + "homeassistant.components.homeassistant_green.config_flow.async_set_green_settings", + ) as set_green_settings: + yield set_green_settings + + async def test_config_flow(hass: HomeAssistant) -> None: """Test the config flow.""" mock_integration(hass, MockModule("hassio")) @@ -56,3 +81,142 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() + + +async def test_option_flow_non_hassio( + hass: HomeAssistant, +) -> None: + """Test installing the multi pan addon on a Core installation, without hassio.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_green.config_flow.is_hassio", + return_value=False, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_hassio" + + +async def test_option_flow_led_settings( + hass: HomeAssistant, + get_green_settings, + set_green_settings, +) -> None: + """Test updating LED settings.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "hardware_settings" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"activity_led": False, "power_led": False, "system_health_led": False}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + set_green_settings.assert_called_once_with( + hass, {"activity_led": False, "power_led": False, "system_health_led": False} + ) + + +async def test_option_flow_led_settings_unchanged( + hass: HomeAssistant, + get_green_settings, + set_green_settings, +) -> None: + """Test updating LED settings.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "hardware_settings" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"activity_led": True, "power_led": True, "system_health_led": True}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + set_green_settings.assert_not_called() + + +async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: + """Test updating LED settings.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_green.config_flow.async_get_green_settings", + side_effect=TimeoutError, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "read_hw_settings_error" + + +async def test_option_flow_led_settings_fail_2( + hass: HomeAssistant, get_green_settings +) -> None: + """Test updating LED settings.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "hardware_settings" + + with patch( + "homeassistant.components.homeassistant_green.config_flow.async_set_green_settings", + side_effect=TimeoutError, + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"activity_led": False, "power_led": False, "system_health_led": False}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "write_hw_settings_error" From ffad30734ba5d61fdcdc6eb53083d192b9fd4ad5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:39:57 +1300 Subject: [PATCH 15/95] ESPHome: dont send error when wake word is aborted (#101032) * ESPHome dont send error when wake word is aborted * Add test --- .../components/esphome/voice_assistant.py | 8 +++-- .../esphome/test_voice_assistant.py | 29 ++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index 58f9ce5abf4..baf3a9011e9 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -24,7 +24,10 @@ from homeassistant.components.assist_pipeline import ( async_pipeline_from_audio_stream, select as pipeline_select, ) -from homeassistant.components.assist_pipeline.error import WakeWordDetectionError +from homeassistant.components.assist_pipeline.error import ( + WakeWordDetectionAborted, + WakeWordDetectionError, +) from homeassistant.components.media_player import async_process_play_media_url from homeassistant.core import Context, HomeAssistant, callback @@ -273,6 +276,8 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): }, ) _LOGGER.warning("Pipeline not found") + except WakeWordDetectionAborted: + pass # Wake word detection was aborted and `handle_finished` is enough. except WakeWordDetectionError as e: self.handle_event( VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, @@ -281,7 +286,6 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): "message": e.message, }, ) - _LOGGER.warning("No Wake word provider found") finally: self.handle_finished() diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 6c54c5f62f3..9b6bcf1c6c7 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -12,7 +12,10 @@ from homeassistant.components.assist_pipeline import ( PipelineEventType, PipelineStage, ) -from homeassistant.components.assist_pipeline.error import WakeWordDetectionError +from homeassistant.components.assist_pipeline.error import ( + WakeWordDetectionAborted, + WakeWordDetectionError, +) from homeassistant.components.esphome import DomainData from homeassistant.components.esphome.voice_assistant import VoiceAssistantUDPServer from homeassistant.core import HomeAssistant @@ -411,3 +414,27 @@ async def test_wake_word_exception( conversation_id=None, flags=2, ) + + +async def test_wake_word_abort_exception( + hass: HomeAssistant, + voice_assistant_udp_server_v2: VoiceAssistantUDPServer, +) -> None: + """Test that the pipeline is set to start with Wake word.""" + + async def async_pipeline_from_audio_stream(*args, **kwargs): + raise WakeWordDetectionAborted + + with patch( + "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), patch.object(voice_assistant_udp_server_v2, "handle_event") as mock_handle_event: + voice_assistant_udp_server_v2.transport = Mock() + + await voice_assistant_udp_server_v2.run_pipeline( + device_id="mock-device-id", + conversation_id=None, + flags=2, + ) + + mock_handle_event.assert_not_called() From 0147108b89b6a45f1d3887239a5688b3f05cfad3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Sep 2023 09:13:33 +0200 Subject: [PATCH 16/95] Fix onvif creating a new entity for every new event (#101035) Use topic value as topic --- homeassistant/components/onvif/parsers.py | 224 ++++++++++++---------- 1 file changed, 125 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 3f405767c54..6185adb70a1 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -48,15 +48,16 @@ async def async_parse_motion_alarm(uid: str, msg) -> Event | None: Topic: tns1:VideoSource/MotionAlarm """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Motion Alarm", "binary_sensor", "motion", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -71,15 +72,16 @@ async def async_parse_image_too_blurry(uid: str, msg) -> Event | None: Topic: tns1:VideoSource/ImageTooBlurry/* """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Image Too Blurry", "binary_sensor", "problem", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -95,15 +97,16 @@ async def async_parse_image_too_dark(uid: str, msg) -> Event | None: Topic: tns1:VideoSource/ImageTooDark/* """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Image Too Dark", "binary_sensor", "problem", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -119,15 +122,16 @@ async def async_parse_image_too_bright(uid: str, msg) -> Event | None: Topic: tns1:VideoSource/ImageTooBright/* """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Image Too Bright", "binary_sensor", "problem", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -143,15 +147,16 @@ async def async_parse_scene_change(uid: str, msg) -> Event | None: Topic: tns1:VideoSource/GlobalSceneChange/* """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Global Scene Change", "binary_sensor", "problem", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -167,8 +172,9 @@ async def async_parse_detected_sound(uid: str, msg) -> Event | None: audio_source = "" audio_analytics = "" rule = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "AudioSourceConfigurationToken": audio_source = source.Value if source.Name == "AudioAnalyticsConfigurationToken": @@ -177,12 +183,12 @@ async def async_parse_detected_sound(uid: str, msg) -> Event | None: rule = source.Value return Event( - f"{uid}_{value_1}_{audio_source}_{audio_analytics}_{rule}", + f"{uid}_{topic_value}_{audio_source}_{audio_analytics}_{rule}", "Detected Sound", "binary_sensor", "sound", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -198,8 +204,9 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None: video_source = "" video_analytics = "" rule = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": @@ -208,12 +215,12 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None: rule = source.Value evt = Event( - f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}", + f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}", "Field Detection", "binary_sensor", "motion", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) return evt except (AttributeError, KeyError): @@ -230,8 +237,9 @@ async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None: video_source = "" video_analytics = "" rule = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": @@ -240,12 +248,12 @@ async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None: rule = source.Value return Event( - f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}", + f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}", "Cell Motion Detection", "binary_sensor", "motion", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -261,8 +269,9 @@ async def async_parse_motion_region_detector(uid: str, msg) -> Event | None: video_source = "" video_analytics = "" rule = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": @@ -271,12 +280,12 @@ async def async_parse_motion_region_detector(uid: str, msg) -> Event | None: rule = source.Value return Event( - f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}", + f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}", "Motion Region Detection", "binary_sensor", "motion", None, - value_1.Data.SimpleItem[0].Value in ["1", "true"], + message_value.Data.SimpleItem[0].Value in ["1", "true"], ) except (AttributeError, KeyError): return None @@ -292,8 +301,9 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event | None: video_source = "" video_analytics = "" rule = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": @@ -302,12 +312,12 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event | None: rule = source.Value return Event( - f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}", + f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}", "Tamper Detection", "binary_sensor", "problem", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -322,18 +332,19 @@ async def async_parse_dog_cat_detector(uid: str, msg) -> Event | None: """ try: video_source = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "Source": video_source = _normalize_video_source(source.Value) return Event( - f"{uid}_{value_1}_{video_source}", + f"{uid}_{topic_value}_{video_source}", "Pet Detection", "binary_sensor", "motion", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -347,18 +358,19 @@ async def async_parse_vehicle_detector(uid: str, msg) -> Event | None: """ try: video_source = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "Source": video_source = _normalize_video_source(source.Value) return Event( - f"{uid}_{value_1}_{video_source}", + f"{uid}_{topic_value}_{video_source}", "Vehicle Detection", "binary_sensor", "motion", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -372,18 +384,19 @@ async def async_parse_person_detector(uid: str, msg) -> Event | None: """ try: video_source = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "Source": video_source = _normalize_video_source(source.Value) return Event( - f"{uid}_{value_1}_{video_source}", + f"{uid}_{topic_value}_{video_source}", "Person Detection", "binary_sensor", "motion", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -397,18 +410,19 @@ async def async_parse_face_detector(uid: str, msg) -> Event | None: """ try: video_source = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "Source": video_source = _normalize_video_source(source.Value) return Event( - f"{uid}_{value_1}_{video_source}", + f"{uid}_{topic_value}_{video_source}", "Face Detection", "binary_sensor", "motion", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -422,18 +436,19 @@ async def async_parse_visitor_detector(uid: str, msg) -> Event | None: """ try: video_source = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "Source": video_source = _normalize_video_source(source.Value) return Event( - f"{uid}_{value_1}_{video_source}", + f"{uid}_{topic_value}_{video_source}", "Visitor Detection", "binary_sensor", "occupancy", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -446,15 +461,16 @@ async def async_parse_digital_input(uid: str, msg) -> Event | None: Topic: tns1:Device/Trigger/DigitalInput """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Digital Input", "binary_sensor", None, None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", ) except (AttributeError, KeyError): return None @@ -467,15 +483,16 @@ async def async_parse_relay(uid: str, msg) -> Event | None: Topic: tns1:Device/Trigger/Relay """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Relay Triggered", "binary_sensor", None, None, - value_1.Data.SimpleItem[0].Value == "active", + message_value.Data.SimpleItem[0].Value == "active", ) except (AttributeError, KeyError): return None @@ -488,15 +505,16 @@ async def async_parse_storage_failure(uid: str, msg) -> Event | None: Topic: tns1:Device/HardwareFailure/StorageFailure """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Storage Failure", "binary_sensor", "problem", None, - value_1.Data.SimpleItem[0].Value == "true", + message_value.Data.SimpleItem[0].Value == "true", EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -510,13 +528,14 @@ async def async_parse_processor_usage(uid: str, msg) -> Event | None: Topic: tns1:Monitoring/ProcessorUsage """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - usage = float(value_1.Data.SimpleItem[0].Value) + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + usage = float(message_value.Data.SimpleItem[0].Value) if usage <= 1: usage *= 100 return Event( - f"{uid}_{value_1}", + f"{uid}_{topic_value}", "Processor Usage", "sensor", None, @@ -535,10 +554,11 @@ async def async_parse_last_reboot(uid: str, msg) -> Event | None: Topic: tns1:Monitoring/OperatingTime/LastReboot """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value) + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value) return Event( - f"{uid}_{value_1}", + f"{uid}_{topic_value}", "Last Reboot", "sensor", "timestamp", @@ -557,10 +577,11 @@ async def async_parse_last_reset(uid: str, msg) -> Event | None: Topic: tns1:Monitoring/OperatingTime/LastReset """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value) + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value) return Event( - f"{uid}_{value_1}", + f"{uid}_{topic_value}", "Last Reset", "sensor", "timestamp", @@ -581,10 +602,11 @@ async def async_parse_backup_last(uid: str, msg) -> Event | None: """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value) + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value) return Event( - f"{uid}_{value_1}", + f"{uid}_{topic_value}", "Last Backup", "sensor", "timestamp", @@ -604,10 +626,11 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event | None: Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value) + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value) return Event( - f"{uid}_{value_1}", + f"{uid}_{topic_value}", "Last Clock Synchronization", "sensor", "timestamp", @@ -628,15 +651,16 @@ async def async_parse_jobstate(uid: str, msg) -> Event | None: """ try: - value_1 = msg.Message._value_1 # pylint: disable=protected-access - source = value_1.Source.SimpleItem[0].Value + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + source = message_value.Source.SimpleItem[0].Value return Event( - f"{uid}_{value_1}_{source}", + f"{uid}_{topic_value}_{source}", "Recording Job State", "binary_sensor", None, None, - value_1.Data.SimpleItem[0].Value == "Active", + message_value.Data.SimpleItem[0].Value == "Active", EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -653,8 +677,9 @@ async def async_parse_linedetector_crossed(uid: str, msg) -> Event | None: video_source = "" video_analytics = "" rule = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": video_source = source.Value if source.Name == "VideoAnalyticsConfigurationToken": @@ -663,12 +688,12 @@ async def async_parse_linedetector_crossed(uid: str, msg) -> Event | None: rule = source.Value return Event( - f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}", + f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}", "Line Detector Crossed", "sensor", None, None, - value_1.Data.SimpleItem[0].Value, + message_value.Data.SimpleItem[0].Value, EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -685,8 +710,9 @@ async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None: video_source = "" video_analytics = "" rule = "" - value_1 = msg.Message._value_1 # pylint: disable=protected-access - for source in value_1.Source.SimpleItem: + message_value = msg.Message._value_1 # pylint: disable=protected-access + topic_value = msg.Topic._value_1 # pylint: disable=protected-access + for source in message_value.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": @@ -695,12 +721,12 @@ async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None: rule = source.Value return Event( - f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}", + f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}", "Count Aggregation Counter", "sensor", None, None, - value_1.Data.SimpleItem[0].Value, + message_value.Data.SimpleItem[0].Value, EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): From f13059eaf58e4f6a3caee41fa4f906a5bec48a1d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Sep 2023 19:06:45 +0200 Subject: [PATCH 17/95] Pin pydantic to 1.10.12 (#101044) --- homeassistant/package_constraints.txt | 5 +++-- script/gen_requirements_all.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bf287f564cc..678195986e4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -126,8 +126,9 @@ multidict>=6.0.2 # Version 2.0 added typing, prevent accidental fallbacks backoff>=2.0 -# Require to avoid issues with decorators (#93904). v2 has breaking changes. -pydantic>=1.10.8,<2.0 +# Required to avoid breaking (#101042). +# v2 has breaking changes (#99218). +pydantic==1.10.12 # Breaks asyncio # https://github.com/pubnub/python/issues/130 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e0e00ebc958..a8bc99d68fa 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -128,8 +128,9 @@ multidict>=6.0.2 # Version 2.0 added typing, prevent accidental fallbacks backoff>=2.0 -# Require to avoid issues with decorators (#93904). v2 has breaking changes. -pydantic>=1.10.8,<2.0 +# Required to avoid breaking (#101042). +# v2 has breaking changes (#99218). +pydantic==1.10.12 # Breaks asyncio # https://github.com/pubnub/python/issues/130 From 081f194f6adb506368bf867ccb454b565b66130a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 28 Sep 2023 16:52:16 +0200 Subject: [PATCH 18/95] Update aioairzone-cloud to v0.2.3 (#101052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_diagnostics.ambr | 31 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 63d9d3fffaa..1a158fcd1fe 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_polling", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.2.2"] + "requirements": ["aioairzone-cloud==0.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c4b3c65ebaa..2c239ba27cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.2.2 +aioairzone-cloud==0.2.3 # homeassistant.components.airzone aioairzone==0.6.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82d5c52e6a6..53e0884fed9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.2.2 +aioairzone-cloud==0.2.3 # homeassistant.components.airzone aioairzone==0.6.8 diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index fb33323378a..44bd0e45e2a 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -113,6 +113,7 @@ 'active': True, 'available': True, 'humidity': 27, + 'id': 'group1', 'installation': 'installation1', 'mode': 2, 'modes': list([ @@ -144,6 +145,7 @@ 'aidoo1', ]), 'available': True, + 'id': 'grp2', 'installation': 'installation1', 'mode': 3, 'modes': list([ @@ -165,12 +167,41 @@ }), 'installations': dict({ 'installation1': dict({ + 'action': 1, + 'active': True, + 'aidoos': list([ + 'aidoo1', + ]), + 'available': True, + 'humidity': 27, 'id': 'installation1', + 'mode': 2, + 'modes': list([ + 1, + 2, + 3, + 4, + 5, + ]), 'name': 'House', + 'num-devices': 3, + 'power': True, + 'systems': list([ + 'system1', + ]), + 'temperature': 22.0, + 'temperature-setpoint': 23.3, + 'temperature-setpoint-max': 30.0, + 'temperature-setpoint-min': 15.0, + 'temperature-step': 0.5, 'web-servers': list([ 'webserver1', '11:22:33:44:55:67', ]), + 'zones': list([ + 'zone1', + 'zone2', + ]), }), }), 'systems': dict({ From ad8033c0f267d1d1b1c7b9435aa32248d5e5cd42 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Sep 2023 19:08:26 +0200 Subject: [PATCH 19/95] Don't show withings repair if it's not in YAML (#101054) --- homeassistant/components/withings/__init__.py | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 7b6a56995c8..44d32b0603c 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -76,29 +76,30 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Withings component.""" - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Withings", - }, - ) - if CONF_CLIENT_ID in config: - await async_import_client_credential( + if conf := config.get(DOMAIN): + async_create_issue( hass, - DOMAIN, - ClientCredential( - config[CONF_CLIENT_ID], - config[CONF_CLIENT_SECRET], - ), + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.4.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Withings", + }, ) + if CONF_CLIENT_ID in conf: + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], + ), + ) return True From 1bbd4662b71a3b010b05bb19ed5a3b7449c7ecd8 Mon Sep 17 00:00:00 2001 From: TJ Horner Date: Thu, 28 Sep 2023 10:07:22 -0700 Subject: [PATCH 20/95] Bump apple_weatherkit to 1.0.4 (#101057) --- homeassistant/components/weatherkit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherkit/manifest.json b/homeassistant/components/weatherkit/manifest.json index 34a5d45ca1f..d28a6ff3315 100644 --- a/homeassistant/components/weatherkit/manifest.json +++ b/homeassistant/components/weatherkit/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherkit", "iot_class": "cloud_polling", - "requirements": ["apple_weatherkit==1.0.3"] + "requirements": ["apple_weatherkit==1.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2c239ba27cb..c71a358fbd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -423,7 +423,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.weatherkit -apple_weatherkit==1.0.3 +apple_weatherkit==1.0.4 # homeassistant.components.apprise apprise==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53e0884fed9..1006ee60b0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -389,7 +389,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.weatherkit -apple_weatherkit==1.0.3 +apple_weatherkit==1.0.4 # homeassistant.components.apprise apprise==1.5.0 From 17362e19543a542a775e2ec7d22ae4e2c26a9b0c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 28 Sep 2023 12:07:00 -0500 Subject: [PATCH 21/95] Remove fma instructions from webrtc-noise-gain (#101060) --- homeassistant/components/assist_pipeline/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/assist_pipeline/manifest.json b/homeassistant/components/assist_pipeline/manifest.json index db6c517a81a..31b3b0d4e32 100644 --- a/homeassistant/components/assist_pipeline/manifest.json +++ b/homeassistant/components/assist_pipeline/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/assist_pipeline", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["webrtc-noise-gain==1.2.2"] + "requirements": ["webrtc-noise-gain==1.2.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 678195986e4..83e7f7d45b9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -50,7 +50,7 @@ typing-extensions>=4.8.0,<5.0 ulid-transform==0.8.1 voluptuous-serialize==2.6.0 voluptuous==0.13.1 -webrtc-noise-gain==1.2.2 +webrtc-noise-gain==1.2.3 yarl==1.9.2 zeroconf==0.115.0 diff --git a/requirements_all.txt b/requirements_all.txt index c71a358fbd6..56ab3940ca7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2700,7 +2700,7 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.assist_pipeline -webrtc-noise-gain==1.2.2 +webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1006ee60b0a..093b1cdc339 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2006,7 +2006,7 @@ wallbox==0.4.12 watchdog==2.3.1 # homeassistant.components.assist_pipeline -webrtc-noise-gain==1.2.2 +webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.4 From fff3c6c6e96434eaa27c009b89a25e6836c66f9e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Sep 2023 18:52:23 +0200 Subject: [PATCH 22/95] Bump aiowaqi to 1.1.0 (#99751) * Bump aiowaqi to 1.1.0 * Fix hassfest * Fix tests --- homeassistant/components/waqi/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/waqi/test_config_flow.py | 16 ++++++++-------- tests/components/waqi/test_sensor.py | 8 ++++---- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index bf31fb570a8..76e25225b7d 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", - "loggers": ["waqiasync"], - "requirements": ["aiowaqi==0.2.1"] + "loggers": ["aiowaqi"], + "requirements": ["aiowaqi==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 56ab3940ca7..413605ff7bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -372,7 +372,7 @@ aiovlc==0.1.0 aiovodafone==0.3.1 # homeassistant.components.waqi -aiowaqi==0.2.1 +aiowaqi==1.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 093b1cdc339..fce934a8068 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ aiovlc==0.1.0 aiovodafone==0.3.1 # homeassistant.components.waqi -aiowaqi==0.2.1 +aiowaqi==1.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/tests/components/waqi/test_config_flow.py b/tests/components/waqi/test_config_flow.py index be738a119e5..7a95e000d82 100644 --- a/tests/components/waqi/test_config_flow.py +++ b/tests/components/waqi/test_config_flow.py @@ -57,7 +57,7 @@ async def test_full_map_flow( "aiowaqi.WAQIClient.authenticate", ), patch( "aiowaqi.WAQIClient.get_by_ip", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): @@ -74,12 +74,12 @@ async def test_full_map_flow( "aiowaqi.WAQIClient.authenticate", ), patch( "aiowaqi.WAQIClient.get_by_coordinates", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ), patch( "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): @@ -133,7 +133,7 @@ async def test_flow_errors( "aiowaqi.WAQIClient.authenticate", ), patch( "aiowaqi.WAQIClient.get_by_ip", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): @@ -150,7 +150,7 @@ async def test_flow_errors( "aiowaqi.WAQIClient.authenticate", ), patch( "aiowaqi.WAQIClient.get_by_coordinates", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): @@ -220,7 +220,7 @@ async def test_error_in_second_step( "aiowaqi.WAQIClient.authenticate", ), patch( "aiowaqi.WAQIClient.get_by_ip", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): @@ -251,12 +251,12 @@ async def test_error_in_second_step( "aiowaqi.WAQIClient.authenticate", ), patch( "aiowaqi.WAQIClient.get_by_coordinates", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ), patch( "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): diff --git a/tests/components/waqi/test_sensor.py b/tests/components/waqi/test_sensor.py index 18f77028a29..ef434bcc544 100644 --- a/tests/components/waqi/test_sensor.py +++ b/tests/components/waqi/test_sensor.py @@ -36,7 +36,7 @@ async def test_legacy_migration(hass: HomeAssistant) -> None: """Test migration from yaml to config flow.""" search_result_json = json.loads(load_fixture("waqi/search_result.json")) search_results = [ - WAQISearchResult.parse_obj(search_result) + WAQISearchResult.from_dict(search_result) for search_result in search_result_json ] with patch( @@ -44,7 +44,7 @@ async def test_legacy_migration(hass: HomeAssistant) -> None: return_value=search_results, ), patch( "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): @@ -64,7 +64,7 @@ async def test_legacy_migration_already_imported( mock_config_entry.add_to_hass(hass) with patch( "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): @@ -98,7 +98,7 @@ async def test_sensor(hass: HomeAssistant, mock_config_entry: MockConfigEntry) - mock_config_entry.add_to_hass(hass) with patch( "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.parse_obj( + return_value=WAQIAirQuality.from_dict( json.loads(load_fixture("waqi/air_quality_sensor.json")) ), ): From d8f96d77093726b8642ce1f49b3c81655e5a31bf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Sep 2023 20:05:38 +0200 Subject: [PATCH 23/95] Bumped version to 2023.10.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0e659a58980..bfea6544b94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 8ed01169f89..11d2d7e54d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b1" +version = "2023.10.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2f6fefefa7d49ddf8e9cb547ebd965a20328b46d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 28 Sep 2023 20:48:07 +0200 Subject: [PATCH 24/95] Update frontend to 20230928.0 (#101067) --- 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 aa417b6e714..9f01fadb710 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230926.0"] + "requirements": ["home-assistant-frontend==20230928.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 83e7f7d45b9..13cc25cdf80 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ ha-av==10.1.1 hass-nabucasa==0.71.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230926.0 +home-assistant-frontend==20230928.0 home-assistant-intents==2023.9.22 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 413605ff7bb..b29ac30cef2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -997,7 +997,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230926.0 +home-assistant-frontend==20230928.0 # homeassistant.components.conversation home-assistant-intents==2023.9.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fce934a8068..21087755a99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -786,7 +786,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230926.0 +home-assistant-frontend==20230928.0 # homeassistant.components.conversation home-assistant-intents==2023.9.22 From 97448eff8f039aa7be47df3af8a1c737f87d1674 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 28 Sep 2023 13:38:33 -0700 Subject: [PATCH 25/95] Bump opower to 0.0.35 (#101072) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 002495b9517..71fd841d0fc 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.34"] + "requirements": ["opower==0.0.35"] } diff --git a/requirements_all.txt b/requirements_all.txt index b29ac30cef2..c1017ea9324 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1383,7 +1383,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.34 +opower==0.0.35 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21087755a99..eaa9282bb39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.34 +opower==0.0.35 # homeassistant.components.oralb oralb-ble==0.17.6 From 9c0bc57fede85e1a2fd206ed8fe86b3c76a21262 Mon Sep 17 00:00:00 2001 From: TJ Horner Date: Thu, 28 Sep 2023 14:24:07 -0700 Subject: [PATCH 26/95] Add native precipitation unit for weatherkit (#101073) --- homeassistant/components/weatherkit/weather.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/weatherkit/weather.py b/homeassistant/components/weatherkit/weather.py index 07745680b01..ce997fa500f 100644 --- a/homeassistant/components/weatherkit/weather.py +++ b/homeassistant/components/weatherkit/weather.py @@ -134,6 +134,7 @@ class WeatherKitWeather( _attr_native_pressure_unit = UnitOfPressure.MBAR _attr_native_visibility_unit = UnitOfLength.KILOMETERS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = UnitOfLength.MILLIMETERS def __init__( self, From 85838c6af95c1e719c1599e15c063f59d2bcb7a5 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 28 Sep 2023 15:30:43 -0500 Subject: [PATCH 27/95] Use wake word description if available (#101079) --- homeassistant/components/wyoming/wake_word.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wyoming/wake_word.py b/homeassistant/components/wyoming/wake_word.py index c9010425c52..d4cbd9b9263 100644 --- a/homeassistant/components/wyoming/wake_word.py +++ b/homeassistant/components/wyoming/wake_word.py @@ -46,7 +46,8 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity): wake_service = service.info.wake[0] self._supported_wake_words = [ - wake_word.WakeWord(id=ww.name, name=ww.name) for ww in wake_service.models + wake_word.WakeWord(id=ww.name, name=ww.description or ww.name) + for ww in wake_service.models ] self._attr_name = wake_service.name self._attr_unique_id = f"{config_entry.entry_id}-wake_word" From bae33799385882bf5d010ae8cc43d945f10906bc Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 29 Sep 2023 07:05:26 +0200 Subject: [PATCH 28/95] Update xknxproject to 3.3.0 (#101081) --- 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 a915d886138..b5c98c7203a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "platinum", "requirements": [ "xknx==2.11.2", - "xknxproject==3.2.0", + "xknxproject==3.3.0", "knx-frontend==2023.6.23.191712" ] } diff --git a/requirements_all.txt b/requirements_all.txt index c1017ea9324..a0b33962ea4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2736,7 +2736,7 @@ xiaomi-ble==0.21.1 xknx==2.11.2 # homeassistant.components.knx -xknxproject==3.2.0 +xknxproject==3.3.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eaa9282bb39..153083fbcf3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2039,7 +2039,7 @@ xiaomi-ble==0.21.1 xknx==2.11.2 # homeassistant.components.knx -xknxproject==3.2.0 +xknxproject==3.3.0 # homeassistant.components.bluesound # homeassistant.components.fritz From 3f57c33f3219b4b9e7bcef611cec9a68e4ba297c Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 29 Sep 2023 03:56:17 +0200 Subject: [PATCH 29/95] Fix ZHA exception when writing `cie_addr` during configuration (#101087) Fix ZHA exception when writing `cie_addr` --- .../components/zha/core/cluster_handlers/security.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/security.py b/homeassistant/components/zha/core/cluster_handlers/security.py index f31830f0bd8..9c74a14daa8 100644 --- a/homeassistant/components/zha/core/cluster_handlers/security.py +++ b/homeassistant/components/zha/core/cluster_handlers/security.py @@ -369,12 +369,11 @@ class IASZoneClusterHandler(ClusterHandler): ieee = self.cluster.endpoint.device.application.state.node_info.ieee try: - res = await self.write_attributes_safe({"cie_addr": ieee}) + await self.write_attributes_safe({"cie_addr": ieee}) self.debug( - "wrote cie_addr: %s to '%s' cluster: %s", + "wrote cie_addr: %s to '%s' cluster", str(ieee), self._cluster.ep_attribute, - res[0], ) except HomeAssistantError as ex: self.debug( From ef3bd0100c0941c5d28bad73dcda9ec1483b06dd Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 29 Sep 2023 22:04:00 -0500 Subject: [PATCH 30/95] Bump plexapi to 4.15.3 (#101088) * Bump plexapi to 4.15.3 * Update tests for updated account endpoint * Update tests for updated resources endpoint * Switch to non-web client fixture * Set __qualname__ attribute for new library behavior --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/plex/conftest.py | 10 +++- .../fixtures/player_plexhtpc_resources.xml | 3 ++ .../plex/fixtures/plextv_account.xml | 23 +++++---- .../fixtures/plextv_resources_one_server.xml | 40 +++++++++------- .../fixtures/plextv_resources_two_servers.xml | 48 +++++++++++-------- tests/components/plex/test_config_flow.py | 12 ++--- tests/components/plex/test_init.py | 10 ++-- tests/components/plex/test_media_players.py | 4 +- tests/components/plex/test_media_search.py | 11 ++++- tests/components/plex/test_playback.py | 40 ++++++++++++---- tests/components/plex/test_sensor.py | 30 ++++++++++-- tests/components/plex/test_services.py | 6 ++- 15 files changed, 159 insertions(+), 84 deletions(-) create mode 100644 tests/components/plex/fixtures/player_plexhtpc_resources.xml diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index bc0c54c49bf..6cf94793173 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.13.2", + "PlexAPI==4.15.3", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index a0b33962ea4..1c3b3b58098 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ Mastodon.py==1.5.1 Pillow==10.0.0 # homeassistant.components.plex -PlexAPI==4.13.2 +PlexAPI==4.15.3 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 153083fbcf3..986828f170c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -42,7 +42,7 @@ HATasmota==0.7.3 Pillow==10.0.0 # homeassistant.components.plex -PlexAPI==4.13.2 +PlexAPI==4.15.3 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index e4bf61ccd94..78a3b7387ea 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -232,6 +232,12 @@ def player_plexweb_resources_fixture(): return load_fixture("plex/player_plexweb_resources.xml") +@pytest.fixture(name="player_plexhtpc_resources", scope="session") +def player_plexhtpc_resources_fixture(): + """Load resources payload for a Plex HTPC player and return it.""" + return load_fixture("plex/player_plexhtpc_resources.xml") + + @pytest.fixture(name="playlists", scope="session") def playlists_fixture(): """Load payload for all playlists and return it.""" @@ -450,8 +456,8 @@ def mock_plex_calls( """Mock Plex API calls.""" requests_mock.get("https://plex.tv/api/users/", text=plextv_shared_users) requests_mock.get("https://plex.tv/api/invites/requested", text=empty_payload) - requests_mock.get("https://plex.tv/users/account", text=plextv_account) - requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) + requests_mock.get("https://plex.tv/api/v2/user", text=plextv_account) + requests_mock.get("https://plex.tv/api/v2/resources", text=plextv_resources) url = plex_server_url(entry) diff --git a/tests/components/plex/fixtures/player_plexhtpc_resources.xml b/tests/components/plex/fixtures/player_plexhtpc_resources.xml new file mode 100644 index 00000000000..6cc9cc0afbd --- /dev/null +++ b/tests/components/plex/fixtures/player_plexhtpc_resources.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/components/plex/fixtures/plextv_account.xml b/tests/components/plex/fixtures/plextv_account.xml index 32d6eec7c2d..b47896de577 100644 --- a/tests/components/plex/fixtures/plextv_account.xml +++ b/tests/components/plex/fixtures/plextv_account.xml @@ -1,15 +1,18 @@ - - - + + + + + + + + + - - - - testuser - testuser@email.com - 2000-01-01 12:34:56 UTC - faketoken + + + + diff --git a/tests/components/plex/fixtures/plextv_resources_one_server.xml b/tests/components/plex/fixtures/plextv_resources_one_server.xml index ff2e458ff24..75b7e54b7e6 100644 --- a/tests/components/plex/fixtures/plextv_resources_one_server.xml +++ b/tests/components/plex/fixtures/plextv_resources_one_server.xml @@ -1,18 +1,22 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/components/plex/fixtures/plextv_resources_two_servers.xml b/tests/components/plex/fixtures/plextv_resources_two_servers.xml index 7da5df4c1df..f14b55fe161 100644 --- a/tests/components/plex/fixtures/plextv_resources_two_servers.xml +++ b/tests/components/plex/fixtures/plextv_resources_two_servers.xml @@ -1,21 +1,27 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index beb454e2e9c..235596715f4 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -143,7 +143,7 @@ async def test_no_servers_found( current_request_with_host: None, ) -> None: """Test when no servers are on an account.""" - requests_mock.get("https://plex.tv/api/resources", text=empty_payload) + requests_mock.get("https://plex.tv/api/v2/resources", text=empty_payload) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -225,7 +225,7 @@ async def test_multiple_servers_with_selection( assert result["step_id"] == "user" requests_mock.get( - "https://plex.tv/api/resources", + "https://plex.tv/api/v2/resources", text=plextv_resources_two_servers, ) with patch("plexauth.PlexAuth.initiate_auth"), patch( @@ -289,7 +289,7 @@ async def test_adding_last_unconfigured_server( assert result["step_id"] == "user" requests_mock.get( - "https://plex.tv/api/resources", + "https://plex.tv/api/v2/resources", text=plextv_resources_two_servers, ) @@ -346,9 +346,9 @@ async def test_all_available_servers_configured( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.get("https://plex.tv/api/v2/user", text=plextv_account) requests_mock.get( - "https://plex.tv/api/resources", + "https://plex.tv/api/v2/resources", text=plextv_resources_two_servers, ) @@ -776,7 +776,7 @@ async def test_reauth_multiple_servers_available( ) -> None: """Test setup and reauthorization of a Plex token when multiple servers are available.""" requests_mock.get( - "https://plex.tv/api/resources", + "https://plex.tv/api/v2/resources", text=plextv_resources_two_servers, ) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index bc43a1e0d89..6e1043b5c52 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -231,7 +231,7 @@ async def test_setup_when_certificate_changed( # Test with account failure requests_mock.get( - "https://plex.tv/users/account", status_code=HTTPStatus.UNAUTHORIZED + "https://plex.tv/api/v2/user", status_code=HTTPStatus.UNAUTHORIZED ) old_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(old_entry.entry_id) is False @@ -241,8 +241,8 @@ async def test_setup_when_certificate_changed( await hass.config_entries.async_unload(old_entry.entry_id) # Test with no servers found - requests_mock.get("https://plex.tv/users/account", text=plextv_account) - requests_mock.get("https://plex.tv/api/resources", text=empty_payload) + requests_mock.get("https://plex.tv/api/v2/user", text=plextv_account) + requests_mock.get("https://plex.tv/api/v2/resources", text=empty_payload) assert await hass.config_entries.async_setup(old_entry.entry_id) is False await hass.async_block_till_done() @@ -252,7 +252,7 @@ async def test_setup_when_certificate_changed( # Test with success new_url = PLEX_DIRECT_URL - requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) + requests_mock.get("https://plex.tv/api/v2/resources", text=plextv_resources) for resource_url in [new_url, "http://1.2.3.4:32400"]: requests_mock.get(resource_url, text=plex_server_default) requests_mock.get(f"{new_url}/accounts", text=plex_server_accounts) @@ -287,7 +287,7 @@ async def test_bad_token_with_tokenless_server( ) -> None: """Test setup with a bad token and a server with token auth disabled.""" requests_mock.get( - "https://plex.tv/users/account", status_code=HTTPStatus.UNAUTHORIZED + "https://plex.tv/api/v2/user", status_code=HTTPStatus.UNAUTHORIZED ) await setup_plex_server() diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index 27fea36e3b0..e9efc945f71 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -12,10 +12,10 @@ async def test_plex_tv_clients( entry, setup_plex_server, requests_mock: requests_mock.Mocker, - player_plexweb_resources, + player_plexhtpc_resources, ) -> None: """Test getting Plex clients from plex.tv.""" - requests_mock.get("/resources", text=player_plexweb_resources) + requests_mock.get("/resources", text=player_plexhtpc_resources) with patch("plexapi.myplex.MyPlexResource.connect", side_effect=NotFound): await setup_plex_server() diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py index 0cc94134f1c..21b50724786 100644 --- a/tests/components/plex/test_media_search.py +++ b/tests/components/plex/test_media_search.py @@ -70,7 +70,10 @@ async def test_media_lookups( ) assert "Library 'Not a Library' not found in" in str(excinfo.value) - with patch("plexapi.library.LibrarySection.search") as search: + with patch( + "plexapi.library.LibrarySection.search", + __qualname__="search", + ) as search: await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -261,7 +264,11 @@ async def test_media_lookups( with pytest.raises(MediaNotFound) as excinfo: payload = '{"library_name": "Movies", "title": "Not a Movie"}' - with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): + with patch( + "plexapi.library.LibrarySection.search", + side_effect=BadRequest, + __qualname__="search", + ): await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index c9dba4e4aca..9ea684256c4 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -49,14 +49,14 @@ async def test_media_player_playback( setup_plex_server, requests_mock: requests_mock.Mocker, playqueue_created, - player_plexweb_resources, + player_plexhtpc_resources, ) -> None: """Test playing media on a Plex media_player.""" - requests_mock.get("http://1.2.3.5:32400/resources", text=player_plexweb_resources) + requests_mock.get("http://1.2.3.6:32400/resources", text=player_plexhtpc_resources) await setup_plex_server() - media_player = "media_player.plex_plex_web_chrome" + media_player = "media_player.plex_plex_htpc_for_mac_plex_htpc" requests_mock.post("/playqueues", text=playqueue_created) playmedia_mock = requests_mock.get( "/player/playback/playMedia", status_code=HTTPStatus.OK @@ -65,7 +65,9 @@ async def test_media_player_playback( # Test media lookup failure payload = '{"library_name": "Movies", "title": "Movie 1" }' with patch( - "plexapi.library.LibrarySection.search", return_value=None + "plexapi.library.LibrarySection.search", + return_value=None, + __qualname__="search", ), pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( MP_DOMAIN, @@ -86,7 +88,11 @@ async def test_media_player_playback( # Test movie success movies = [movie1] - with patch("plexapi.library.LibrarySection.search", return_value=movies): + with patch( + "plexapi.library.LibrarySection.search", + return_value=movies, + __qualname__="search", + ): await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -101,7 +107,11 @@ async def test_media_player_playback( # Test movie success with resume playmedia_mock.reset() - with patch("plexapi.library.LibrarySection.search", return_value=movies): + with patch( + "plexapi.library.LibrarySection.search", + return_value=movies, + __qualname__="search", + ): await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -163,7 +173,11 @@ async def test_media_player_playback( # Test multiple choices with exact match playmedia_mock.reset() movies = [movie1, movie2] - with patch("plexapi.library.LibrarySection.search", return_value=movies): + with patch( + "plexapi.library.LibrarySection.search", + return_value=movies, + __qualname__="search", + ): await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -181,7 +195,11 @@ async def test_media_player_playback( movies = [movie2, movie3] with pytest.raises(HomeAssistantError) as excinfo: payload = '{"library_name": "Movies", "title": "Movie" }' - with patch("plexapi.library.LibrarySection.search", return_value=movies): + with patch( + "plexapi.library.LibrarySection.search", + return_value=movies, + __qualname__="search", + ): await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -197,7 +215,11 @@ async def test_media_player_playback( # Test multiple choices with allow_multiple movies = [movie1, movie2, movie3] - with patch("plexapi.library.LibrarySection.search", return_value=movies), patch( + with patch( + "plexapi.library.LibrarySection.search", + return_value=movies, + __qualname__="search", + ), patch( "homeassistant.components.plex.server.PlexServer.create_playqueue" ) as mock_create_playqueue: await hass.services.async_call( diff --git a/tests/components/plex/test_sensor.py b/tests/components/plex/test_sensor.py index 9c73bf9f915..5b9729792f4 100644 --- a/tests/components/plex/test_sensor.py +++ b/tests/components/plex/test_sensor.py @@ -129,7 +129,11 @@ async def test_library_sensor_values( ) media = [MockPlexTVEpisode()] - with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + with patch( + "plexapi.library.LibrarySection.recentlyAdded", + return_value=media, + __qualname__="recentlyAdded", + ): await hass.async_block_till_done() library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") @@ -165,7 +169,11 @@ async def test_library_sensor_values( trigger_plex_update( mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD ) - with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + with patch( + "plexapi.library.LibrarySection.recentlyAdded", + return_value=media, + __qualname__="recentlyAdded", + ): await hass.async_block_till_done() library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") @@ -200,7 +208,11 @@ async def test_library_sensor_values( ) media = [MockPlexMovie()] - with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + with patch( + "plexapi.library.LibrarySection.recentlyAdded", + return_value=media, + __qualname__="recentlyAdded", + ): await hass.async_block_till_done() library_movies_sensor = hass.states.get("sensor.plex_server_1_library_movies") @@ -210,7 +222,11 @@ async def test_library_sensor_values( # Test with clip media = [MockPlexClip()] - with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + with patch( + "plexapi.library.LibrarySection.recentlyAdded", + return_value=media, + __qualname__="recentlyAdded", + ): async_dispatcher_send( hass, PLEX_UPDATE_LIBRARY_SIGNAL.format(mock_plex_server.machine_identifier) ) @@ -236,7 +252,11 @@ async def test_library_sensor_values( ) media = [MockPlexMusic()] - with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + with patch( + "plexapi.library.LibrarySection.recentlyAdded", + return_value=media, + __qualname__="recentlyAdded", + ): await hass.async_block_till_done() library_music_sensor = hass.states.get("sensor.plex_server_1_library_music") diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index a74b3e91460..dfd02bb1d3f 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -190,7 +190,11 @@ async def test_lookup_media_for_other_integrations( assert result.shuffle # Test with media not found - with patch("plexapi.library.LibrarySection.search", return_value=None): + with patch( + "plexapi.library.LibrarySection.search", + return_value=None, + __qualname__="search", + ): with pytest.raises(HomeAssistantError) as excinfo: process_plex_payload(hass, MediaType.MUSIC, CONTENT_ID_BAD_MEDIA) assert f"No {MediaType.MUSIC} results in 'Music' for" in str(excinfo.value) From bfd727597272aad2af316dde7bf108d85f541c3f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Sep 2023 08:58:05 +0200 Subject: [PATCH 31/95] Update Home Assistant base image to 2023.09.0 (#101092) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index cc13a4e595f..f9e19f89e23 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.08.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.08.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.08.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.08.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.08.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.09.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.09.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.09.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.09.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.09.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 73356ae2325ee6cc4cd4e35f5ddaef0ceec27b96 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Sep 2023 09:30:00 +0200 Subject: [PATCH 32/95] Use pep 503 compatible wheels index for builds (#101096) --- Dockerfile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index e229f27cb33..f2a365b2b8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,9 +15,8 @@ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ RUN \ pip3 install \ --no-cache-dir \ - --no-index \ --only-binary=:all: \ - --find-links "${WHEELS_LINKS}" \ + --index-url "https://wheels.home-assistant.io/musllinux-index/" \ -r homeassistant/requirements.txt COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ @@ -39,9 +38,8 @@ RUN \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ pip3 install \ --no-cache-dir \ - --no-index \ --only-binary=:all: \ - --find-links "${WHEELS_LINKS}" \ + --index-url "https://wheels.home-assistant.io/musllinux-index/" \ -r homeassistant/requirements_all.txt ## Setup Home Assistant Core @@ -49,9 +47,8 @@ COPY . homeassistant/ RUN \ pip3 install \ --no-cache-dir \ - --no-index \ --only-binary=:all: \ - --find-links "${WHEELS_LINKS}" \ + --index-url "https://wheels.home-assistant.io/musllinux-index/" \ -e ./homeassistant \ && python3 -m compileall \ homeassistant/homeassistant From 2cc229ce420ca72f17fb8b46ed021f7eb074ce76 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:49:19 +0200 Subject: [PATCH 33/95] Fix circular dependency on homeassistant (#101099) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13cc25cdf80..61b6de913d5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -173,3 +173,7 @@ pysnmp==1000000000.0.0 # The get-mac package has been replaced with getmac. Installing get-mac alongside getmac # breaks getmac due to them both sharing the same python package name inside 'getmac'. get-mac==1000000000.0.0 + +# Circular dependency on homeassistant itself +# https://gitlab.com/keatontaylor/alexapy/-/blob/v1.27.0/pyproject.toml#L29 +alexapy<1.27.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a8bc99d68fa..4291d2c6e2f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -175,6 +175,10 @@ pysnmp==1000000000.0.0 # The get-mac package has been replaced with getmac. Installing get-mac alongside getmac # breaks getmac due to them both sharing the same python package name inside 'getmac'. get-mac==1000000000.0.0 + +# Circular dependency on homeassistant itself +# https://gitlab.com/keatontaylor/alexapy/-/blob/v1.27.0/pyproject.toml#L29 +alexapy<1.27.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 1d2c570a018b44b7b2db1b4fe5b231cb10d1190d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Sep 2023 12:58:29 +0200 Subject: [PATCH 34/95] Ignore binary distribution wheels for charset-normalizer (#101104) --- .github/workflows/wheels.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6c3022b194b..85912623f61 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -186,7 +186,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtaa" @@ -200,7 +200,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtab" @@ -214,7 +214,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtac" From 65c7b307202fa9a2a8e2cdc5a281cd92aff08d3e Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sat, 30 Sep 2023 10:43:07 +0200 Subject: [PATCH 35/95] Stop the Home Assistant Core container by default (#101105) --- rootfs/etc/services.d/home-assistant/finish | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish index 057957a9c03..ae5b17e171a 100755 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -18,13 +18,11 @@ elif [[ ${APP_EXIT_CODE} -eq ${SIGNAL_EXIT_CODE} ]]; then NEW_EXIT_CODE=$((128 + SIGNAL_NO)) echo ${NEW_EXIT_CODE} > /run/s6-linux-init-container-results/exitcode - - if [[ ${SIGNAL_NO} -eq ${SIGTERM} ]]; then - /run/s6/basedir/bin/halt - fi else bashio::log.info "Home Assistant Core service shutdown" echo ${APP_EXIT_CODE} > /run/s6-linux-init-container-results/exitcode - /run/s6/basedir/bin/halt fi + +# Make sure to stop the container +/run/s6/basedir/bin/halt From 124eda6906cc8b9d4b4aad6fe6cf94eced3b51ea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Sep 2023 13:18:33 +0200 Subject: [PATCH 36/95] Correct binary ignore for charset-normalizer to charset_normalizer (#101106) --- .github/workflows/wheels.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85912623f61..25245795c56 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -186,7 +186,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset_normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtaa" @@ -200,7 +200,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset_normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtab" @@ -214,7 +214,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset_normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtac" From c1ade85d655831eeefdcf46b97be388b39860800 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Sep 2023 14:43:03 +0200 Subject: [PATCH 37/95] Pin charset-normalizer in our package constraints (#101107) --- .github/workflows/wheels.yml | 6 +++--- homeassistant/package_constraints.txt | 5 +++++ script/gen_requirements_all.py | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 25245795c56..85912623f61 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -186,7 +186,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;charset_normalizer;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtaa" @@ -200,7 +200,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;charset_normalizer;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtab" @@ -214,7 +214,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;charset_normalizer;grpcio;SQLAlchemy;protobuf + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtac" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 61b6de913d5..4f5868e2fff 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -177,3 +177,8 @@ get-mac==1000000000.0.0 # Circular dependency on homeassistant itself # https://gitlab.com/keatontaylor/alexapy/-/blob/v1.27.0/pyproject.toml#L29 alexapy<1.27.0 + +# We want to skip the binary wheels for the 'charset-normalizer' packages. +# They are build with mypyc, but causes issues with our wheel builder. +# In order to do so, we need to constrain the version. +charset-normalizer==3.2.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4291d2c6e2f..e87e8b16bcb 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -179,6 +179,11 @@ get-mac==1000000000.0.0 # Circular dependency on homeassistant itself # https://gitlab.com/keatontaylor/alexapy/-/blob/v1.27.0/pyproject.toml#L29 alexapy<1.27.0 + +# We want to skip the binary wheels for the 'charset-normalizer' packages. +# They are build with mypyc, but causes issues with our wheel builder. +# In order to do so, we need to constrain the version. +charset-normalizer==3.2.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From d84d83a42ae1718cdc21212503a97483fe048df0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 29 Sep 2023 19:30:35 +0200 Subject: [PATCH 38/95] Migrate WAQI unique id (#101112) * Migrate unique_id * Add docstring --- homeassistant/components/waqi/__init__.py | 16 +++++++++++++ homeassistant/components/waqi/sensor.py | 2 +- tests/components/waqi/test_sensor.py | 29 ++++++++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waqi/__init__.py b/homeassistant/components/waqi/__init__.py index bc51a91364c..d3cf1af21a2 100644 --- a/homeassistant/components/waqi/__init__.py +++ b/homeassistant/components/waqi/__init__.py @@ -7,6 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.entity_registry as er from .const import DOMAIN from .coordinator import WAQIDataUpdateCoordinator @@ -17,6 +18,8 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up World Air Quality Index (WAQI) from a config entry.""" + await _migrate_unique_ids(hass, entry) + client = WAQIClient(session=async_get_clientsession(hass)) client.authenticate(entry.data[CONF_API_KEY]) @@ -35,3 +38,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def _migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Migrate pre-config flow unique ids.""" + entity_registry = er.async_get(hass) + registry_entries = er.async_entries_for_config_entry( + entity_registry, entry.entry_id + ) + for reg_entry in registry_entries: + if isinstance(reg_entry.unique_id, int): + entity_registry.async_update_entity( + reg_entry.entity_id, new_unique_id=f"{reg_entry.unique_id}_air_quality" + ) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 0ad295ca5af..62170b329f4 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -159,7 +159,7 @@ class WaqiSensor(CoordinatorEntity[WAQIDataUpdateCoordinator], SensorEntity): """Initialize the sensor.""" super().__init__(coordinator) self._attr_name = f"WAQI {self.coordinator.data.city.name}" - self._attr_unique_id = str(coordinator.data.station_id) + self._attr_unique_id = f"{coordinator.data.station_id}_air_quality" @property def native_value(self) -> int | None: diff --git a/tests/components/waqi/test_sensor.py b/tests/components/waqi/test_sensor.py index ef434bcc544..7feb37a1b09 100644 --- a/tests/components/waqi/test_sensor.py +++ b/tests/components/waqi/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import patch from aiowaqi import WAQIAirQuality, WAQIError, WAQISearchResult +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.waqi.const import CONF_STATION_NUMBER, DOMAIN from homeassistant.components.waqi.sensor import CONF_LOCATIONS, CONF_STATIONS from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntryState @@ -15,7 +16,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture @@ -93,6 +94,32 @@ async def test_legacy_migration_already_imported( assert len(issue_registry.issues) == 1 +async def test_sensor_id_migration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test migrating unique id for original sensor.""" + mock_config_entry.add_to_hass(hass) + entity_registry = er.async_get(hass) + entity_registry.async_get_or_create( + SENSOR_DOMAIN, DOMAIN, 4584, config_entry=mock_config_entry + ) + with patch( + "aiowaqi.WAQIClient.get_by_station_number", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + entities = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + assert len(entities) == 1 + assert hass.states.get("sensor.waqi_4584") + assert hass.states.get("sensor.waqi_de_jongweg_utrecht") is None + assert entities[0].unique_id == "4584_air_quality" + + async def test_sensor(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None: """Test failed update.""" mock_config_entry.add_to_hass(hass) From d216fbddae5c859bfc9f19e3c7bc1e81d679c27a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 29 Sep 2023 19:12:27 +0200 Subject: [PATCH 39/95] Add logging to media extractor to know the selected stream (#101117) --- homeassistant/components/media_extractor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index c6f899c4909..89f0a11ba61 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -153,7 +153,7 @@ class MediaExtractor: except MEQueryException: _LOGGER.error("Wrong query format: %s", stream_query) return - + _LOGGER.debug("Selected the following stream: %s", stream_url) data = {k: v for k, v in self.call_data.items() if k != ATTR_ENTITY_ID} data[ATTR_MEDIA_CONTENT_ID] = stream_url From b5eb1586974a6f7c8d894c8a03b0aa06f52ad2e1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 29 Sep 2023 19:09:18 +0200 Subject: [PATCH 40/95] Correct youtube stream selector in media extractor (#101119) --- .../components/media_extractor/__init__.py | 13 ++++++++++--- .../media_extractor/snapshots/test_init.ambr | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 89f0a11ba61..328871cf78c 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -193,9 +193,16 @@ def get_best_stream(formats: list[dict[str, Any]]) -> str: def get_best_stream_youtube(formats: list[dict[str, Any]]) -> str: - """YouTube requests also include manifest files. + """YouTube responses also include files with only video or audio. - They don't have a filesize so we skip all formats without filesize. + So we filter on files with both audio and video codec. """ - return get_best_stream([format for format in formats if "filesize" in format]) + return get_best_stream( + [ + format + for format in formats + if format.get("acodec", "none") != "none" + and format.get("vcodec", "none") != "none" + ] + ) diff --git a/tests/components/media_extractor/snapshots/test_init.ambr b/tests/components/media_extractor/snapshots/test_init.ambr index 56162ca3040..d70c370b60c 100644 --- a/tests/components/media_extractor/snapshots/test_init.ambr +++ b/tests/components/media_extractor/snapshots/test_init.ambr @@ -6,7 +6,7 @@ ]), 'extra': dict({ }), - 'media_content_id': 'https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1694805294&ei=zlgEZcCPFpqOx_APj42f2Ao&ip=45.93.75.130&id=o-AJK-SE-1BW0w1_4zhkyevHLKWnD0vrRBPNot5eVH0ogM&itag=248&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-aigzrnld&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=2095000&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=40874930&dur=212.040&lmt=1694044655610179&mt=1694783390&fvip=1&keepalive=yes&fexp=24007246%2C24362685&beids=24350017&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAJ-5AjGgFTR1w-qObfMtwCvs07CU5OUDG7bsNqAXrZMxAiEA4pJO9wj-ZQTqFHg5OP2_XZIJbog8NvY8BVSwENMwJfM%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRAIgMFD0fR8NqzBiP481IpIhnKJjW4Z2fLVfgKt5-OsWbxICICLr46c0ycoE_Ngo3heXuwdOWXs0nyZXegtnP5uHLJSb', + 'media_content_id': 'https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1694805294&ei=zlgEZaLeHcrlgAeFhLrYBA&ip=45.93.75.130&id=o-AFIa6Sil61_wuEFkUVhjKkr-0pyzj2cHi52leur2vR1j&itag=22&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=2095000&spc=UWF9f2Ob7Uhbkv1q69SZBYEqtijLGjs&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045086815467&mt=1694783390&fvip=3&fexp=24007246%2C24362685&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgUiMmQEGPqT5Hb00S74LeTwF4PCN31mwbC_fUNSejdsQCIF2D11o2OXBxoLlOX00vyB1wfYLIo6dBnodrfYc9gH6y&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAI4QpoB0iBj-oMiNFMMdN0RN-u3nLji437a3jqTbhncSAiEAlvsdhJjG0-VZ2jCjyUZBtidBcUzYFwnk6qG7mIiNjCA%3D', 'media_content_type': 'VIDEO', }) # --- @@ -87,7 +87,7 @@ 'entity_id': 'media_player.bedroom', 'extra': dict({ }), - 'media_content_id': 'https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1694805268&ei=tFgEZcu0DoOD-gaqg47wBA&ip=45.93.75.130&id=o-ALADwM6dkuCPsPIQiQ_ygvtMcP-xvew7ntgwcwtzWc4N&itag=248&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hne6nzy%2Csn-5hnekn7k&ms=au%2Crdu&mv=m&mvi=3&pl=22&initcwndbps=1957500&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=40874930&dur=212.040&lmt=1694044655610179&mt=1694783146&fvip=2&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgT7VwysCFd3nXvaSSiJoVxkNj5jfMPSeitLsQmy_S1b4CIQDWFiZSIH3tV4hQRtHa9DbzdYL8RQpbKD_6aeNZ7t-3IA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRAIgHX4-RXGLMMOGBkRk1sGy7XnQ3wkahwF60RoxGmOabF0CIBpQjZOMeQQeqZX8JccDZAypFCP3chfxrtgzsfWCJJ0l', + 'media_content_id': 'https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1694805268&ei=tFgEZaHmFN2Px_AP2tSt2AQ&ip=45.93.75.130&id=o-AEj4DudORoGviGzjggo2mjXrQpjRh8L2BrOU-wekY859&itag=22&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hne6nzy%2Csn-5hnekn7k&ms=au%2Crdu&mv=m&mvi=3&pl=22&initcwndbps=1957500&spc=UWF9f7_CV3gS4VV2VFq7hgxtUAyOlog&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045086815467&mt=1694783146&fvip=2&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAO2IJciEtkI3PvYyVC_zkyo61I70wYJQXuGOMueeacrKAiA-UAdaJSlqqkfaa6QtqVnC_BJJZn7BXs85gh_fdbGoSg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIgRCGi20K-ZvdukYkBZOidcHpGPUpIBOkw-jZGEncsKQECIQC5h-rCfQhDTQFqocOTtQXcNZVA54oIqjweF0mN5GpzFA%3D%3D', 'media_content_type': 'VIDEO', }) # --- @@ -105,7 +105,7 @@ 'entity_id': 'media_player.bedroom', 'extra': dict({ }), - 'media_content_id': 'https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1694805294&ei=zlgEZcCPFpqOx_APj42f2Ao&ip=45.93.75.130&id=o-AJK-SE-1BW0w1_4zhkyevHLKWnD0vrRBPNot5eVH0ogM&itag=248&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-aigzrnld&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=2095000&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=40874930&dur=212.040&lmt=1694044655610179&mt=1694783390&fvip=1&keepalive=yes&fexp=24007246%2C24362685&beids=24350017&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAJ-5AjGgFTR1w-qObfMtwCvs07CU5OUDG7bsNqAXrZMxAiEA4pJO9wj-ZQTqFHg5OP2_XZIJbog8NvY8BVSwENMwJfM%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRAIgMFD0fR8NqzBiP481IpIhnKJjW4Z2fLVfgKt5-OsWbxICICLr46c0ycoE_Ngo3heXuwdOWXs0nyZXegtnP5uHLJSb', + 'media_content_id': 'https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1694805294&ei=zlgEZaLeHcrlgAeFhLrYBA&ip=45.93.75.130&id=o-AFIa6Sil61_wuEFkUVhjKkr-0pyzj2cHi52leur2vR1j&itag=22&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=2095000&spc=UWF9f2Ob7Uhbkv1q69SZBYEqtijLGjs&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045086815467&mt=1694783390&fvip=3&fexp=24007246%2C24362685&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgUiMmQEGPqT5Hb00S74LeTwF4PCN31mwbC_fUNSejdsQCIF2D11o2OXBxoLlOX00vyB1wfYLIo6dBnodrfYc9gH6y&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAI4QpoB0iBj-oMiNFMMdN0RN-u3nLji437a3jqTbhncSAiEAlvsdhJjG0-VZ2jCjyUZBtidBcUzYFwnk6qG7mIiNjCA%3D', 'media_content_type': 'VIDEO', }) # --- @@ -114,7 +114,7 @@ 'entity_id': 'media_player.bedroom', 'extra': dict({ }), - 'media_content_id': 'https://rr2---sn-5hne6nzk.googlevideo.com/videoplayback?expire=1694818322&ei=sosEZcmcMdGVgQeatIDABA&ip=45.93.75.130&id=o-ANZGIl8-Lo8u8x_fU-l5VosaHna8zx8_6Ab0CCT-vzjQ&itag=243&source=youtube&requiressl=yes&mh=6Q&mm=31%2C29&mn=sn-5hne6nzk%2Csn-5hnednss&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1868750&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=104373&dur=9.009&lmt=1660945832037331&mt=1694796392&fvip=5&keepalive=yes&fexp=24007246&c=IOS&txp=4437434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAMLnlCaLvJ2scyVr6qYrCp3rzn_Op9eerIVWyp62NXKIAiEAnswRfxH5KssHQAKETF2MPncVWX_eDgpTXBEHN589-Xo%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAN9Und25H4_kUjcAoZ_LVv0lAVTnPDkI-t5f7JJBA_jhAiAsXrF-84K_iBGiTwIwXS_eOlp5JPXxLEhyDj_cB8zdxQ%3D%3D', + 'media_content_id': 'https://rr2---sn-5hne6nzk.googlevideo.com/videoplayback?expire=1694818322&ei=sosEZfXrN8mrx_APirihiAo&ip=45.93.75.130&id=o-AK8fF61bmcIHhl_2kv1XxpCtdRixUPDqG0y6aunrwcZa&itag=18&source=youtube&requiressl=yes&mh=6Q&mm=31%2C29&mn=sn-5hne6nzk%2Csn-5hnednss&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1868750&spc=UWF9f0JgCQlRLpY93JZnveUdoMCdkmY&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=9.055&lmt=1665508348849369&mt=1694796392&fvip=5&fexp=24007246&beids=24350017&c=ANDROID&txp=4438434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRQIhALn143d2vS16xd_ndXj_rB8QOeHSCHC9YxSeOaRMF9eWAiAaYxqrRyV5bREBHLPCrs8Wk8Msm3hJrj11OJc2RIEyzw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAKy1C4o9YUyi7o2_03UfJ8n8vXWgF4t8zB-4FXiAtJ5uAiEAh2chtgFo6quycJIs1kagkaa_AAQbEFrnFU1xEUDEqp4%3D', 'media_content_type': 'VIDEO', }) # --- From 730acb34f2c3ff8f0e3a08e15f0829e89697cff8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Sep 2023 19:40:13 +0200 Subject: [PATCH 41/95] Revert pin on AlexaPy (#101123) --- homeassistant/package_constraints.txt | 4 ---- script/gen_requirements_all.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4f5868e2fff..d6f923f0047 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -174,10 +174,6 @@ pysnmp==1000000000.0.0 # breaks getmac due to them both sharing the same python package name inside 'getmac'. get-mac==1000000000.0.0 -# Circular dependency on homeassistant itself -# https://gitlab.com/keatontaylor/alexapy/-/blob/v1.27.0/pyproject.toml#L29 -alexapy<1.27.0 - # We want to skip the binary wheels for the 'charset-normalizer' packages. # They are build with mypyc, but causes issues with our wheel builder. # In order to do so, we need to constrain the version. diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e87e8b16bcb..e27b681f998 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -176,10 +176,6 @@ pysnmp==1000000000.0.0 # breaks getmac due to them both sharing the same python package name inside 'getmac'. get-mac==1000000000.0.0 -# Circular dependency on homeassistant itself -# https://gitlab.com/keatontaylor/alexapy/-/blob/v1.27.0/pyproject.toml#L29 -alexapy<1.27.0 - # We want to skip the binary wheels for the 'charset-normalizer' packages. # They are build with mypyc, but causes issues with our wheel builder. # In order to do so, we need to constrain the version. From 822af4d40df121430d465afaa041bf315a82cfdc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 30 Sep 2023 05:07:11 +0200 Subject: [PATCH 42/95] Return None when value is not known in OpenHardwareMonitor (#101127) * Return None when value is not known * Add to coverage --- .coveragerc | 1 + homeassistant/components/openhardwaremonitor/sensor.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 2f899999f41..533fd8de18d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -898,6 +898,7 @@ omit = homeassistant/components/opengarage/cover.py homeassistant/components/opengarage/entity.py homeassistant/components/opengarage/sensor.py + homeassistant/components/openhardwaremonitor/sensor.py homeassistant/components/openhome/__init__.py homeassistant/components/openhome/const.py homeassistant/components/openhome/media_player.py diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 70dbbd38fc8..4206bc72c1d 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -79,6 +79,8 @@ class OpenHardwareMonitorDevice(SensorEntity): @property def native_value(self): """Return the state of the device.""" + if self.value == "-": + return None return self.value @property From af041d290024f4e6d31eef066286b59b8bbbe1ad Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 29 Sep 2023 20:57:53 +0200 Subject: [PATCH 43/95] Bump aiowaqi to 1.1.1 (#101129) --- homeassistant/components/waqi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 76e25225b7d..7b6bd3b8592 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", "loggers": ["aiowaqi"], - "requirements": ["aiowaqi==1.1.0"] + "requirements": ["aiowaqi==1.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1c3b3b58098..57a632284fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -372,7 +372,7 @@ aiovlc==0.1.0 aiovodafone==0.3.1 # homeassistant.components.waqi -aiowaqi==1.1.0 +aiowaqi==1.1.1 # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 986828f170c..72431ace108 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ aiovlc==0.1.0 aiovodafone==0.3.1 # homeassistant.components.waqi -aiowaqi==1.1.0 +aiowaqi==1.1.1 # homeassistant.components.watttime aiowatttime==0.1.1 From 01182e8a5c0d75040cbd6b87e386f1938620cca4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 29 Sep 2023 23:05:33 -0400 Subject: [PATCH 44/95] Fix zwave_js firmware update logic (#101143) * Fix zwave_js firmware update logic * add comment * tweak implementation for ssame outcome --- homeassistant/components/zwave_js/update.py | 12 ++++++++---- tests/components/zwave_js/test_update.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 3dedd8bf370..6efae29e46e 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -211,11 +211,15 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): return try: - available_firmware_updates = ( - await self.driver.controller.async_get_available_firmware_updates( - self.node, API_KEY_FIRMWARE_UPDATE_SERVICE + # Retrieve all firmware updates including non-stable ones but filter + # non-stable channels out + available_firmware_updates = [ + update + for update in await self.driver.controller.async_get_available_firmware_updates( + self.node, API_KEY_FIRMWARE_UPDATE_SERVICE, True ) - ) + if update.channel == "stable" + ] except FailedZWaveCommand as err: LOGGER.debug( "Failed to get firmware updates for node %s: %s", diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 4c3aa9f5499..46dca7a35ec 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -87,6 +87,24 @@ FIRMWARE_UPDATES = { "rfRegion": 1, }, }, + # This firmware update should never show because it's in the beta channel + { + "version": "999.999.999", + "changelog": "blah 3", + "channel": "beta", + "files": [ + {"target": 0, "url": "https://example3.com", "integrity": "sha3"} + ], + "downgrade": True, + "normalizedVersion": "999.999.999", + "device": { + "manufacturerId": 1, + "productType": 2, + "productId": 3, + "firmwareVersion": "0.4.4", + "rfRegion": 1, + }, + }, ] } From 04829f0a1b57ff8c8b2dacd1b879737accd820b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 30 Sep 2023 10:54:01 +0200 Subject: [PATCH 45/95] Bumped version to 2023.10.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bfea6544b94..e6a2c08d045 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 11d2d7e54d6..ca5716c4fca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b2" +version = "2023.10.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 51069075712a07333925f29ba3bfbb966158a41c Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:20:09 +0200 Subject: [PATCH 46/95] Terminology: Rename Multi-PAN to Multiprotocol to be consistent (#99262) --- .../silabs_multiprotocol_addon.py | 2 +- .../components/homeassistant_sky_connect/__init__.py | 2 +- .../homeassistant_sky_connect/config_flow.py | 2 +- .../components/homeassistant_yellow/__init__.py | 2 +- .../components/homeassistant_yellow/config_flow.py | 2 +- .../test_silabs_multiprotocol_addon.py | 12 ++++++------ .../homeassistant_sky_connect/test_init.py | 2 +- tests/components/homeassistant_yellow/test_init.py | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index c04575d8005..40cf1e18b0e 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -885,7 +885,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def check_multi_pan_addon(hass: HomeAssistant) -> None: - """Check the multi-PAN addon state, and start it if installed but not started. + """Check the multiprotocol addon state, and start it if installed but not started. Does nothing if Hass.io is not loaded. Raises on error or if the add-on is installed but not started. diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 5f17069f5d5..218e0c3e88d 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -45,7 +45,7 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: return hw_discovery_data = { - "name": "SkyConnect Multi-PAN", + "name": "SkyConnect Multiprotocol", "port": { "path": get_zigbee_socket(), }, diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 5ac44f3f290..fce731777b1 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -76,7 +76,7 @@ class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowH def _zha_name(self) -> str: """Return the ZHA name.""" - return "SkyConnect Multi-PAN" + return "SkyConnect Multiprotocol" def _hardware_name(self) -> str: """Return the name of the hardware.""" diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index 30015d1bae4..b61e01061c3 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hw_discovery_data = ZHA_HW_DISCOVERY_DATA else: hw_discovery_data = { - "name": "Yellow Multi-PAN", + "name": "Yellow Multiprotocol", "port": { "path": get_zigbee_socket(), }, diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index 8be7b8a4ff7..667b8f3d97a 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -153,7 +153,7 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl def _zha_name(self) -> str: """Return the ZHA name.""" - return "Yellow Multi-PAN" + return "Yellow Multiprotocol" def _hardware_name(self) -> str: """Return the name of the hardware.""" diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 17cd288050c..fbc77cdee9e 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -85,7 +85,7 @@ class FakeOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): def _zha_name(self) -> str: """Return the ZHA name.""" - return "Test Multi-PAN" + return "Test Multiprotocol" def _hardware_name(self) -> str: """Return the name of the hardware.""" @@ -353,7 +353,7 @@ async def test_option_flow_install_multi_pan_addon_zha( }, "radio_type": "ezsp", } - assert zha_config_entry.title == "Test Multi-PAN" + assert zha_config_entry.title == "Test Multiprotocol" result = await hass.config_entries.options.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE @@ -663,7 +663,7 @@ async def test_option_flow_addon_installed_same_device_uninstall( }, domain=ZHA_DOMAIN, options={}, - title="Test Multi-PAN", + title="Test Multiprotocol", ) zha_config_entry.add_to_hass(hass) @@ -928,7 +928,7 @@ async def test_option_flow_flasher_install_failure( }, domain=ZHA_DOMAIN, options={}, - title="Test Multi-PAN", + title="Test Multiprotocol", ) zha_config_entry.add_to_hass(hass) @@ -1071,7 +1071,7 @@ async def test_option_flow_uninstall_migration_initiate_failure( }, domain=ZHA_DOMAIN, options={}, - title="Test Multi-PAN", + title="Test Multiprotocol", ) zha_config_entry.add_to_hass(hass) @@ -1132,7 +1132,7 @@ async def test_option_flow_uninstall_migration_finish_failure( }, domain=ZHA_DOMAIN, options={}, - title="Test Multi-PAN", + title="Test Multiprotocol", ) zha_config_entry.add_to_hass(hass) diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 3afc8c24774..e00603dc8f7 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -207,7 +207,7 @@ async def test_setup_zha_multipan( "radio_type": "ezsp", } assert config_entry.options == {} - assert config_entry.title == "SkyConnect Multi-PAN" + assert config_entry.title == "SkyConnect Multiprotocol" async def test_setup_zha_multipan_other_device( diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index a785e46c8b2..addc519c865 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -152,7 +152,7 @@ async def test_setup_zha_multipan( "radio_type": "ezsp", } assert config_entry.options == {} - assert config_entry.title == "Yellow Multi-PAN" + assert config_entry.title == "Yellow Multiprotocol" async def test_setup_zha_multipan_other_device( From c4d85ac41f6e278af41392a54c2812ed6fcb958e Mon Sep 17 00:00:00 2001 From: hlyi Date: Sun, 1 Oct 2023 06:21:26 -0500 Subject: [PATCH 47/95] Report unavailability for yolink sensor and binary_sensor (#100743) --- homeassistant/components/yolink/binary_sensor.py | 5 +++++ homeassistant/components/yolink/const.py | 1 + homeassistant/components/yolink/coordinator.py | 12 ++++++++++-- homeassistant/components/yolink/sensor.py | 5 +++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 38ea7d46537..e65896cdd42 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -136,3 +136,8 @@ class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): state.get(self.entity_description.state_key) ) self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return true is device is available.""" + return super().available and self.coordinator.dev_online diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 935889a0368..9fc4dac8ada 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -8,3 +8,4 @@ ATTR_DEVICE_NAME = "name" ATTR_DEVICE_STATE = "state" ATTR_DEVICE_ID = "deviceId" YOLINK_EVENT = f"{DOMAIN}_event" +YOLINK_OFFLINE_TIME = 32400 diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index 9055b2d044e..f2c942caab9 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from datetime import timedelta +from datetime import UTC, datetime, timedelta import logging from yolink.device import YoLinkDevice @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_DEVICE_STATE, DOMAIN +from .const import ATTR_DEVICE_STATE, DOMAIN, YOLINK_OFFLINE_TIME _LOGGER = logging.getLogger(__name__) @@ -37,6 +37,7 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): ) self.device = device self.paired_device = paired_device + self.dev_online = True async def _async_update_data(self) -> dict: """Fetch device state.""" @@ -44,6 +45,13 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): async with asyncio.timeout(10): device_state_resp = await self.device.fetch_state() device_state = device_state_resp.data.get(ATTR_DEVICE_STATE) + device_reporttime = device_state_resp.data.get("reportAt") + if device_reporttime is not None: + rpt_time_delta = ( + datetime.now(tz=UTC).replace(tzinfo=None) + - datetime.strptime(device_reporttime, "%Y-%m-%dT%H:%M:%S.%fZ") + ).total_seconds() + self.dev_online = rpt_time_delta < YOLINK_OFFLINE_TIME if self.paired_device is not None and device_state is not None: paried_device_state_resp = await self.paired_device.fetch_state() paried_device_state = paried_device_state_resp.data.get( diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 451b486acd2..2fc4a2b0725 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -261,3 +261,8 @@ class YoLinkSensorEntity(YoLinkEntity, SensorEntity): return self._attr_native_value = attr_val self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return true is device is available.""" + return super().available and self.coordinator.dev_online From 3941d2c8975ff74fa1a1071cd0e18cd57843acfa Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 1 Oct 2023 02:28:14 -0400 Subject: [PATCH 48/95] Bump zwave-js-server-python to 0.52.1 (#101162) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 3e8a5e4f757..505196c43eb 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.52.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.52.1"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index 57a632284fc..10bcdc65afa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2817,7 +2817,7 @@ zigpy==0.57.2 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.52.0 +zwave-js-server-python==0.52.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72431ace108..a18e298a753 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2102,7 +2102,7 @@ zigpy-znp==0.11.5 zigpy==0.57.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.52.0 +zwave-js-server-python==0.52.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From 531479bf5b3dd23d00e19de8c806f025d7df8b2d Mon Sep 17 00:00:00 2001 From: Tereza Tomcova Date: Sat, 30 Sep 2023 22:53:58 +0300 Subject: [PATCH 49/95] Bump PySwitchbot to 0.40.1 (#101164) --- 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 e685d1de806..e835a2f4aca 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.40.0"] + "requirements": ["PySwitchbot==0.40.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 10bcdc65afa..4abe6e55e62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -97,7 +97,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.40.0 +PySwitchbot==0.40.1 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a18e298a753..baf890939d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.40.0 +PySwitchbot==0.40.1 # homeassistant.components.syncthru PySyncThru==0.7.10 From d7fa98454b4262ee3b3f9259b49c027afcdcdebf Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 Oct 2023 08:12:44 -0700 Subject: [PATCH 50/95] Fix rainbird entity unique ids (#101168) * Fix unique ids for rainbird entities * Update entity unique id use based on config entry entity id * Update tests/components/rainbird/test_binary_sensor.py Co-authored-by: Martin Hjelmare * Rename all entity_registry variables * Shorten long comment under line length limits --------- Co-authored-by: Martin Hjelmare --- .../components/rainbird/binary_sensor.py | 7 ++- homeassistant/components/rainbird/calendar.py | 17 ++++--- .../components/rainbird/coordinator.py | 27 +++++++---- homeassistant/components/rainbird/number.py | 7 ++- homeassistant/components/rainbird/sensor.py | 9 +++- homeassistant/components/rainbird/switch.py | 19 ++++---- tests/components/rainbird/conftest.py | 6 +-- .../components/rainbird/test_binary_sensor.py | 36 ++++++++++++++ tests/components/rainbird/test_calendar.py | 30 ++++++++++++ tests/components/rainbird/test_config_flow.py | 4 +- tests/components/rainbird/test_number.py | 34 +++++++++++++- tests/components/rainbird/test_sensor.py | 47 ++++++++++++++++++- tests/components/rainbird/test_switch.py | 32 +++++++++++++ 13 files changed, 237 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index b5886011ea3..3333d8bc4cb 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -48,8 +48,11 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorE """Initialize the Rain Bird sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" - self._attr_device_info = coordinator.device_info + if coordinator.unique_id: + self._attr_unique_id = f"{coordinator.unique_id}-{description.key}" + self._attr_device_info = coordinator.device_info + else: + self._attr_name = f"{coordinator.device_name} Rainsensor" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/rainbird/calendar.py b/homeassistant/components/rainbird/calendar.py index 4d8cc38c8bf..356f7d7cc4e 100644 --- a/homeassistant/components/rainbird/calendar.py +++ b/homeassistant/components/rainbird/calendar.py @@ -34,8 +34,9 @@ async def async_setup_entry( [ RainBirdCalendarEntity( data.schedule_coordinator, - data.coordinator.serial_number, + data.coordinator.unique_id, data.coordinator.device_info, + data.coordinator.device_name, ) ] ) @@ -47,20 +48,24 @@ class RainBirdCalendarEntity( """A calendar event entity.""" _attr_has_entity_name = True - _attr_name = None + _attr_name: str | None = None _attr_icon = "mdi:sprinkler" def __init__( self, coordinator: RainbirdScheduleUpdateCoordinator, - serial_number: str, - device_info: DeviceInfo, + unique_id: str | None, + device_info: DeviceInfo | None, + device_name: str, ) -> None: """Create the Calendar event device.""" super().__init__(coordinator) self._event: CalendarEvent | None = None - self._attr_unique_id = serial_number - self._attr_device_info = device_info + if unique_id: + self._attr_unique_id = unique_id + self._attr_device_info = device_info + else: + self._attr_name = device_name @property def event(self) -> CalendarEvent | None: diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 5c40ef808b2..763e50fe5d9 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER, TIMEOUT_SECONDS +from .const import DOMAIN, MANUFACTURER, TIMEOUT_SECONDS UPDATE_INTERVAL = datetime.timedelta(minutes=1) # The calendar data requires RPCs for each program/zone, and the data rarely @@ -51,7 +51,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): hass: HomeAssistant, name: str, controller: AsyncRainbirdController, - serial_number: str, + unique_id: str | None, model_info: ModelAndVersion, ) -> None: """Initialize RainbirdUpdateCoordinator.""" @@ -62,7 +62,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): update_interval=UPDATE_INTERVAL, ) self._controller = controller - self._serial_number = serial_number + self._unique_id = unique_id self._zones: set[int] | None = None self._model_info = model_info @@ -72,16 +72,23 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): return self._controller @property - def serial_number(self) -> str: - """Return the device serial number.""" - return self._serial_number + def unique_id(self) -> str | None: + """Return the config entry unique id.""" + return self._unique_id @property - def device_info(self) -> DeviceInfo: + def device_name(self) -> str: + """Device name for the rainbird controller.""" + return f"{MANUFACTURER} Controller" + + @property + def device_info(self) -> DeviceInfo | None: """Return information about the device.""" + if not self._unique_id: + return None return DeviceInfo( - name=f"{MANUFACTURER} Controller", - identifiers={(DOMAIN, self._serial_number)}, + name=self.device_name, + identifiers={(DOMAIN, self._unique_id)}, manufacturer=MANUFACTURER, model=self._model_info.model_name, sw_version=f"{self._model_info.major}.{self._model_info.minor}", @@ -164,7 +171,7 @@ class RainbirdData: self.hass, name=self.entry.title, controller=self.controller, - serial_number=self.entry.data[CONF_SERIAL_NUMBER], + unique_id=self.entry.unique_id, model_info=self.model_info, ) diff --git a/homeassistant/components/rainbird/number.py b/homeassistant/components/rainbird/number.py index d0945609a1b..1e72fabafcd 100644 --- a/homeassistant/components/rainbird/number.py +++ b/homeassistant/components/rainbird/number.py @@ -51,8 +51,11 @@ class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity ) -> None: """Initialize the Rain Bird sensor.""" super().__init__(coordinator) - self._attr_unique_id = f"{coordinator.serial_number}-rain-delay" - self._attr_device_info = coordinator.device_info + if coordinator.unique_id: + self._attr_unique_id = f"{coordinator.unique_id}-rain-delay" + self._attr_device_info = coordinator.device_info + else: + self._attr_name = f"{coordinator.device_name} Rain delay" @property def native_value(self) -> float | None: diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 32eb053f478..d44e7156cb5 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -52,8 +52,13 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity) """Initialize the Rain Bird sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" - self._attr_device_info = coordinator.device_info + if coordinator.unique_id: + self._attr_unique_id = f"{coordinator.unique_id}-{description.key}" + self._attr_device_info = coordinator.device_info + else: + self._attr_name = ( + f"{coordinator.device_name} {description.key.capitalize()}" + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index cafc541d860..62b3b0e9a8c 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -65,20 +65,23 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) """Initialize a Rain Bird Switch Device.""" super().__init__(coordinator) self._zone = zone + if coordinator.unique_id: + self._attr_unique_id = f"{coordinator.unique_id}-{zone}" + device_name = f"{MANUFACTURER} Sprinkler {zone}" if imported_name: self._attr_name = imported_name self._attr_has_entity_name = False else: - self._attr_name = None + self._attr_name = None if coordinator.unique_id else device_name self._attr_has_entity_name = True self._duration_minutes = duration_minutes - self._attr_unique_id = f"{coordinator.serial_number}-{zone}" - self._attr_device_info = DeviceInfo( - name=f"{MANUFACTURER} Sprinkler {zone}", - identifiers={(DOMAIN, self._attr_unique_id)}, - manufacturer=MANUFACTURER, - via_device=(DOMAIN, coordinator.serial_number), - ) + if coordinator.unique_id and self._attr_unique_id: + self._attr_device_info = DeviceInfo( + name=device_name, + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer=MANUFACTURER, + via_device=(DOMAIN, coordinator.unique_id), + ) @property def extra_state_attributes(self): diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index dbc3456117c..f25bdfb1d86 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -86,7 +86,7 @@ def yaml_config() -> dict[str, Any]: @pytest.fixture -async def unique_id() -> str: +async def config_entry_unique_id() -> str: """Fixture for serial number used in the config entry.""" return SERIAL_NUMBER @@ -100,13 +100,13 @@ async def config_entry_data() -> dict[str, Any]: @pytest.fixture async def config_entry( config_entry_data: dict[str, Any] | None, - unique_id: str, + config_entry_unique_id: str | None, ) -> MockConfigEntry | None: """Fixture for MockConfigEntry.""" if config_entry_data is None: return None return MockConfigEntry( - unique_id=unique_id, + unique_id=config_entry_unique_id, domain=DOMAIN, data=config_entry_data, options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index cfa2c4d2684..e372a10ae23 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -5,6 +5,7 @@ import pytest from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, ComponentSetup @@ -25,6 +26,7 @@ async def test_rainsensor( hass: HomeAssistant, setup_integration: ComponentSetup, responses: list[AiohttpClientMockResponse], + entity_registry: er.EntityRegistry, expected_state: bool, ) -> None: """Test rainsensor binary sensor.""" @@ -38,3 +40,37 @@ async def test_rainsensor( "friendly_name": "Rain Bird Controller Rainsensor", "icon": "mdi:water", } + + entity_entry = entity_registry.async_get( + "binary_sensor.rain_bird_controller_rainsensor" + ) + assert entity_entry + assert entity_entry.unique_id == "1263613994342-rainsensor" + + +@pytest.mark.parametrize( + ("config_entry_unique_id"), + [ + (None), + ], +) +async def test_no_unique_id( + hass: HomeAssistant, + setup_integration: ComponentSetup, + responses: list[AiohttpClientMockResponse], + entity_registry: er.EntityRegistry, +) -> None: + """Test rainsensor binary sensor with no unique id.""" + + assert await setup_integration() + + rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") + assert rainsensor is not None + assert ( + rainsensor.attributes.get("friendly_name") == "Rain Bird Controller Rainsensor" + ) + + entity_entry = entity_registry.async_get( + "binary_sensor.rain_bird_controller_rainsensor" + ) + assert not entity_entry diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 2028fccc24f..2e486226a7b 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -14,6 +14,7 @@ import pytest from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import ComponentSetup, mock_response, mock_response_error @@ -176,6 +177,7 @@ async def test_event_state( freezer: FrozenDateTimeFactory, freeze_time: datetime.datetime, expected_state: str, + entity_registry: er.EntityRegistry, ) -> None: """Test calendar upcoming event state.""" freezer.move_to(freeze_time) @@ -196,6 +198,10 @@ async def test_event_state( } assert state.state == expected_state + entity = entity_registry.async_get(TEST_ENTITY) + assert entity + assert entity.unique_id == 1263613994342 + @pytest.mark.parametrize( ("model_and_version_response", "has_entity"), @@ -270,3 +276,27 @@ async def test_program_schedule_disabled( "friendly_name": "Rain Bird Controller", "icon": "mdi:sprinkler", } + + +@pytest.mark.parametrize( + ("config_entry_unique_id"), + [ + (None), + ], +) +async def test_no_unique_id( + hass: HomeAssistant, + setup_integration: ComponentSetup, + get_events: GetEventsFn, + entity_registry: er.EntityRegistry, +) -> None: + """Test calendar entity with no unique id.""" + + assert await setup_integration() + + state = hass.states.get(TEST_ENTITY) + assert state is not None + assert state.attributes.get("friendly_name") == "Rain Bird Controller" + + entity_entry = entity_registry.async_get(TEST_ENTITY) + assert not entity_entry diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index e7337ad6508..cfc4ff3b5cb 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -106,7 +106,7 @@ async def test_controller_flow( @pytest.mark.parametrize( ( - "unique_id", + "config_entry_unique_id", "config_entry_data", "config_flow_responses", "expected_config_entry", @@ -154,7 +154,7 @@ async def test_multiple_config_entries( @pytest.mark.parametrize( ( - "unique_id", + "config_entry_unique_id", "config_entry_data", "config_flow_responses", ), diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 6ce7d10c9f2..5d208f08a25 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from .conftest import ( ACK_ECHO, @@ -39,8 +39,9 @@ async def test_number_values( hass: HomeAssistant, setup_integration: ComponentSetup, expected_state: str, + entity_registry: er.EntityRegistry, ) -> None: - """Test sensor platform.""" + """Test number platform.""" assert await setup_integration() @@ -57,6 +58,10 @@ async def test_number_values( "unit_of_measurement": "d", } + entity_entry = entity_registry.async_get("number.rain_bird_controller_rain_delay") + assert entity_entry + assert entity_entry.unique_id == "1263613994342-rain-delay" + async def test_set_value( hass: HomeAssistant, @@ -127,3 +132,28 @@ async def test_set_value_error( ) assert len(aioclient_mock.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("config_entry_unique_id"), + [ + (None), + ], +) +async def test_no_unique_id( + hass: HomeAssistant, + setup_integration: ComponentSetup, + entity_registry: er.EntityRegistry, +) -> None: + """Test number platform with no unique id.""" + + assert await setup_integration() + + raindelay = hass.states.get("number.rain_bird_controller_rain_delay") + assert raindelay is not None + assert ( + raindelay.attributes.get("friendly_name") == "Rain Bird Controller Rain delay" + ) + + entity_entry = entity_registry.async_get("number.rain_bird_controller_rain_delay") + assert not entity_entry diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index 049a5f15c45..d8fb053c0ff 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -5,8 +5,9 @@ import pytest from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from .conftest import RAIN_DELAY, RAIN_DELAY_OFF, ComponentSetup +from .conftest import CONFIG_ENTRY_DATA, RAIN_DELAY, RAIN_DELAY_OFF, ComponentSetup @pytest.fixture @@ -22,6 +23,7 @@ def platforms() -> list[str]: async def test_sensors( hass: HomeAssistant, setup_integration: ComponentSetup, + entity_registry: er.EntityRegistry, expected_state: str, ) -> None: """Test sensor platform.""" @@ -35,3 +37,46 @@ async def test_sensors( "friendly_name": "Rain Bird Controller Raindelay", "icon": "mdi:water-off", } + + entity_entry = entity_registry.async_get("sensor.rain_bird_controller_raindelay") + assert entity_entry + assert entity_entry.unique_id == "1263613994342-raindelay" + + +@pytest.mark.parametrize( + ("config_entry_unique_id", "config_entry_data"), + [ + # Config entry setup without a unique id since it had no serial number + ( + None, + { + **CONFIG_ENTRY_DATA, + "serial_number": 0, + }, + ), + # Legacy case for old config entries with serial number 0 preserves old behavior + ( + "0", + { + **CONFIG_ENTRY_DATA, + "serial_number": 0, + }, + ), + ], +) +async def test_sensor_no_unique_id( + hass: HomeAssistant, + setup_integration: ComponentSetup, + entity_registry: er.EntityRegistry, + config_entry_unique_id: str | None, +) -> None: + """Test sensor platform with no unique id.""" + + assert await setup_integration() + + raindelay = hass.states.get("sensor.rain_bird_controller_raindelay") + assert raindelay is not None + assert raindelay.attributes.get("friendly_name") == "Rain Bird Controller Raindelay" + + entity_entry = entity_registry.async_get("sensor.rain_bird_controller_raindelay") + assert (entity_entry is None) == (config_entry_unique_id is None) diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index 9ce5e799c92..46a875e8928 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -8,6 +8,7 @@ from homeassistant.components.rainbird import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from .conftest import ( ACK_ECHO, @@ -57,6 +58,7 @@ async def test_no_zones( async def test_zones( hass: HomeAssistant, setup_integration: ComponentSetup, + entity_registry: er.EntityRegistry, ) -> None: """Test switch platform with fake data that creates 7 zones with one enabled.""" @@ -100,6 +102,10 @@ async def test_zones( assert not hass.states.get("switch.rain_bird_sprinkler_8") + # Verify unique id for one of the switches + entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3") + assert entity_entry.unique_id == "1263613994342-3" + async def test_switch_on( hass: HomeAssistant, @@ -275,3 +281,29 @@ async def test_switch_error( with pytest.raises(HomeAssistantError, match=expected_msg): await switch_common.async_turn_off(hass, "switch.rain_bird_sprinkler_3") await hass.async_block_till_done() + + +@pytest.mark.parametrize( + ("config_entry_unique_id"), + [ + None, + ], +) +async def test_no_unique_id( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + responses: list[AiohttpClientMockResponse], + entity_registry: er.EntityRegistry, +) -> None: + """Test an irrigation switch with no unique id.""" + + assert await setup_integration() + + zone = hass.states.get("switch.rain_bird_sprinkler_3") + assert zone is not None + assert zone.attributes.get("friendly_name") == "Rain Bird Sprinkler 3" + assert zone.state == "off" + + entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3") + assert entity_entry is None From b27097808dab66e2f08b970910efe49248c59487 Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sun, 1 Oct 2023 12:26:28 +0200 Subject: [PATCH 51/95] Update denonavr to `0.11.4` (#101169) --- .../components/denonavr/manifest.json | 2 +- .../components/denonavr/media_player.py | 35 ++++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index b3c36ed39d2..0ba8caed6c5 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/denonavr", "iot_class": "local_push", "loggers": ["denonavr"], - "requirements": ["denonavr==0.11.3"], + "requirements": ["denonavr==0.11.4"], "ssdp": [ { "manufacturer": "Denon", diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 51ede0d65b4..8b6907a60f7 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -8,7 +8,15 @@ import logging from typing import Any, Concatenate, ParamSpec, TypeVar from denonavr import DenonAVR -from denonavr.const import POWER_ON, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING +from denonavr.const import ( + ALL_TELNET_EVENTS, + ALL_ZONES, + POWER_ON, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) from denonavr.exceptions import ( AvrCommandError, AvrForbiddenError, @@ -73,6 +81,23 @@ SERVICE_GET_COMMAND = "get_command" SERVICE_SET_DYNAMIC_EQ = "set_dynamic_eq" SERVICE_UPDATE_AUDYSSEY = "update_audyssey" +# HA Telnet events +TELNET_EVENTS = { + "HD", + "MS", + "MU", + "MV", + "NS", + "NSE", + "PS", + "SI", + "SS", + "TF", + "ZM", + "Z2", + "Z3", +} + _DenonDeviceT = TypeVar("_DenonDeviceT", bound="DenonDevice") _R = TypeVar("_R") _P = ParamSpec("_P") @@ -254,7 +279,9 @@ class DenonDevice(MediaPlayerEntity): async def _telnet_callback(self, zone, event, parameter) -> None: """Process a telnet command callback.""" # There are multiple checks implemented which reduce unnecessary updates of the ha state machine - if zone != self._receiver.zone: + if zone not in (self._receiver.zone, ALL_ZONES): + return + if event not in TELNET_EVENTS: return # Some updates trigger multiple events like one for artist and one for title for one change # We skip every event except the last one @@ -268,11 +295,11 @@ class DenonDevice(MediaPlayerEntity): async def async_added_to_hass(self) -> None: """Register for telnet events.""" - self._receiver.register_callback("ALL", self._telnet_callback) + self._receiver.register_callback(ALL_TELNET_EVENTS, self._telnet_callback) async def async_will_remove_from_hass(self) -> None: """Clean up the entity.""" - self._receiver.unregister_callback("ALL", self._telnet_callback) + self._receiver.unregister_callback(ALL_TELNET_EVENTS, self._telnet_callback) @async_log_errors async def async_update(self) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 4abe6e55e62..4db254c5a73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -668,7 +668,7 @@ deluge-client==1.7.1 demetriek==0.4.0 # homeassistant.components.denonavr -denonavr==0.11.3 +denonavr==0.11.4 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index baf890939d1..58ead8233e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -545,7 +545,7 @@ deluge-client==1.7.1 demetriek==0.4.0 # homeassistant.components.denonavr -denonavr==0.11.3 +denonavr==0.11.4 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 From b20f9c40be0de434dd175853b27eba4a33a0becd Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 Oct 2023 06:25:04 -0700 Subject: [PATCH 52/95] Clear calendar alarms after scheduling and add debug loggging (#101176) --- homeassistant/components/calendar/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 96872e039e1..1622f568a2d 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -531,6 +531,7 @@ class CalendarEntity(Entity): for unsub in self._alarm_unsubs: unsub() + self._alarm_unsubs.clear() now = dt_util.now() event = self.event @@ -540,6 +541,7 @@ class CalendarEntity(Entity): @callback def update(_: datetime.datetime) -> None: """Run when the active or upcoming event starts or ends.""" + _LOGGER.debug("Running %s update", self.entity_id) self._async_write_ha_state() if now < event.start_datetime_local: @@ -553,6 +555,13 @@ class CalendarEntity(Entity): self._alarm_unsubs.append( async_track_point_in_time(self.hass, update, event.end_datetime_local) ) + _LOGGER.debug( + "Scheduled %d updates for %s (%s, %s)", + len(self._alarm_unsubs), + self.entity_id, + event.start_datetime_local, + event.end_datetime_local, + ) async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass. @@ -561,6 +570,7 @@ class CalendarEntity(Entity): """ for unsub in self._alarm_unsubs: unsub() + self._alarm_unsubs.clear() async def async_get_events( self, From e0d7c1440b314c6efb2ce65b2863a7f06616f9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 1 Oct 2023 10:12:06 +0200 Subject: [PATCH 53/95] Update Mill library to 0.11.6 (#101180) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 561a24c29df..cb0ba4522bf 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.11.5", "mill-local==0.3.0"] + "requirements": ["millheater==0.11.6", "mill-local==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4db254c5a73..04ac8758390 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1222,7 +1222,7 @@ micloud==0.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.11.5 +millheater==0.11.6 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58ead8233e2..9038e992c42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -948,7 +948,7 @@ micloud==0.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.11.5 +millheater==0.11.6 # homeassistant.components.minio minio==7.1.12 From b24f09b47e337675b8480114a15407d7a7506738 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 1 Oct 2023 18:29:53 +0200 Subject: [PATCH 54/95] Add config entry name to Withings webhook name (#101205) --- homeassistant/components/withings/__init__.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 44d32b0603c..aaef7bdb142 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -41,7 +41,14 @@ from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from .api import ConfigEntryWithingsApi -from .const import CONF_CLOUDHOOK_URL, CONF_PROFILES, CONF_USE_WEBHOOK, DOMAIN, LOGGER +from .const import ( + CONF_CLOUDHOOK_URL, + CONF_PROFILES, + CONF_USE_WEBHOOK, + DEFAULT_TITLE, + DOMAIN, + LOGGER, +) from .coordinator import WithingsDataUpdateCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -151,10 +158,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return + webhook_name = "Withings" + if entry.title != DEFAULT_TITLE: + webhook_name += " ".join([webhook_name, entry.title]) + webhook_register( hass, DOMAIN, - "Withings", + webhook_name, entry.data[CONF_WEBHOOK_ID], get_webhook_handler(coordinator), ) From cf6f0cf2665ff433c6cb31fa4b889ff94d71e51c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 1 Oct 2023 18:28:53 +0200 Subject: [PATCH 55/95] Correct JSONDecodeError in co2signal (#101206) --- homeassistant/components/co2signal/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/co2signal/coordinator.py b/homeassistant/components/co2signal/coordinator.py index c210d989c04..24d7bbd18af 100644 --- a/homeassistant/components/co2signal/coordinator.py +++ b/homeassistant/components/co2signal/coordinator.py @@ -3,11 +3,11 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta -from json import JSONDecodeError import logging from typing import Any, cast import CO2Signal +from requests.exceptions import JSONDecodeError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE From 8c84237e6b9185f7813d91a3f429d50e74b55945 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 1 Oct 2023 18:32:38 +0200 Subject: [PATCH 56/95] Bumped version to 2023.10.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e6a2c08d045..d656032f32b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index ca5716c4fca..62e59dbe7cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b3" +version = "2023.10.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From cfa923252bd20a315a5072d7011399394e70d0fd Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:15:54 +0200 Subject: [PATCH 57/95] Fix loop in progress config flow (#97229) * Fix data entry flow with multiple steps * Update a test * Update description and add a show progress change test --------- Co-authored-by: Martin Hjelmare --- homeassistant/data_entry_flow.py | 15 ++-- tests/test_data_entry_flow.py | 116 ++++++++++++++++++++++++++++--- 2 files changed, 119 insertions(+), 12 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 63cbfda5b9b..e22d4229511 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -320,10 +320,17 @@ class FlowManager(abc.ABC): ) # If the result has changed from last result, fire event to update - # the frontend. - if ( - cur_step["step_id"] != result.get("step_id") - or result["type"] == FlowResultType.SHOW_PROGRESS + # the frontend. The result is considered to have changed if: + # - The step has changed + # - The step is same but result type is SHOW_PROGRESS and progress_action + # or description_placeholders has changed + if cur_step["step_id"] != result.get("step_id") or ( + result["type"] == FlowResultType.SHOW_PROGRESS + and ( + cur_step["progress_action"] != result.get("progress_action") + or cur_step["description_placeholders"] + != result.get("description_placeholders") + ) ): # Tell frontend to reload the flow state. self.hass.bus.async_fire( diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 168f97ba779..e6a28fc2e4f 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -344,14 +344,20 @@ async def test_show_progress(hass: HomeAssistant, manager) -> None: VERSION = 5 data = None task_one_done = False + task_two_done = False async def async_step_init(self, user_input=None): - if not user_input: - if not self.task_one_done: + if user_input and "task_finished" in user_input: + if user_input["task_finished"] == 1: self.task_one_done = True - progress_action = "task_one" - else: - progress_action = "task_two" + elif user_input["task_finished"] == 2: + self.task_two_done = True + + if not self.task_one_done: + progress_action = "task_one" + elif not self.task_two_done: + progress_action = "task_two" + if not self.task_one_done or not self.task_two_done: return self.async_show_progress( step_id="init", progress_action=progress_action, @@ -376,7 +382,7 @@ async def test_show_progress(hass: HomeAssistant, manager) -> None: # Mimic task one done and moving to task two # Called by integrations: `hass.config_entries.flow.async_configure(…)` - result = await manager.async_configure(result["flow_id"]) + result = await manager.async_configure(result["flow_id"], {"task_finished": 1}) assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_two" @@ -388,13 +394,20 @@ async def test_show_progress(hass: HomeAssistant, manager) -> None: "refresh": True, } + # Frontend refreshes the flow + result = await manager.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "task_two" + # Mimic task two done and continuing step # Called by integrations: `hass.config_entries.flow.async_configure(…)` - result = await manager.async_configure(result["flow_id"], {"title": "Hello"}) + result = await manager.async_configure( + result["flow_id"], {"task_finished": 2, "title": "Hello"} + ) assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE await hass.async_block_till_done() - assert len(events) == 2 + assert len(events) == 2 # 1 for task one and 1 for task two assert events[1].data == { "handler": "test", "flow_id": result["flow_id"], @@ -407,6 +420,93 @@ async def test_show_progress(hass: HomeAssistant, manager) -> None: assert result["title"] == "Hello" +async def test_show_progress_fires_only_when_changed( + hass: HomeAssistant, manager +) -> None: + """Test show progress change logic.""" + manager.hass = hass + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + data = None + + async def async_step_init(self, user_input=None): + if user_input: + progress_action = user_input["progress_action"] + description_placeholders = user_input["description_placeholders"] + return self.async_show_progress( + step_id="init", + progress_action=progress_action, + description_placeholders=description_placeholders, + ) + return self.async_show_progress(step_id="init", progress_action="task_one") + + async def async_step_finish(self, user_input=None): + return self.async_create_entry(title=self.data["title"], data=self.data) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED + ) + + async def test_change( + flow_id, + events, + progress_action, + description_placeholders_progress, + number_of_events, + is_change, + ) -> None: + # Called by integrations: `hass.config_entries.flow.async_configure(…)` + result = await manager.async_configure( + flow_id, + { + "progress_action": progress_action, + "description_placeholders": { + "progress": description_placeholders_progress + }, + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == progress_action + assert ( + result["description_placeholders"]["progress"] + == description_placeholders_progress + ) + + await hass.async_block_till_done() + assert len(events) == number_of_events + if is_change: + assert events[number_of_events - 1].data == { + "handler": "test", + "flow_id": result["flow_id"], + "refresh": True, + } + + result = await manager.async_init("test") + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "task_one" + assert len(manager.async_progress()) == 1 + assert len(manager.async_progress_by_handler("test")) == 1 + assert manager.async_get(result["flow_id"])["handler"] == "test" + + # Mimic task one tests + await test_change( + result["flow_id"], events, "task_one", 0, 1, True + ) # change (progress action) + await test_change(result["flow_id"], events, "task_one", 0, 1, False) # no change + await test_change( + result["flow_id"], events, "task_one", 25, 2, True + ) # change (description placeholder) + await test_change( + result["flow_id"], events, "task_two", 50, 3, True + ) # change (progress action and description placeholder) + await test_change(result["flow_id"], events, "task_two", 50, 3, False) # no change + await test_change( + result["flow_id"], events, "task_two", 100, 4, True + ) # change (description placeholder) + + async def test_abort_flow_exception(manager) -> None: """Test that the AbortFlow exception works.""" From 93033e037d5efb71e7166311f1e4cd8ae8e91008 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 2 Oct 2023 14:11:16 -0400 Subject: [PATCH 58/95] Bump python-roborock to 0.34.6 (#101147) --- homeassistant/components/roborock/manifest.json | 2 +- homeassistant/components/roborock/strings.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roborock/test_switch.py | 2 +- tests/components/roborock/test_time.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index dfd5a9ee1c7..6882754f49a 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.34.1"] + "requirements": ["python-roborock==0.34.6"] } diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index c46eb814151..53c536494f9 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -176,7 +176,8 @@ "moderate": "Moderate", "high": "High", "intense": "Intense", - "custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]" + "custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]", + "custom_water_flow": "Custom water flow" } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 04ac8758390..488857fb0d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2174,7 +2174,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.34.1 +python-roborock==0.34.6 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9038e992c42..79f35b36f74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1618,7 +1618,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.34.1 +python-roborock==0.34.6 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py index 40ecdc267ed..fb301390fee 100644 --- a/tests/components/roborock/test_switch.py +++ b/tests/components/roborock/test_switch.py @@ -27,7 +27,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClient._send_command" ) as mock_send_message: await hass.services.async_call( "switch", diff --git a/tests/components/roborock/test_time.py b/tests/components/roborock/test_time.py index 6ba996ca23f..1cf2fe6bed5 100644 --- a/tests/components/roborock/test_time.py +++ b/tests/components/roborock/test_time.py @@ -27,7 +27,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClient._send_command" ) as mock_send_message: await hass.services.async_call( "time", From 98ca71fc966d1afe164c344ac64074e773f8ea84 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 2 Oct 2023 14:20:19 +0100 Subject: [PATCH 59/95] Split get users into chunks of 100 for Twitch sensors (#101211) * Split get users into chunks of 100 * Move to own function --- homeassistant/components/twitch/sensor.py | 26 +++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 11d6611ef99..05fd3fa3e71 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -52,6 +52,11 @@ STATE_OFFLINE = "offline" STATE_STREAMING = "streaming" +def chunk_list(lst: list, chunk_size: int) -> list[list]: + """Split a list into chunks of chunk_size.""" + return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)] + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -94,13 +99,20 @@ async def async_setup_entry( """Initialize entries.""" client = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [ - TwitchSensor(channel, client) - async for channel in client.get_users(logins=entry.options[CONF_CHANNELS]) - ], - True, - ) + channels = entry.options[CONF_CHANNELS] + + entities: list[TwitchSensor] = [] + + # Split channels into chunks of 100 to avoid hitting the rate limit + for chunk in chunk_list(channels, 100): + entities.extend( + [ + TwitchSensor(channel, client) + async for channel in client.get_users(logins=chunk) + ] + ) + + async_add_entities(entities, True) class TwitchSensor(SensorEntity): From bfe16e25365aae972117ab13a1334032c8a26377 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Oct 2023 19:33:38 +0100 Subject: [PATCH 60/95] Bump zeroconf to 0.115.1 (#101213) --- 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 9898c6a3496..53475588cfe 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.115.0"] + "requirements": ["zeroconf==0.115.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d6f923f0047..659caa1078d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.2 -zeroconf==0.115.0 +zeroconf==0.115.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 488857fb0d9..3fea672d794 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2784,7 +2784,7 @@ zamg==0.3.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.115.0 +zeroconf==0.115.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79f35b36f74..634d7dbb534 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2078,7 +2078,7 @@ yt-dlp==2023.9.24 zamg==0.3.0 # homeassistant.components.zeroconf -zeroconf==0.115.0 +zeroconf==0.115.1 # homeassistant.components.zeversolar zeversolar==0.3.1 From 18f3fb42c975d049d98ec79c1a3b08d488d82774 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Oct 2023 19:33:53 +0100 Subject: [PATCH 61/95] Bump aioesphomeapi to 17.0.1 (#101214) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d6fdd971fa6..8169eeb70e3 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==17.0.0", + "aioesphomeapi==17.0.1", "bluetooth-data-tools==1.12.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 3fea672d794..6f83e88ccdc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.0.0 +aioesphomeapi==17.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 634d7dbb534..a255103e3bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.0.0 +aioesphomeapi==17.0.1 # homeassistant.components.flo aioflo==2021.11.0 From ced616fafa6fe729f87f39441bbe629915d7442d Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 2 Oct 2023 14:31:25 +0200 Subject: [PATCH 62/95] Remove invalid doc about multi origin/dest in google_travel_time (#101215) --- homeassistant/components/google_travel_time/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 78b84038c7f..270f8fe31e2 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.", + "description": "You can specify the origin and destination in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.", "data": { "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", From ebf806111754d6584a7aa4b94e5066dd617b7101 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 1 Oct 2023 20:55:00 +0200 Subject: [PATCH 63/95] Fix withings webhook name (#101221) --- homeassistant/components/withings/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index aaef7bdb142..246bcc134d0 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -160,7 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: webhook_name = "Withings" if entry.title != DEFAULT_TITLE: - webhook_name += " ".join([webhook_name, entry.title]) + webhook_name = " ".join([DEFAULT_TITLE, entry.title]) webhook_register( hass, From e76396b1842bf7df9d7e3c7253f5299f65d7f121 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 2 Oct 2023 21:35:15 +1300 Subject: [PATCH 64/95] ESPHome: fix voice assistant default audio settings (#101241) --- homeassistant/components/esphome/voice_assistant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index baf3a9011e9..dc36b7475c4 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -222,7 +222,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): audio_settings: VoiceAssistantAudioSettings | None = None, ) -> None: """Run the Voice Assistant pipeline.""" - if audio_settings is None: + if audio_settings is None or audio_settings.volume_multiplier == 0: audio_settings = VoiceAssistantAudioSettings() tts_audio_output = ( From 63b5ba6b3a1299c03a368b87fb411dd949c1692e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Sep 2023 22:38:21 +0200 Subject: [PATCH 65/95] Remove dead code from broadlink light (#101063) --- homeassistant/components/broadlink/light.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index 796698c6a4c..57797ca592a 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -6,7 +6,6 @@ from broadlink.exceptions import BroadlinkException from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ColorMode, @@ -113,16 +112,6 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): state["colortemp"] = (color_temp - 153) * 100 + 2700 state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE - elif ATTR_COLOR_MODE in kwargs: - color_mode = kwargs[ATTR_COLOR_MODE] - if color_mode == ColorMode.HS: - state["bulb_colormode"] = BROADLINK_COLOR_MODE_RGB - elif color_mode == ColorMode.COLOR_TEMP: - state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE - else: - # Scenes are not yet supported. - state["bulb_colormode"] = BROADLINK_COLOR_MODE_SCENES - await self._async_set_state(state) async def async_turn_off(self, **kwargs: Any) -> None: From ad53ff037e533652a9782d451e4f90f3d246096f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Oct 2023 14:12:06 +0200 Subject: [PATCH 66/95] Fix color temperature setting in broadlink light (#101251) --- homeassistant/components/broadlink/light.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index 57797ca592a..fde6d322bc6 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -6,7 +6,7 @@ from broadlink.exceptions import BroadlinkException from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ColorMode, LightEntity, @@ -45,6 +45,8 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): _attr_has_entity_name = True _attr_name = None + _attr_min_color_temp_kelvin = 2700 + _attr_max_color_temp_kelvin = 6500 def __init__(self, device): """Initialize the light.""" @@ -79,7 +81,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): self._attr_hs_color = [data["hue"], data["saturation"]] if "colortemp" in data: - self._attr_color_temp = round((data["colortemp"] - 2700) / 100 + 153) + self._attr_color_temp_kelvin = data["colortemp"] if "bulb_colormode" in data: if data["bulb_colormode"] == BROADLINK_COLOR_MODE_RGB: @@ -107,9 +109,9 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): state["saturation"] = int(hs_color[1]) state["bulb_colormode"] = BROADLINK_COLOR_MODE_RGB - elif ATTR_COLOR_TEMP in kwargs: - color_temp = kwargs[ATTR_COLOR_TEMP] - state["colortemp"] = (color_temp - 153) * 100 + 2700 + elif ATTR_COLOR_TEMP_KELVIN in kwargs: + color_temp = kwargs[ATTR_COLOR_TEMP_KELVIN] + state["colortemp"] = color_temp state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE await self._async_set_state(state) From 3cfae485778a56614ca6ebe7d650f7a869216df8 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 2 Oct 2023 12:56:39 +0100 Subject: [PATCH 67/95] Add extra validation in private_ble_device config flow (#101254) --- .../private_ble_device/config_flow.py | 33 ++++++++++++++----- .../private_ble_device/test_config_flow.py | 26 +++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/private_ble_device/config_flow.py b/homeassistant/components/private_ble_device/config_flow.py index 5bf130a0396..4fec68e507e 100644 --- a/homeassistant/components/private_ble_device/config_flow.py +++ b/homeassistant/components/private_ble_device/config_flow.py @@ -19,6 +19,30 @@ _LOGGER = logging.getLogger(__name__) CONF_IRK = "irk" +def _parse_irk(irk: str) -> bytes | None: + if irk.startswith("irk:"): + irk = irk[4:] + + if irk.endswith("="): + try: + irk_bytes = bytes(reversed(base64.b64decode(irk))) + except binascii.Error: + # IRK is not valid base64 + return None + else: + try: + irk_bytes = binascii.unhexlify(irk) + except binascii.Error: + # IRK is not correctly hex encoded + return None + + if len(irk_bytes) != 16: + # IRK must be 16 bytes when decoded + return None + + return irk_bytes + + class BLEDeviceTrackerConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for BLE Device Tracker.""" @@ -35,15 +59,8 @@ class BLEDeviceTrackerConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: irk = user_input[CONF_IRK] - if irk.startswith("irk:"): - irk = irk[4:] - if irk.endswith("="): - irk_bytes = bytes(reversed(base64.b64decode(irk))) - else: - irk_bytes = binascii.unhexlify(irk) - - if len(irk_bytes) != 16: + if not (irk_bytes := _parse_irk(irk)): errors[CONF_IRK] = "irk_not_valid" elif not (service_info := async_last_service_info(self.hass, irk_bytes)): errors[CONF_IRK] = "irk_not_found" diff --git a/tests/components/private_ble_device/test_config_flow.py b/tests/components/private_ble_device/test_config_flow.py index aa8ea0d905c..bb58cfedb29 100644 --- a/tests/components/private_ble_device/test_config_flow.py +++ b/tests/components/private_ble_device/test_config_flow.py @@ -42,6 +42,32 @@ async def test_invalid_irk(hass: HomeAssistant, enable_bluetooth: None) -> None: assert_form_error(result, "irk", "irk_not_valid") +async def test_invalid_irk_base64(hass: HomeAssistant, enable_bluetooth: None) -> None: + """Test invalid irk.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"irk": "Ucredacted4T8n!!ZZZ=="} + ) + assert_form_error(result, "irk", "irk_not_valid") + + +async def test_invalid_irk_hex(hass: HomeAssistant, enable_bluetooth: None) -> None: + """Test invalid irk.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"irk": "irk:abcdefghi"} + ) + assert_form_error(result, "irk", "irk_not_valid") + + async def test_irk_not_found(hass: HomeAssistant, enable_bluetooth: None) -> None: """Test irk not found.""" result = await hass.config_entries.flow.async_init( From 9b810dcf9f9a5bf3645aad90344abe7843cfbe4b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 2 Oct 2023 13:07:56 +0200 Subject: [PATCH 68/95] Downgrade pylitterbot to 2023.4.5 (#101255) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index fd37365eb7d..9a3334cbaac 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.8"] + "requirements": ["pylitterbot==2023.4.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6f83e88ccdc..e53f8951f5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1830,7 +1830,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.8 +pylitterbot==2023.4.5 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a255103e3bf..5822e33f24b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1376,7 +1376,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.8 +pylitterbot==2023.4.5 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.2 From b069f92d953f7b79ee9e20ad3f9fd9120eeb0d19 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Oct 2023 13:01:26 +0200 Subject: [PATCH 69/95] Add missing device class to sensor.DEVICE_CLASS_UNITS (#101256) --- homeassistant/components/sensor/const.py | 1 + tests/components/number/test_init.py | 10 ++++++++++ tests/components/sensor/test_init.py | 13 +++++++++++++ 3 files changed, 24 insertions(+) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 139725ee1ab..e8b1742f315 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -542,6 +542,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { }, SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential), SensorDeviceClass.VOLUME: set(UnitOfVolume), + SensorDeviceClass.VOLUME_STORAGE: set(UnitOfVolume), SensorDeviceClass.WATER: { UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 23758fe345d..3f612c421c8 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -901,3 +901,13 @@ async def test_name(hass: HomeAssistant) -> None: "mode": NumberMode.AUTO, "step": 1.0, } + + +def test_device_class_units(hass: HomeAssistant) -> None: + """Test all numeric device classes have unit.""" + # DEVICE_CLASS_UNITS should include all device classes except: + # - NumberDeviceClass.MONETARY + # - Device classes enumerated in NON_NUMERIC_DEVICE_CLASSES + assert set(NUMBER_DEVICE_CLASS_UNITS) == set( + NumberDeviceClass + ) - NON_NUMERIC_DEVICE_CLASSES - {NumberDeviceClass.MONETARY} diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 07d44207c68..01dfb9b3649 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_STATE_CLASSES, DEVICE_CLASS_UNITS, DOMAIN as SENSOR_DOMAIN, + NON_NUMERIC_DEVICE_CLASSES, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -2483,3 +2484,15 @@ def test_async_rounded_state_registered_entity_with_display_precision( hass.states.async_set(entity_id, "-0.0") state = hass.states.get(entity_id) assert async_rounded_state(hass, entity_id, state) == "0.0000" + + +def test_device_class_units_state_classes(hass: HomeAssistant) -> None: + """Test all numeric device classes have unit and state class.""" + # DEVICE_CLASS_UNITS should include all device classes except: + # - SensorDeviceClass.MONETARY + # - Device classes enumerated in NON_NUMERIC_DEVICE_CLASSES + assert set(DEVICE_CLASS_UNITS) == set( + SensorDeviceClass + ) - NON_NUMERIC_DEVICE_CLASSES - {SensorDeviceClass.MONETARY} + # DEVICE_CLASS_STATE_CLASSES should include all device classes + assert set(DEVICE_CLASS_STATE_CLASSES) == set(SensorDeviceClass) From 5370db4a3e3fd4cbaab9cca03d161f07d44c82f5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 2 Oct 2023 13:49:22 +0200 Subject: [PATCH 70/95] Bump aiowaqi to 2.0.0 (#101259) --- homeassistant/components/waqi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 7b6bd3b8592..a866dc2c902 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", "loggers": ["aiowaqi"], - "requirements": ["aiowaqi==1.1.1"] + "requirements": ["aiowaqi==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e53f8951f5c..4d714f9b835 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -372,7 +372,7 @@ aiovlc==0.1.0 aiovodafone==0.3.1 # homeassistant.components.waqi -aiowaqi==1.1.1 +aiowaqi==2.0.0 # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5822e33f24b..136ec198853 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ aiovlc==0.1.0 aiovodafone==0.3.1 # homeassistant.components.waqi -aiowaqi==1.1.1 +aiowaqi==2.0.0 # homeassistant.components.watttime aiowatttime==0.1.1 From a0e5f016e1736d51c5f543989e21745874c8fedc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Oct 2023 14:30:33 +0200 Subject: [PATCH 71/95] Add documentation URL for the Home Assistant Green (#101263) --- homeassistant/components/homeassistant_green/hardware.py | 3 ++- tests/components/homeassistant_green/test_hardware.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_green/hardware.py b/homeassistant/components/homeassistant_green/hardware.py index 2b5268f8d03..c7b1641c09c 100644 --- a/homeassistant/components/homeassistant_green/hardware.py +++ b/homeassistant/components/homeassistant_green/hardware.py @@ -9,6 +9,7 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN BOARD_NAME = "Home Assistant Green" +DOCUMENTATION_URL = "https://green.home-assistant.io/documentation/" MANUFACTURER = "homeassistant" MODEL = "green" @@ -39,6 +40,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: config_entries=config_entries, dongle=None, name=BOARD_NAME, - url=None, + url=DOCUMENTATION_URL, ) ] diff --git a/tests/components/homeassistant_green/test_hardware.py b/tests/components/homeassistant_green/test_hardware.py index 8aacf09978d..0221bf3a577 100644 --- a/tests/components/homeassistant_green/test_hardware.py +++ b/tests/components/homeassistant_green/test_hardware.py @@ -54,7 +54,7 @@ async def test_hardware_info( "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Green", - "url": None, + "url": "https://green.home-assistant.io/documentation/", } ] } From bad9b1c95f203ec9ef052d38f0d547259fb38791 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 2 Oct 2023 11:03:53 -0400 Subject: [PATCH 72/95] Bump python-myq to 3.1.11 (#101266) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 02bf454bc3e..5efcb8e1bb0 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["pkce", "pymyq"], - "requirements": ["python-myq==3.1.9"] + "requirements": ["python-myq==3.1.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4d714f9b835..11377c14cc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2149,7 +2149,7 @@ python-miio==0.5.12 python-mpd2==3.0.5 # homeassistant.components.myq -python-myq==3.1.9 +python-myq==3.1.11 # homeassistant.components.mystrom python-mystrom==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 136ec198853..9c2750f007b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1599,7 +1599,7 @@ python-matter-server==3.7.0 python-miio==0.5.12 # homeassistant.components.myq -python-myq==3.1.9 +python-myq==3.1.11 # homeassistant.components.mystrom python-mystrom==2.2.0 From 06d6122663f0173e2f20216f55d55ae37a3992e9 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 2 Oct 2023 14:23:13 -0500 Subject: [PATCH 73/95] Bump intents to 2023.10.2 (#101277) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2f733ead486..f11dda15a4e 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.9.22"] + "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.10.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 659caa1078d..db28fbc4ac6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ hass-nabucasa==0.71.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 home-assistant-frontend==20230928.0 -home-assistant-intents==2023.9.22 +home-assistant-intents==2023.10.2 httpx==0.24.1 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 11377c14cc0..ac292c53434 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1000,7 +1000,7 @@ holidays==0.28 home-assistant-frontend==20230928.0 # homeassistant.components.conversation -home-assistant-intents==2023.9.22 +home-assistant-intents==2023.10.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c2750f007b..bd8e3c47874 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -789,7 +789,7 @@ holidays==0.28 home-assistant-frontend==20230928.0 # homeassistant.components.conversation -home-assistant-intents==2023.9.22 +home-assistant-intents==2023.10.2 # homeassistant.components.home_connect homeconnect==0.7.2 From 98d7945521f9530a85bf74b5f15fc1966426a052 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 2 Oct 2023 19:10:15 +0200 Subject: [PATCH 74/95] Revert "Use shorthand attributes in Telldus live" (#101281) --- homeassistant/components/tellduslive/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 06b505d9574..e15f89888b1 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -142,7 +142,6 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): def __init__(self, client, device_id): """Initialize TelldusLiveSensor.""" super().__init__(client, device_id) - self._attr_unique_id = "{}-{}-{}".format(*device_id) if desc := SENSOR_TYPES.get(self._type): self.entity_description = desc else: @@ -190,3 +189,8 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): if self._type == SENSOR_TYPE_LUMINANCE: return self._value_as_luminance return self._value + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return "{}-{}-{}".format(*self._id) From 791293ca87d073706146b3d68659e6fbec6f6b60 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 2 Oct 2023 12:44:47 -0600 Subject: [PATCH 75/95] Bump pylitterbot to 2023.4.9 (#101285) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 9a3334cbaac..ea096a908fc 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.5"] + "requirements": ["pylitterbot==2023.4.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index ac292c53434..c812251cf89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1830,7 +1830,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.5 +pylitterbot==2023.4.9 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd8e3c47874..8da274571a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1376,7 +1376,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.5 +pylitterbot==2023.4.9 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.2 From 9834c1de9aadc2d6951d5d7ecb85344abdc1c04b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Oct 2023 21:34:50 +0200 Subject: [PATCH 76/95] Bumped version to 2023.10.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d656032f32b..d380645df3c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 62e59dbe7cc..b85b6da35cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b4" +version = "2023.10.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0e29ccf06978de22f8eded6518cbb33edb42a744 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Oct 2023 22:56:50 +0200 Subject: [PATCH 77/95] Update frontend to 20231002.0 (#101294) --- 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 9f01fadb710..40339e955f9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230928.0"] + "requirements": ["home-assistant-frontend==20231002.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index db28fbc4ac6..eaba1eb6508 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ ha-av==10.1.1 hass-nabucasa==0.71.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230928.0 +home-assistant-frontend==20231002.0 home-assistant-intents==2023.10.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c812251cf89..8f920edc0ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -997,7 +997,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230928.0 +home-assistant-frontend==20231002.0 # homeassistant.components.conversation home-assistant-intents==2023.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8da274571a3..d068412ef8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -786,7 +786,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230928.0 +home-assistant-frontend==20231002.0 # homeassistant.components.conversation home-assistant-intents==2023.10.2 From be32db70a01c33b6f1ceb60d8eedac1411dbc9b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Oct 2023 23:01:30 +0200 Subject: [PATCH 78/95] Update Lokalise CLI to v2.6.8 (#101297) --- script/translations/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/translations/const.py b/script/translations/const.py index 7c50b7db5e3..ef8e3f2df74 100644 --- a/script/translations/const.py +++ b/script/translations/const.py @@ -3,6 +3,6 @@ import pathlib CORE_PROJECT_ID = "130246255a974bd3b5e8a1.51616605" FRONTEND_PROJECT_ID = "3420425759f6d6d241f598.13594006" -CLI_2_DOCKER_IMAGE = "2.5.1" +CLI_2_DOCKER_IMAGE = "v2.6.8" INTEGRATIONS_DIR = pathlib.Path("homeassistant/components") FRONTEND_DIR = pathlib.Path("../frontend") From a9bc380c32c439909f18c50347ae198b9f737de8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Oct 2023 23:03:02 +0200 Subject: [PATCH 79/95] Bumped version to 2023.10.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d380645df3c..c490d09028d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index b85b6da35cd..95ef4b80d46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b5" +version = "2023.10.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From fd6eb614894abc63a7035d5fb7bbd68579d50f59 Mon Sep 17 00:00:00 2001 From: Aaron Collins Date: Tue, 3 Oct 2023 21:11:21 +1300 Subject: [PATCH 80/95] Remove duplicated device before daikin migration (#99900) Co-authored-by: Erik Montnemery --- homeassistant/components/daikin/__init__.py | 31 +++++- tests/components/daikin/test_init.py | 105 +++++++++++++++++++- 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index f6fd399f855..eda7976e572 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -135,9 +135,11 @@ async def async_migrate_unique_id( ) -> None: """Migrate old entry.""" dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) old_unique_id = config_entry.unique_id new_unique_id = api.device.mac - new_name = api.device.values.get("name") + new_mac = dr.format_mac(new_unique_id) + new_name = api.name @callback def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: @@ -147,15 +149,36 @@ async def async_migrate_unique_id( if new_unique_id == old_unique_id: return + duplicate = dev_reg.async_get_device( + connections={(CONNECTION_NETWORK_MAC, new_mac)}, identifiers=None + ) + + # Remove duplicated device + if duplicate is not None: + if config_entry.entry_id in duplicate.config_entries: + _LOGGER.debug( + "Removing duplicated device %s", + duplicate.name, + ) + + # The automatic cleanup in entity registry is scheduled as a task, remove + # the entities manually to avoid unique_id collision when the entities + # are migrated. + duplicate_entities = er.async_entries_for_device( + ent_reg, duplicate.id, True + ) + for entity in duplicate_entities: + ent_reg.async_remove(entity.entity_id) + + dev_reg.async_remove_device(duplicate.id) + # Migrate devices for device_entry in dr.async_entries_for_config_entry( dev_reg, config_entry.entry_id ): for connection in device_entry.connections: if connection[1] == old_unique_id: - new_connections = { - (CONNECTION_NETWORK_MAC, dr.format_mac(new_unique_id)) - } + new_connections = {(CONNECTION_NETWORK_MAC, new_mac)} _LOGGER.debug( "Migrating device %s connections to %s", diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py index a6a58b4fb39..3b5f81ae2e5 100644 --- a/tests/components/daikin/test_init.py +++ b/tests/components/daikin/test_init.py @@ -1,11 +1,13 @@ """Define tests for the Daikin init.""" import asyncio +from datetime import timedelta from unittest.mock import AsyncMock, PropertyMock, patch from aiohttp import ClientConnectionError +from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.daikin import update_unique_id +from homeassistant.components.daikin import DaikinApi, update_unique_id from homeassistant.components.daikin.const import DOMAIN, KEY_MAC from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST @@ -14,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .test_config_flow import HOST, MAC -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture @@ -28,6 +30,7 @@ def mock_daikin(): with patch("homeassistant.components.daikin.Appliance") as Appliance: Appliance.factory.side_effect = mock_daikin_factory type(Appliance).update_status = AsyncMock() + type(Appliance).device_ip = PropertyMock(return_value=HOST) type(Appliance).inside_temperature = PropertyMock(return_value=22) type(Appliance).target_temperature = PropertyMock(return_value=22) type(Appliance).zones = PropertyMock(return_value=[("Zone 1", "0", 0)]) @@ -47,6 +50,67 @@ DATA = { INVALID_DATA = {**DATA, "name": None, "mac": HOST} +async def test_duplicate_removal(hass: HomeAssistant, mock_daikin) -> None: + """Test duplicate device removal.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=HOST, + title=None, + data={CONF_HOST: HOST, KEY_MAC: HOST}, + ) + config_entry.add_to_hass(hass) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + type(mock_daikin).mac = PropertyMock(return_value=HOST) + type(mock_daikin).values = PropertyMock(return_value=INVALID_DATA) + + with patch( + "homeassistant.components.daikin.async_migrate_unique_id", return_value=None + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + assert config_entry.unique_id != MAC + + type(mock_daikin).mac = PropertyMock(return_value=MAC) + type(mock_daikin).values = PropertyMock(return_value=DATA) + + assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + device_registry.async_get_device({}, {(KEY_MAC, MAC)}).name + == "DaikinAP00000" + ) + + assert device_registry.async_get_device({}, {(KEY_MAC, HOST)}).name is None + + assert entity_registry.async_get("climate.daikin_127_0_0_1").unique_id == HOST + assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith( + HOST + ) + + assert entity_registry.async_get("climate.daikinap00000").unique_id == MAC + assert entity_registry.async_get( + "switch.daikinap00000_zone_1" + ).unique_id.startswith(MAC) + + assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + device_registry.async_get_device({}, {(KEY_MAC, MAC)}).name == "DaikinAP00000" + ) + + assert entity_registry.async_get("climate.daikinap00000") is None + assert entity_registry.async_get("switch.daikinap00000_zone_1") is None + + assert entity_registry.async_get("climate.daikin_127_0_0_1").unique_id == MAC + assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(MAC) + + async def test_unique_id_migrate(hass: HomeAssistant, mock_daikin) -> None: """Test unique id migration.""" config_entry = MockConfigEntry( @@ -97,8 +161,41 @@ async def test_unique_id_migrate(hass: HomeAssistant, mock_daikin) -> None: assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(MAC) +async def test_client_update_connection_error( + hass: HomeAssistant, mock_daikin, freezer: FrozenDateTimeFactory +) -> None: + """Test client connection error on update.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MAC, + data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) + config_entry.add_to_hass(hass) + er.async_get(hass) + + type(mock_daikin).mac = PropertyMock(return_value=MAC) + type(mock_daikin).values = PropertyMock(return_value=DATA) + + await hass.config_entries.async_setup(config_entry.entry_id) + + api: DaikinApi = hass.data[DOMAIN][config_entry.entry_id] + + assert api.available is True + + type(mock_daikin).update_status.side_effect = ClientConnectionError + + freezer.tick(timedelta(seconds=90)) + async_fire_time_changed(hass) + + await hass.async_block_till_done() + + assert api.available is False + + assert mock_daikin.update_status.call_count == 2 + + async def test_client_connection_error(hass: HomeAssistant, mock_daikin) -> None: - """Test unique id migration.""" + """Test client connection error on setup.""" config_entry = MockConfigEntry( domain=DOMAIN, unique_id=MAC, @@ -114,7 +211,7 @@ async def test_client_connection_error(hass: HomeAssistant, mock_daikin) -> None async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: - """Test unique id migration.""" + """Test timeout error on setup.""" config_entry = MockConfigEntry( domain=DOMAIN, unique_id=MAC, From e0cbbf7d57be1d95c0ecf539e3d8b80493460825 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 3 Oct 2023 19:52:01 +1000 Subject: [PATCH 81/95] Revert PR #99077 for Aussie Broadband (#101314) --- .../components/aussie_broadband/__init__.py | 20 ++--------------- .../components/aussie_broadband/test_init.py | 22 ------------------- 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index 1bdb0579976..6fc4a4dd4d1 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -3,11 +3,10 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any from aiohttp import ClientError from aussiebb.asyncio import AussieBB -from aussiebb.const import FETCH_TYPES, NBN_TYPES, PHONE_TYPES +from aussiebb.const import FETCH_TYPES from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType from homeassistant.config_entries import ConfigEntry @@ -23,19 +22,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] -# Backport for the pyaussiebb=0.0.15 validate_service_type method -def validate_service_type(service: dict[str, Any]) -> None: - """Check the service types against known types.""" - - if "type" not in service: - raise ValueError("Field 'type' not found in service data") - if service["type"] not in NBN_TYPES + PHONE_TYPES + ["Hardware"]: - raise UnrecognisedServiceType( - f"Service type {service['type']=} {service['name']=} - not recognised - ", - "please report this at https://github.com/yaleman/aussiebb/issues/new", - ) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aussie Broadband from a config entry.""" # Login to the Aussie Broadband API and retrieve the current service list @@ -44,9 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], async_get_clientsession(hass), ) - # Overwrite the pyaussiebb=0.0.15 validate_service_type method with backport - # Required until pydantic 2.x is supported - client.validate_service_type = validate_service_type + try: await client.login() services = await client.get_services(drop_types=FETCH_TYPES) diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index dc32212ee87..3eb1972011c 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -3,11 +3,8 @@ from unittest.mock import patch from aiohttp import ClientConnectionError from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType -import pydantic -import pytest from homeassistant import data_entry_flow -from homeassistant.components.aussie_broadband import validate_service_type from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -22,19 +19,6 @@ async def test_unload(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED -async def test_validate_service_type() -> None: - """Testing the validation function.""" - test_service = {"type": "Hardware", "name": "test service"} - validate_service_type(test_service) - - with pytest.raises(ValueError): - test_service = {"name": "test service"} - validate_service_type(test_service) - with pytest.raises(UnrecognisedServiceType): - test_service = {"type": "FunkyBob", "name": "test service"} - validate_service_type(test_service) - - async def test_auth_failure(hass: HomeAssistant) -> None: """Test init with an authentication failure.""" with patch( @@ -55,9 +39,3 @@ async def test_service_failure(hass: HomeAssistant) -> None: """Test init with a invalid service.""" entry = await setup_platform(hass, usage_effect=UnrecognisedServiceType()) assert entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_not_pydantic2() -> None: - """Test that Home Assistant still does not support Pydantic 2.""" - """For PR#99077 and validate_service_type backport""" - assert pydantic.__version__ < "2" From 9e4f9a88ad56a17c2cc53634fac14b0e7b391269 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 4 Oct 2023 00:35:26 +1000 Subject: [PATCH 82/95] Fix reference error in Aussie Broadband (#101315) --- homeassistant/components/aussie_broadband/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index 6fc4a4dd4d1..093480afd7d 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -45,10 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: return await client.get_usage(service_id) except UnrecognisedServiceType as err: - raise UpdateFailed( - f"Service {service_id} of type '{services[service_id]['type']}' was" - " unrecognised" - ) from err + raise UpdateFailed(f"Service {service_id} was unrecognised") from err return async_update_data From 38423ad6f1538eadf407f244c7d340ae7764aee7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 3 Oct 2023 12:17:23 +0200 Subject: [PATCH 83/95] Bump pyW800rf32 to 0.4 (#101317) bump pyW800rf32 from 0.3 to 0.4 --- homeassistant/components/w800rf32/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/w800rf32/manifest.json b/homeassistant/components/w800rf32/manifest.json index e76835abcbe..769eb96b3c0 100644 --- a/homeassistant/components/w800rf32/manifest.json +++ b/homeassistant/components/w800rf32/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/w800rf32", "iot_class": "local_push", "loggers": ["W800rf32"], - "requirements": ["pyW800rf32==0.1"] + "requirements": ["pyW800rf32==0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f920edc0ab..e370f6d64a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1565,7 +1565,7 @@ pyTibber==0.28.2 pyW215==0.7.0 # homeassistant.components.w800rf32 -pyW800rf32==0.1 +pyW800rf32==0.4 # homeassistant.components.ads pyads==3.2.2 From 9c5d9344e2b38d123bfa2ba707a9448a71518b5b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 3 Oct 2023 12:41:00 -0500 Subject: [PATCH 84/95] Increase pipeline timeout to 5 minutes (#101327) --- .../assist_pipeline/websocket_api.py | 4 ++-- .../snapshots/test_websocket.ambr | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index f57424223cf..798843ea6e3 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -30,8 +30,8 @@ from .pipeline import ( async_get_pipeline, ) -DEFAULT_TIMEOUT = 30 -DEFAULT_WAKE_WORD_TIMEOUT = 3 +DEFAULT_TIMEOUT = 60 * 5 # seconds +DEFAULT_WAKE_WORD_TIMEOUT = 3 # seconds _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 044e7758eb2..7cecf9fed40 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -5,7 +5,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': 1, - 'timeout': 30, + 'timeout': 300, }), }) # --- @@ -86,7 +86,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': 1, - 'timeout': 30, + 'timeout': 300, }), }) # --- @@ -179,7 +179,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': 1, - 'timeout': 30, + 'timeout': 300, }), }) # --- @@ -359,7 +359,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': 1, - 'timeout': 30, + 'timeout': 300, }), }) # --- @@ -460,7 +460,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': 1, - 'timeout': 30, + 'timeout': 300, }), }) # --- @@ -491,7 +491,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': None, - 'timeout': 30, + 'timeout': 300, }), }) # --- @@ -564,7 +564,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': 1, - 'timeout': 30, + 'timeout': 300, }), }) # --- @@ -590,7 +590,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': None, - 'timeout': 30, + 'timeout': 300, }), }) # --- @@ -640,7 +640,7 @@ 'pipeline': , 'runner_data': dict({ 'stt_binary_handler_id': None, - 'timeout': 30, + 'timeout': 300, }), }) # --- From b9a929e63b0749393373099e04b7b932ea99a0e3 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 3 Oct 2023 16:52:31 -0500 Subject: [PATCH 85/95] Pipeline runs are only equal with same id (#101341) * Pipeline runs are only equal with same id * Use dict instead of list in PipelineRuns * Let it blow up * Test * Test rest of __eq__ --- .../components/assist_pipeline/pipeline.py | 21 +++++++++----- tests/components/assist_pipeline/test_init.py | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 7e4c71671ad..76444fb2436 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -3,7 +3,7 @@ from __future__ import annotations import array import asyncio -from collections import deque +from collections import defaultdict, deque from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable from dataclasses import asdict, dataclass, field from enum import StrEnum @@ -475,7 +475,7 @@ class PipelineRun: stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False, repr=False) tts_engine: str = field(init=False, repr=False) tts_options: dict | None = field(init=False, default=None) - wake_word_entity_id: str = field(init=False, repr=False) + wake_word_entity_id: str | None = field(init=False, default=None, repr=False) wake_word_entity: wake_word.WakeWordDetectionEntity = field(init=False, repr=False) abort_wake_word_detection: bool = field(init=False, default=False) @@ -518,6 +518,13 @@ class PipelineRun: self.audio_settings.noise_suppression_level, ) + def __eq__(self, other: Any) -> bool: + """Compare pipeline runs by id.""" + if isinstance(other, PipelineRun): + return self.id == other.id + + return False + @callback def process_event(self, event: PipelineEvent) -> None: """Log an event and call listener.""" @@ -1565,21 +1572,19 @@ class PipelineRuns: def __init__(self, pipeline_store: PipelineStorageCollection) -> None: """Initialize.""" - self._pipeline_runs: dict[str, list[PipelineRun]] = {} + self._pipeline_runs: dict[str, dict[str, PipelineRun]] = defaultdict(dict) self._pipeline_store = pipeline_store pipeline_store.async_add_listener(self._change_listener) def add_run(self, pipeline_run: PipelineRun) -> None: """Add pipeline run.""" pipeline_id = pipeline_run.pipeline.id - if pipeline_id not in self._pipeline_runs: - self._pipeline_runs[pipeline_id] = [] - self._pipeline_runs[pipeline_id].append(pipeline_run) + self._pipeline_runs[pipeline_id][pipeline_run.id] = pipeline_run def remove_run(self, pipeline_run: PipelineRun) -> None: """Remove pipeline run.""" pipeline_id = pipeline_run.pipeline.id - self._pipeline_runs[pipeline_id].remove(pipeline_run) + self._pipeline_runs[pipeline_id].pop(pipeline_run.id) async def _change_listener( self, change_type: str, item_id: str, change: dict @@ -1589,7 +1594,7 @@ class PipelineRuns: return if pipeline_runs := self._pipeline_runs.get(item_id): # Create a temporary list in case the list is modified while we iterate - for pipeline_run in list(pipeline_runs): + for pipeline_run in list(pipeline_runs.values()): pipeline_run.abort_wake_word_detection = True diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 5258736c89f..98ecae628f1 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -627,3 +627,32 @@ async def test_wake_word_detection_aborted( await pipeline_input.execute() assert process_events(events) == snapshot + + +def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: + """Test that pipeline run equality uses unique id.""" + + def event_callback(event): + pass + + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass) + run_1 = assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.STT, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=event_callback, + ) + run_2 = assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.STT, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=event_callback, + ) + + assert run_1 == run_1 + assert run_1 != run_2 + assert run_1 != 1234 From 776b26de3fddcdf019031a358765ac78813cdbf7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:15:56 +1300 Subject: [PATCH 86/95] Fix manual stopping of the voice assistant pipeline (#101351) --- homeassistant/components/esphome/manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index f9f24128e2a..dfd7376f4f4 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -330,17 +330,17 @@ class ESPHomeManager: return None hass = self.hass - voice_assistant_udp_server = VoiceAssistantUDPServer( + self.voice_assistant_udp_server = VoiceAssistantUDPServer( hass, self.entry_data, self._handle_pipeline_event, self._handle_pipeline_finished, ) - port = await voice_assistant_udp_server.start_server() + port = await self.voice_assistant_udp_server.start_server() assert self.device_id is not None, "Device ID must be set" hass.async_create_background_task( - voice_assistant_udp_server.run_pipeline( + self.voice_assistant_udp_server.run_pipeline( device_id=self.device_id, conversation_id=conversation_id or None, flags=flags, From 937a26117c2a1bc27523d66a7829de655617ed38 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:09:12 +1300 Subject: [PATCH 87/95] Allow esphome device to disable vad on stream (#101352) --- homeassistant/components/esphome/voice_assistant.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index dc36b7475c4..8fba4bfb39a 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -260,6 +260,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): noise_suppression_level=audio_settings.noise_suppression_level, auto_gain_dbfs=audio_settings.auto_gain, volume_multiplier=audio_settings.volume_multiplier, + is_vad_enabled=bool(flags & VoiceAssistantCommandFlag.USE_VAD), ), ) From 55ff8e1fcb77fcf9d824a121a003ba555e273ebc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Oct 2023 22:07:38 -0400 Subject: [PATCH 88/95] Bumped version to 2023.10.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c490d09028d..8af4f1175fb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 95ef4b80d46..5abfa672a84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b6" +version = "2023.10.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9b9a16e9c64a9c05cb2b9f1d744af63fa13c0443 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 4 Oct 2023 18:05:44 +1000 Subject: [PATCH 89/95] Fix temperature when myZone is in use for Advantage air (#101316) --- homeassistant/components/advantage_air/climate.py | 7 +++++++ homeassistant/components/advantage_air/entity.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index fa9f609ba10..cda123f62ee 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -125,6 +125,13 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the current target temperature.""" + # If the system is in MyZone mode, and a zone is set, return that temperature instead. + if ( + self._ac["myZone"] > 0 + and not self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED) + and not self._ac.get(ADVANTAGE_AIR_MYTEMP_ENABLED) + ): + return self._myzone["setTemp"] return self._ac["setTemp"] @property diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 00750fb4e94..b300a677793 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -62,6 +62,12 @@ class AdvantageAirAcEntity(AdvantageAirEntity): def _ac(self) -> dict[str, Any]: return self.coordinator.data["aircons"][self.ac_key]["info"] + @property + def _myzone(self) -> dict[str, Any]: + return self.coordinator.data["aircons"][self.ac_key]["zones"].get( + f"z{self._ac['myZone']:02}" + ) + class AdvantageAirZoneEntity(AdvantageAirAcEntity): """Parent class for Advantage Air Zone Entities.""" From 337f9197bb8fe7c714b45a26fe172388bf832891 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 4 Oct 2023 03:40:03 -0400 Subject: [PATCH 90/95] Check that dock error status is not None for Roborock (#101321) Co-authored-by: Robert Resch --- homeassistant/components/roborock/sensor.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 8a18c281d59..113e02e4abe 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +import datetime from roborock.containers import ( RoborockDockErrorCode, @@ -38,7 +39,7 @@ from .device import RoborockCoordinatedEntity class RoborockSensorDescriptionMixin: """A class that describes sensor entities.""" - value_fn: Callable[[DeviceProp], int] + value_fn: Callable[[DeviceProp], StateType | datetime.datetime] @dataclass @@ -48,6 +49,15 @@ class RoborockSensorDescription( """A class that describes Roborock sensors.""" +def _dock_error_value_fn(properties: DeviceProp) -> str | None: + if ( + status := properties.status.dock_error_status + ) is not None and properties.status.dock_type != RoborockDockTypeCode.no_dock: + return status.name + + return None + + SENSOR_DESCRIPTIONS = [ RoborockSensorDescription( native_unit_of_measurement=UnitOfTime.SECONDS, @@ -173,9 +183,7 @@ SENSOR_DESCRIPTIONS = [ key="dock_error", icon="mdi:garage-open", translation_key="dock_error", - value_fn=lambda data: data.status.dock_error_status.name - if data.status.dock_type != RoborockDockTypeCode.no_dock - else None, + value_fn=_dock_error_value_fn, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=RoborockDockErrorCode.keys(), @@ -228,7 +236,7 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime.datetime: """Return the value reported by the sensor.""" return self.entity_description.value_fn( self.coordinator.roborock_device_info.props From ebde9914f220e5431854782a2f76cc1dd2ec1b68 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 4 Oct 2023 09:19:57 +0200 Subject: [PATCH 91/95] Increase update interval of update platform in devolo_home_network (#101366) Increase update interval of firmware platform --- homeassistant/components/devolo_home_network/__init__.py | 3 ++- homeassistant/components/devolo_home_network/const.py | 1 + tests/components/devolo_home_network/test_update.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index d76a6163516..94e848fe8af 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -35,6 +35,7 @@ from .const import ( CONNECTED_PLC_DEVICES, CONNECTED_WIFI_CLIENTS, DOMAIN, + FIRMWARE_UPDATE_INTERVAL, LONG_UPDATE_INTERVAL, NEIGHBORING_WIFI_NETWORKS, REGULAR_FIRMWARE, @@ -146,7 +147,7 @@ async def async_setup_entry( # noqa: C901 _LOGGER, name=REGULAR_FIRMWARE, update_method=async_update_firmware_available, - update_interval=LONG_UPDATE_INTERVAL, + update_interval=FIRMWARE_UPDATE_INTERVAL, ) if device.device and "wifi1" in device.device.features: coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator( diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index ba3f5e5b815..aaee8051cb5 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -14,6 +14,7 @@ PRODUCT = "product" SERIAL_NUMBER = "serial_number" TITLE = "title" +FIRMWARE_UPDATE_INTERVAL = timedelta(hours=5) LONG_UPDATE_INTERVAL = timedelta(minutes=5) SHORT_UPDATE_INTERVAL = timedelta(seconds=15) diff --git a/tests/components/devolo_home_network/test_update.py b/tests/components/devolo_home_network/test_update.py index 97d313d9273..cb6de649e8e 100644 --- a/tests/components/devolo_home_network/test_update.py +++ b/tests/components/devolo_home_network/test_update.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.devolo_home_network.const import ( DOMAIN, - LONG_UPDATE_INTERVAL, + FIRMWARE_UPDATE_INTERVAL, ) from homeassistant.components.update import ( DOMAIN as PLATFORM, @@ -78,7 +78,7 @@ async def test_update_firmware( mock_device.device.async_check_firmware_available.return_value = ( UpdateFirmwareCheck(result=UPDATE_NOT_AVAILABLE) ) - freezer.tick(LONG_UPDATE_INTERVAL) + freezer.tick(FIRMWARE_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -106,7 +106,7 @@ async def test_device_failure_check( assert state is not None mock_device.device.async_check_firmware_available.side_effect = DeviceUnavailable - freezer.tick(LONG_UPDATE_INTERVAL) + freezer.tick(FIRMWARE_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() From 0470ca3e76b0d814500ec0b06246c3ba0a042d29 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Oct 2023 09:54:43 +0200 Subject: [PATCH 92/95] Update Pillow to 10.0.1 (#101368) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image_upload/manifest.json | 2 +- homeassistant/components/matrix/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index bc7c7d97430..12397eb8990 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/doods", "iot_class": "local_polling", "loggers": ["pydoods"], - "requirements": ["pydoods==1.0.2", "Pillow==10.0.0"] + "requirements": ["pydoods==1.0.2", "Pillow==10.0.1"] } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index a89ee370920..2966d668ac9 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "iot_class": "local_push", - "requirements": ["ha-av==10.1.1", "Pillow==10.0.0"] + "requirements": ["ha-av==10.1.1", "Pillow==10.0.1"] } diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index 4f139785cd3..b6c74f0c53c 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/image_upload", "integration_type": "system", "quality_scale": "internal", - "requirements": ["Pillow==10.0.0"] + "requirements": ["Pillow==10.0.1"] } diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 74bb97d10fc..69d059fdce5 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.21.2", "Pillow==10.0.0"] + "requirements": ["matrix-nio==0.21.2", "Pillow==10.0.1"] } diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index b38bc93567d..b5b25a66342 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,5 +3,5 @@ "name": "Camera Proxy", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["Pillow==10.0.0"] + "requirements": ["Pillow==10.0.1"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 2176aa0c91e..f1f40dd8973 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/qrcode", "iot_class": "calculated", "loggers": ["pyzbar"], - "requirements": ["Pillow==10.0.0", "pyzbar==0.1.7"] + "requirements": ["Pillow==10.0.1", "pyzbar==0.1.7"] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index ed8638d8419..2b730648e22 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/seven_segments", "iot_class": "local_polling", - "requirements": ["Pillow==10.0.0"] + "requirements": ["Pillow==10.0.1"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 33080a9c1a2..d1bc97da7a8 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/sighthound", "iot_class": "cloud_polling", "loggers": ["simplehound"], - "requirements": ["Pillow==10.0.0", "simplehound==0.3"] + "requirements": ["Pillow==10.0.1", "simplehound==0.3"] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index bfd3e77ee50..c8682941e28 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -10,6 +10,6 @@ "tf-models-official==2.5.0", "pycocotools==2.0.6", "numpy==1.26.0", - "Pillow==10.0.0" + "Pillow==10.0.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eaba1eb6508..51d03a40971 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ mutagen==1.47.0 orjson==3.9.7 packaging>=23.1 paho-mqtt==1.6.1 -Pillow==10.0.0 +Pillow==10.0.1 pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.8.0 diff --git a/requirements_all.txt b/requirements_all.txt index e370f6d64a3..cbf8738cb2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -43,7 +43,7 @@ Mastodon.py==1.5.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==10.0.0 +Pillow==10.0.1 # homeassistant.components.plex PlexAPI==4.15.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d068412ef8e..083595f13aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,7 +39,7 @@ HATasmota==0.7.3 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==10.0.0 +Pillow==10.0.1 # homeassistant.components.plex PlexAPI==4.15.3 From 8e05df2b44cb946e126c3123486b8f3853c0e13f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Oct 2023 10:11:43 +0200 Subject: [PATCH 93/95] Bumped version to 2023.10.0b8 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8af4f1175fb..d9199dc035c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 5abfa672a84..56d469272d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b7" +version = "2023.10.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 512b2af13cc84237129772750df4c12aad105e1a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Oct 2023 10:24:20 +0200 Subject: [PATCH 94/95] Bumped version to 2023.10.0b9 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d9199dc035c..41d1ad3449b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 56d469272d8..0052c4999d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b8" +version = "2023.10.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 01daae69ab016f19da3dc62f1cb2dcad2dbacd4b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Oct 2023 13:48:40 +0200 Subject: [PATCH 95/95] Bumped version to 2023.10.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 41d1ad3449b..c027875eae1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "0b9" +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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 0052c4999d0..9e153b6cc4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0b9" +version = "2023.10.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"