From 4d63bf473d689fd575365937de9ac63eb0826833 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 19 Sep 2024 09:50:47 +0200 Subject: [PATCH 01/16] Add validation to set_humidity action in humidifier (#125863) --- .../components/humidifier/__init__.py | 38 ++++++++- .../components/humidifier/strings.json | 5 ++ tests/components/humidifier/conftest.py | 69 +++++++++++++++ tests/components/humidifier/test_init.py | 85 ++++++++++++++++++- 4 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 tests/components/humidifier/conftest.py diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 37e2bd3e3ba882..605bd4284f8b5a 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -18,7 +18,8 @@ SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, @@ -45,7 +46,13 @@ DOMAIN, MODE_AUTO, MODE_AWAY, + MODE_BABY, + MODE_BOOST, + MODE_COMFORT, + MODE_ECO, + MODE_HOME, MODE_NORMAL, + MODE_SLEEP, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, HumidifierAction, @@ -108,7 +115,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: vol.Coerce(int), vol.Range(min=0, max=100) ) }, - "async_set_humidity", + async_service_humidity_set, ) return True @@ -281,6 +288,33 @@ def supported_features_compat(self) -> HumidifierEntityFeature: return features +async def async_service_humidity_set( + entity: HumidifierEntity, service_call: ServiceCall +) -> None: + """Handle set humidity service.""" + humidity = service_call.data[ATTR_HUMIDITY] + min_humidity = entity.min_humidity + max_humidity = entity.max_humidity + _LOGGER.debug( + "Check valid humidity %d in range %d - %d", + humidity, + min_humidity, + max_humidity, + ) + if humidity < min_humidity or humidity > max_humidity: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="humidity_out_of_range", + translation_placeholders={ + "humidity": str(humidity), + "min_humidity": str(min_humidity), + "max_humidity": str(max_humidity), + }, + ) + + await entity.async_set_humidity(humidity) + + # As we import deprecated constants from the const module, we need to add these two functions # otherwise this module will be logged for using deprecated constants and not the custom component # These can be removed if no deprecated constant are in this module anymore diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index 0416f4a68a6338..753368dc572aab 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -115,5 +115,10 @@ "name": "[%key:common::action::toggle%]", "description": "Toggles the humidifier on/off." } + }, + "exceptions": { + "humidity_out_of_range": { + "message": "Provided humidity {humidity} is not valid. Accepted range is {min_humidity} to {max_humidity}." + } } } diff --git a/tests/components/humidifier/conftest.py b/tests/components/humidifier/conftest.py new file mode 100644 index 00000000000000..9fe1720ffc0871 --- /dev/null +++ b/tests/components/humidifier/conftest.py @@ -0,0 +1,69 @@ +"""Fixtures for Humidifier platform tests.""" + +from collections.abc import Generator + +import pytest + +from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from tests.common import ( + MockConfigEntry, + MockModule, + mock_config_flow, + mock_integration, + mock_platform, +) + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: + """Mock config flow.""" + mock_platform(hass, "test.config_flow") + + with mock_config_flow("test", MockFlow): + yield + + +@pytest.fixture +def register_test_integration( + hass: HomeAssistant, config_flow_fixture: None +) -> Generator: + """Provide a mocked integration for tests.""" + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + async def help_async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups( + config_entry, [HUMIDIFIER_DOMAIN] + ) + return True + + async def help_async_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Unload test config emntry.""" + return await hass.config_entries.async_unload_platforms( + config_entry, [Platform.HUMIDIFIER] + ) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + ) + + return config_entry diff --git a/tests/components/humidifier/test_init.py b/tests/components/humidifier/test_init.py index b31750a3a3b65a..2725f942576243 100644 --- a/tests/components/humidifier/test_init.py +++ b/tests/components/humidifier/test_init.py @@ -8,16 +8,28 @@ from homeassistant.components import humidifier from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, ATTR_MODE, + DOMAIN as HUMIDIFIER_DOMAIN, + MODE_ECO, + MODE_NORMAL, + SERVICE_SET_HUMIDITY, HumidifierEntity, HumidifierEntityFeature, ) from homeassistant.core import HomeAssistant - -from tests.common import help_test_all, import_and_test_deprecated_constant_enum +from homeassistant.exceptions import ServiceValidationError + +from tests.common import ( + MockConfigEntry, + MockEntity, + help_test_all, + import_and_test_deprecated_constant_enum, + setup_test_component_platform, +) -class MockHumidifierEntity(HumidifierEntity): +class MockHumidifierEntity(MockEntity, HumidifierEntity): """Mock Humidifier device to use in tests.""" @property @@ -101,3 +113,70 @@ def supported_features(self) -> int: assert "is using deprecated supported features values" not in caplog.text assert entity.state_attributes[ATTR_MODE] == "mode1" + + +async def test_humidity_validation( + hass: HomeAssistant, + register_test_integration: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test validation for humidity.""" + + class MockHumidifierEntityHumidity(MockEntity, HumidifierEntity): + """Mock climate class with mocked aux heater.""" + + _attr_supported_features = HumidifierEntityFeature.MODES + _attr_available_modes = [MODE_NORMAL, MODE_ECO] + _attr_mode = MODE_NORMAL + _attr_target_humidity = 50 + _attr_min_humidity = 50 + _attr_max_humidity = 60 + + def set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + self._attr_target_humidity = humidity + + test_humidifier = MockHumidifierEntityHumidity( + name="Test", + unique_id="unique_humidifier_test", + ) + + setup_test_component_platform( + hass, HUMIDIFIER_DOMAIN, entities=[test_humidifier], from_config_entry=True + ) + await hass.config_entries.async_setup(register_test_integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.attributes.get(ATTR_HUMIDITY) == 50 + + with pytest.raises( + ServiceValidationError, + match="Provided humidity 1 is not valid. Accepted range is 50 to 60", + ) as exc: + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + { + "entity_id": "humidifier.test", + ATTR_HUMIDITY: "1", + }, + blocking=True, + ) + + assert exc.value.translation_key == "humidity_out_of_range" + assert "Check valid humidity 1 in range 50 - 60" in caplog.text + + with pytest.raises( + ServiceValidationError, + match="Provided humidity 70 is not valid. Accepted range is 50 to 60", + ) as exc: + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + { + "entity_id": "humidifier.test", + ATTR_HUMIDITY: "70", + }, + blocking=True, + ) From 1dd1de2636e54df75976eeaecf93cd004145ce5e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 19 Sep 2024 10:07:28 +0200 Subject: [PATCH 02/16] Pass default value in Z-Wave websocket handler for configuration values (#125343) * Pass default value in zwave websocket handler for configuration values * Update test --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 1 + tests/components/zwave_js/test_api.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 8f81790708f4fd..b43528fe358d0b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1713,6 +1713,7 @@ async def websocket_get_config_parameters( "unit": metadata.unit, "writeable": metadata.writeable, "readable": metadata.readable, + "default": metadata.default, }, "value": zwave_value.value, } diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 0437f9d908500d..bb236ea9acbafa 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3048,9 +3048,21 @@ async def test_get_config_parameters( assert result[key]["property"] == 2 assert result[key]["property_key"] is None assert result[key]["endpoint"] == 0 - assert result[key]["metadata"]["type"] == "number" assert result[key]["configuration_value_type"] == "enumerated" assert result[key]["metadata"]["states"] + assert ( + result[key]["metadata"]["description"] + == "Stay awake for 10 minutes at power on" + ) + assert result[key]["metadata"]["label"] == "Stay Awake in Battery Mode" + assert result[key]["metadata"]["type"] == "number" + assert result[key]["metadata"]["min"] == 0 + assert result[key]["metadata"]["max"] == 1 + assert result[key]["metadata"]["unit"] is None + assert result[key]["metadata"]["writeable"] is True + assert result[key]["metadata"]["readable"] is True + assert result[key]["metadata"]["default"] == 0 + assert result[key]["value"] == 0 key = "52-112-0-201-255" assert result[key]["property_key"] == 255 From 31f9687ba1cfa9e5f6b9382fc7ffc70922c5bdaf Mon Sep 17 00:00:00 2001 From: TimL Date: Thu, 19 Sep 2024 18:29:02 +1000 Subject: [PATCH 03/16] Update repairs for Smlight integration to allow firmware updates where possible (#126113) * Dont launch SSE client for core firmware 0.9.9 * Dont offer updates on core firmware 0.9.9 * Add correct firmware done event for legacy v2 firmware * test update legacy v2 firmware * Dont raise issue for firmware v2 --- homeassistant/components/smlight/__init__.py | 6 +- .../components/smlight/coordinator.py | 5 +- homeassistant/components/smlight/update.py | 8 +++ tests/components/smlight/test_init.py | 4 +- tests/components/smlight/test_update.py | 55 +++++++++++++++++-- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/smlight/__init__.py b/homeassistant/components/smlight/__init__.py index 52db6c8770b8a7..cbfb8162d63541 100644 --- a/homeassistant/components/smlight/__init__.py +++ b/homeassistant/components/smlight/__init__.py @@ -36,7 +36,6 @@ class SmlightData: async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool: """Set up SMLIGHT Zigbee from a config entry.""" client = Api2(host=entry.data[CONF_HOST], session=async_get_clientsession(hass)) - entry.async_create_background_task(hass, client.sse.client(), "smlight-sse-client") data_coordinator = SmDataUpdateCoordinator(hass, entry.data[CONF_HOST], client) firmware_coordinator = SmFirmwareUpdateCoordinator( @@ -46,6 +45,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool: await data_coordinator.async_config_entry_first_refresh() await firmware_coordinator.async_config_entry_first_refresh() + if data_coordinator.data.info.legacy_api < 2: + entry.async_create_background_task( + hass, client.sse.client(), "smlight-sse-client" + ) + entry.runtime_data = SmlightData( data=data_coordinator, firmware=firmware_coordinator ) diff --git a/homeassistant/components/smlight/coordinator.py b/homeassistant/components/smlight/coordinator.py index e5ef21bd53190e..5b38ec4a89ea5c 100644 --- a/homeassistant/components/smlight/coordinator.py +++ b/homeassistant/components/smlight/coordinator.py @@ -80,9 +80,8 @@ async def _async_setup(self) -> None: info = await self.client.get_info() self.unique_id = format_mac(info.MAC) - - if info.legacy_api: - self.legacy_api = info.legacy_api + self.legacy_api = info.legacy_api + if info.legacy_api == 2: ir.async_create_issue( self.hass, DOMAIN, diff --git a/homeassistant/components/smlight/update.py b/homeassistant/components/smlight/update.py index e00499760b1aa0..cb28a1978605a6 100644 --- a/homeassistant/components/smlight/update.py +++ b/homeassistant/components/smlight/update.py @@ -102,6 +102,8 @@ def installed_version(self) -> str | None: def latest_version(self) -> str | None: """Latest version available for install.""" data = self.coordinator.data + if self.coordinator.legacy_api == 2: + return None fw = self.entity_description.fw_list(data) @@ -126,6 +128,12 @@ def register_callbacks(self) -> None: SmEvents.FW_UPD_done, self._update_finished ) ) + if self.coordinator.legacy_api == 1: + self._unload.append( + self.coordinator.client.sse.register_callback( + SmEvents.ESP_UPD_done, self._update_finished + ) + ) self._unload.append( self.coordinator.client.sse.register_callback( SmEvents.ZB_FW_err, self._update_failed diff --git a/tests/components/smlight/test_init.py b/tests/components/smlight/test_init.py index eb7b6396d26096..afc53932fb069b 100644 --- a/tests/components/smlight/test_init.py +++ b/tests/components/smlight/test_init.py @@ -122,10 +122,10 @@ async def test_device_legacy_firmware( issue_registry: IssueRegistry, ) -> None: """Test device setup for old firmware version that dont support required API.""" - LEGACY_VERSION = "v2.3.1" + LEGACY_VERSION = "v0.9.9" mock_smlight_client.get_sensors.side_effect = SmlightError mock_smlight_client.get_info.return_value = Info( - legacy_api=1, sw_version=LEGACY_VERSION, MAC="AA:BB:CC:DD:EE:FF" + legacy_api=2, sw_version=LEGACY_VERSION, MAC="AA:BB:CC:DD:EE:FF" ) entry = await setup_integration(hass, mock_config_entry) diff --git a/tests/components/smlight/test_update.py b/tests/components/smlight/test_update.py index b8b8de8a09b2a8..b0b8910ef9bbcc 100644 --- a/tests/components/smlight/test_update.py +++ b/tests/components/smlight/test_update.py @@ -126,10 +126,7 @@ async def test_update_firmware( mock_smlight_client, SmEvents.ZB_FW_prgs ) - async def _call_event_function(event: MessageEvent): - event_function(event) - - await _call_event_function(MOCK_FIRMWARE_PROGRESS) + event_function(MOCK_FIRMWARE_PROGRESS) state = hass.states.get(entity_id) assert state.attributes[ATTR_IN_PROGRESS] == 50 @@ -137,7 +134,55 @@ async def _call_event_function(event: MessageEvent): mock_smlight_client, SmEvents.FW_UPD_done ) - await _call_event_function(MOCK_FIRMWARE_DONE) + event_function(MOCK_FIRMWARE_DONE) + + mock_smlight_client.get_info.return_value = Info( + sw_version="v2.5.2", + ) + + freezer.tick(SCAN_FIRMWARE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.5.2" + assert state.attributes[ATTR_LATEST_VERSION] == "v2.5.2" + + +async def test_update_legacy_firmware_v2( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, +) -> None: + """Test firmware update for legacy v2 firmware.""" + mock_smlight_client.get_info.return_value = Info( + sw_version="v2.0.18", + legacy_api=1, + MAC="AA:BB:CC:DD:EE:FF", + ) + await setup_integration(hass, mock_config_entry) + entity_id = "update.mock_title_core_firmware" + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.0.18" + assert state.attributes[ATTR_LATEST_VERSION] == "v2.5.2" + + await hass.services.async_call( + PLATFORM, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: entity_id}, + blocking=False, + ) + + assert len(mock_smlight_client.fw_update.mock_calls) == 1 + + event_function: Callable[[MessageEvent], None] = get_callback_function( + mock_smlight_client, SmEvents.ESP_UPD_done + ) + + event_function(MOCK_FIRMWARE_DONE) mock_smlight_client.get_info.return_value = Info( sw_version="v2.5.2", From 5d2f8319b159f9e235336f232d7510164e0c37c1 Mon Sep 17 00:00:00 2001 From: Alberto Montes Date: Thu, 19 Sep 2024 10:32:38 +0200 Subject: [PATCH 04/16] Update string formatting to use f-string on tests (#125986) * Update string formatting to use f-string on tests * Update test_package.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update statement given feedback --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/config/test_config_entries.py | 8 +-- tests/components/datadog/test_init.py | 2 +- tests/components/directv/test_media_player.py | 6 +-- .../dte_energy_bridge/test_sensor.py | 6 +-- tests/components/emulated_hue/test_hue_api.py | 8 +-- tests/components/html5/test_notify.py | 2 +- tests/components/http/test_auth.py | 2 +- tests/components/intent/test_init.py | 4 +- tests/components/locative/test_init.py | 42 +++++---------- .../components/lovelace/test_system_health.py | 2 +- .../components/meraki/test_device_tracker.py | 8 +-- .../mobile_app/test_device_tracker.py | 6 +-- tests/components/mobile_app/test_webhook.py | 52 +++++++++---------- .../owntracks/test_device_tracker.py | 2 +- tests/components/ps4/test_init.py | 4 +- tests/components/ps4/test_media_player.py | 33 ++++-------- tests/components/traccar/test_init.py | 26 ++++------ .../trafikverket_ferry/test_config_flow.py | 4 +- .../trafikverket_train/test_config_flow.py | 4 +- tests/helpers/test_dispatcher.py | 2 +- tests/helpers/test_icon.py | 8 +-- tests/util/test_package.py | 4 +- 22 files changed, 87 insertions(+), 148 deletions(-) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index a4dc91d53554d8..4c61ab506e30d2 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -791,9 +791,7 @@ async def async_step_user(self, user_input=None): assert resp.status == HTTPStatus.OK data = await resp.json() - resp2 = await client.get( - "/api/config/config_entries/flow/{}".format(data["flow_id"]) - ) + resp2 = await client.get(f"/api/config/config_entries/flow/{data['flow_id']}") assert resp2.status == HTTPStatus.OK data2 = await resp2.json() @@ -829,9 +827,7 @@ async def async_step_user(self, user_input=None): hass_admin_user.groups = [] - resp2 = await client.get( - "/api/config/config_entries/flow/{}".format(data["flow_id"]) - ) + resp2 = await client.get(f"/api/config/config_entries/flow/{data['flow_id']}") assert resp2.status == HTTPStatus.UNAUTHORIZED diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 36c1d9510781eb..3b7bea3c926a04 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -79,7 +79,7 @@ async def test_logbook_entry(hass: HomeAssistant) -> None: assert mock_statsd.event.call_count == 1 assert mock_statsd.event.call_args == mock.call( title="Home Assistant", - text="%%% \n **{}** {} \n %%%".format(event["name"], event["message"]), + text=f"%%% \n **{event['name']}** {event['message']} \n %%%", tags=["entity:sensor.foo.bar", "domain:automation"], ) diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 33eb35ed26864b..37762a22fe261b 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -215,7 +215,7 @@ async def test_check_attributes( assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) assert state.attributes.get(ATTR_MEDIA_TITLE) == "Snow Bride" assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) is None - assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "{} ({})".format("HALLHD", "312") + assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "HALLHD (312)" assert state.attributes.get(ATTR_INPUT_SOURCE) == "312" assert not state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) assert state.attributes.get(ATTR_MEDIA_RATING) == "TV-G" @@ -234,7 +234,7 @@ async def test_check_attributes( assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) assert state.attributes.get(ATTR_MEDIA_TITLE) == "Tyler's Ultimate" assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) == "Spaghetti and Clam Sauce" - assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "{} ({})".format("FOODHD", "231") + assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "FOODHD (231)" assert state.attributes.get(ATTR_INPUT_SOURCE) == "231" assert not state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) assert state.attributes.get(ATTR_MEDIA_RATING) == "No Rating" @@ -255,7 +255,7 @@ async def test_check_attributes( assert state.attributes.get(ATTR_MEDIA_ARTIST) == "Gerald Albright" assert state.attributes.get(ATTR_MEDIA_ALBUM_NAME) == "Slam Dunk (2014)" assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) is None - assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "{} ({})".format("MCSJ", "851") + assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "MCSJ (851)" assert state.attributes.get(ATTR_INPUT_SOURCE) == "851" assert not state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) assert state.attributes.get(ATTR_MEDIA_RATING) == "TV-PG" diff --git a/tests/components/dte_energy_bridge/test_sensor.py b/tests/components/dte_energy_bridge/test_sensor.py index 244bec4e2709a8..41d340fae48a90 100644 --- a/tests/components/dte_energy_bridge/test_sensor.py +++ b/tests/components/dte_energy_bridge/test_sensor.py @@ -20,7 +20,7 @@ async def test_setup_correct_reading(hass: HomeAssistant) -> None: """Test DTE Energy bridge returns a correct value.""" with requests_mock.Mocker() as mock_req: mock_req.get( - "http://{}/instantaneousdemand".format(DTE_ENERGY_BRIDGE_CONFIG["ip"]), + f"http://{DTE_ENERGY_BRIDGE_CONFIG['ip']}/instantaneousdemand", text=".411 kW", ) assert await async_setup_component( @@ -34,7 +34,7 @@ async def test_setup_incorrect_units_reading(hass: HomeAssistant) -> None: """Test DTE Energy bridge handles a value with incorrect units.""" with requests_mock.Mocker() as mock_req: mock_req.get( - "http://{}/instantaneousdemand".format(DTE_ENERGY_BRIDGE_CONFIG["ip"]), + f"http://{DTE_ENERGY_BRIDGE_CONFIG['ip']}/instantaneousdemand", text="411 kW", ) assert await async_setup_component( @@ -48,7 +48,7 @@ async def test_setup_bad_format_reading(hass: HomeAssistant) -> None: """Test DTE Energy bridge handles an invalid value.""" with requests_mock.Mocker() as mock_req: mock_req.get( - "http://{}/instantaneousdemand".format(DTE_ENERGY_BRIDGE_CONFIG["ip"]), + f"http://{DTE_ENERGY_BRIDGE_CONFIG['ip']}/instantaneousdemand", text="411", ) assert await async_setup_component( diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 28e269fdaeb0b5..a445f8bae0d168 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1248,9 +1248,7 @@ async def test_proper_put_state_request(hue_client: TestClient) -> None: """Test the request to set the state.""" # Test proper on value parsing result = await hue_client.put( - "/api/username/lights/{}/state".format( - ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] - ), + f"/api/username/lights/{ENTITY_NUMBERS_BY_ID['light.ceiling_lights']}/state", data=json.dumps({HUE_API_STATE_ON: 1234}), ) @@ -1258,9 +1256,7 @@ async def test_proper_put_state_request(hue_client: TestClient) -> None: # Test proper brightness value parsing result = await hue_client.put( - "/api/username/lights/{}/state".format( - ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] - ), + f"/api/username/lights/{ENTITY_NUMBERS_BY_ID['light.ceiling_lights']}/state", data=json.dumps({HUE_API_STATE_ON: True, HUE_API_STATE_BRI: "Hello world!"}), ) diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 85a790c06104f6..0d9388907a9366 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -495,7 +495,7 @@ async def test_callback_view_with_jwt( assert push_payload["body"] == "Hello" assert push_payload["icon"] == "beer.png" - bearer_token = "Bearer {}".format(push_payload["data"]["jwt"]) + bearer_token = f"Bearer {push_payload['data']['jwt']}" resp = await client.post( PUBLISH_URL, json={"type": "push"}, headers={AUTHORIZATION: bearer_token} diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 76c512c9686425..052c0031469999 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -312,7 +312,7 @@ async def test_auth_access_signed_path_with_refresh_token( assert data["user_id"] == refresh_token.user.id # Use signature on other path - req = await client.get("/another_path?{}".format(signed_path.split("?")[1])) + req = await client.get(f"/another_path?{signed_path.split('?')[1]}") assert req.status == HTTPStatus.UNAUTHORIZED # We only allow GET diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 7288c4855af83f..659ca16c0bbc7e 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -34,11 +34,11 @@ async def async_handle(self, intent_obj): assert intent_obj.context.user_id == hass_admin_user.id response = intent_obj.create_response() response.async_set_speech( - "I've ordered a {}!".format(intent_obj.slots["type"]["value"]) + f"I've ordered a {intent_obj.slots['type']['value']}!" ) response.async_set_card( "Beer ordered", - "You chose a {}.".format(intent_obj.slots["type"]["value"]), + f"You chose a {intent_obj.slots['type']['value']}.", ) return response diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 8fd239ee39830a..89d26ea6c7a67f 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -134,9 +134,7 @@ async def test_enter_and_exit( req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == "home" data["id"] = "HOME" @@ -146,9 +144,7 @@ async def test_enter_and_exit( req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == "not_home" data["id"] = "hOmE" @@ -158,9 +154,7 @@ async def test_enter_and_exit( req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == "home" data["trigger"] = "exit" @@ -169,9 +163,7 @@ async def test_enter_and_exit( req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == "not_home" data["id"] = "work" @@ -181,9 +173,7 @@ async def test_enter_and_exit( req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == "work" @@ -206,7 +196,7 @@ async def test_exit_after_enter( await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}") assert state.state == "home" data["id"] = "Work" @@ -216,7 +206,7 @@ async def test_exit_after_enter( await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}") assert state.state == "work" data["id"] = "Home" @@ -227,7 +217,7 @@ async def test_exit_after_enter( await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}") assert state.state == "work" @@ -250,7 +240,7 @@ async def test_exit_first( await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}") assert state.state == "not_home" @@ -273,9 +263,7 @@ async def test_two_devices( await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["device"]) - ) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data_device_1['device']}") assert state.state == "not_home" # Enter Home @@ -286,13 +274,9 @@ async def test_two_devices( await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_2["device"]) - ) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data_device_2['device']}") assert state.state == "home" - state = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["device"]) - ) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data_device_1['device']}") assert state.state == "not_home" @@ -318,7 +302,7 @@ async def test_load_unload_entry( await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}") assert state.state == "not_home" assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 diff --git a/tests/components/lovelace/test_system_health.py b/tests/components/lovelace/test_system_health.py index 4fe248fa95059f..251153fe419ae3 100644 --- a/tests/components/lovelace/test_system_health.py +++ b/tests/components/lovelace/test_system_health.py @@ -72,6 +72,6 @@ async def test_system_health_info_yaml_not_found(hass: HomeAssistant) -> None: assert info == { "dashboards": 1, "mode": "yaml", - "error": "{} not found".format(hass.config.path("ui-lovelace.yaml")), + "error": f"{hass.config.path('ui-lovelace.yaml')} not found", "resources": 0, } diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index c3126f7b76abaa..139396a0689e51 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -142,12 +142,8 @@ async def test_data_will_be_saved( req = await meraki_client.post(URL, data=json.dumps(data)) assert req.status == HTTPStatus.OK await hass.async_block_till_done() - state_name = hass.states.get( - "{}.{}".format("device_tracker", "00_26_ab_b8_a9_a4") - ).state + state_name = hass.states.get("device_tracker.00_26_ab_b8_a9_a4").state assert state_name == "home" - state_name = hass.states.get( - "{}.{}".format("device_tracker", "00_26_ab_b8_a9_a5") - ).state + state_name = hass.states.get("device_tracker.00_26_ab_b8_a9_a5").state assert state_name == "home" diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index d1cbc21c36b466..92a956ab6292a7 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -15,7 +15,7 @@ async def test_sending_location( ) -> None: """Test sending a location via a webhook.""" resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": { @@ -48,7 +48,7 @@ async def test_sending_location( assert state.attributes["vertical_accuracy"] == 80 resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": { @@ -87,7 +87,7 @@ async def test_restoring_location( ) -> None: """Test sending a location via a webhook.""" resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": { diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 61e342a45ce448..dda5f369ad5980 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -101,7 +101,7 @@ async def test_webhook_handle_render_template( ) -> None: """Test that we render templates properly.""" resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "render_template", "data": { @@ -133,7 +133,7 @@ async def test_webhook_handle_call_services( calls = async_mock_service(hass, "test", "mobile_app") resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json=CALL_SERVICE, ) @@ -158,7 +158,7 @@ def store_event(event): hass.bus.async_listen("test_event", store_event) resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json=FIRE_EVENT + f"/api/webhook/{create_registrations[1]['webhook_id']}", json=FIRE_EVENT ) assert resp.status == HTTPStatus.OK @@ -224,7 +224,7 @@ async def test_webhook_handle_get_zones( await hass.services.async_call(ZONE_DOMAIN, "reload", blocking=True) resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={"type": "get_zones"}, ) @@ -317,7 +317,7 @@ async def test_webhook_returns_error_incorrect_json( ) -> None: """Test that an error is returned when JSON is invalid.""" resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), data="not json" + f"/api/webhook/{create_registrations[1]['webhook_id']}", data="not json" ) assert resp.status == HTTPStatus.BAD_REQUEST @@ -350,7 +350,7 @@ async def test_webhook_handle_decryption( container = {"type": msg["type"], "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -374,7 +374,7 @@ async def test_webhook_handle_decryption_legacy( container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -399,7 +399,7 @@ async def test_webhook_handle_decryption_fail( data = encrypt_payload(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -412,7 +412,7 @@ async def test_webhook_handle_decryption_fail( data = encrypt_payload(key, "{not_valid", encode_json=False) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -424,7 +424,7 @@ async def test_webhook_handle_decryption_fail( data = encrypt_payload(key[::-1], RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -444,7 +444,7 @@ async def test_webhook_handle_decryption_legacy_fail( data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -457,7 +457,7 @@ async def test_webhook_handle_decryption_legacy_fail( data = encrypt_payload_legacy(key, "{not_valid", encode_json=False) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -469,7 +469,7 @@ async def test_webhook_handle_decryption_legacy_fail( data = encrypt_payload_legacy(key[::-1], RENDER_TEMPLATE["data"]) container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -490,7 +490,7 @@ async def test_webhook_handle_decryption_legacy_upgrade( container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -508,7 +508,7 @@ async def test_webhook_handle_decryption_legacy_upgrade( container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -526,7 +526,7 @@ async def test_webhook_handle_decryption_legacy_upgrade( container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=container ) assert resp.status == HTTPStatus.OK @@ -539,7 +539,7 @@ async def test_webhook_requires_encryption( ) -> None: """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), + f"/api/webhook/{create_registrations[0]['webhook_id']}", json=RENDER_TEMPLATE, ) @@ -560,7 +560,7 @@ async def test_webhook_update_location_without_locations( # start off with a location set by name resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": {"location_name": STATE_HOME}, @@ -575,7 +575,7 @@ async def test_webhook_update_location_without_locations( # set location to an 'unknown' state resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": {"altitude": 123}, @@ -597,7 +597,7 @@ async def test_webhook_update_location_with_gps( ) -> None: """Test that location can be updated.""" resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": {"gps": [1, 2], "gps_accuracy": 10, "altitude": -10}, @@ -621,7 +621,7 @@ async def test_webhook_update_location_with_gps_without_accuracy( ) -> None: """Test that location can be updated.""" resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": {"gps": [1, 2]}, @@ -659,7 +659,7 @@ async def test_webhook_update_location_with_location_name( await hass.services.async_call(ZONE_DOMAIN, "reload", blocking=True) resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": {"location_name": "zone_name"}, @@ -672,7 +672,7 @@ async def test_webhook_update_location_with_location_name( assert state.state == "zone_name" resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": {"location_name": STATE_HOME}, @@ -685,7 +685,7 @@ async def test_webhook_update_location_with_location_name( assert state.state == STATE_HOME resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "update_location", "data": {"location_name": STATE_NOT_HOME}, @@ -876,7 +876,7 @@ async def test_webhook_handle_scan_tag( events = async_capture_events(hass, EVENT_TAG_SCANNED) resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={"type": "scan_tag", "data": {"tag_id": "mock-tag-id"}}, ) @@ -1052,7 +1052,7 @@ async def test_webhook_handle_conversation_process( return_value=mock_conversation_agent, ): resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + f"/api/webhook/{create_registrations[1]['webhook_id']}", json={ "type": "conversation_process", "data": { diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 2f35139c021dc8..93f40d0ae3d66f 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1540,7 +1540,7 @@ async def test_encrypted_payload_wrong_topic_key( async def test_encrypted_payload_no_topic_key(hass: HomeAssistant, setup_comp) -> None: """Test encrypted payload with no topic key.""" await setup_owntracks( - hass, {CONF_SECRET: {"owntracks/{}/{}".format(USER, "otherdevice"): "foobar"}} + hass, {CONF_SECRET: {f"owntracks/{USER}/otherdevice": "foobar"}} ) await send_message(hass, LOCATION_TOPIC, MOCK_ENCRYPTED_LOCATION_MESSAGE) assert hass.states.get(DEVICE_TRACKER_STATE) is None diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 3a9aac386465ab..d14f367b2bd6a2 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -269,9 +269,7 @@ async def test_send_command(hass: HomeAssistant) -> None: """Test send_command service.""" await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4", ".media_player.PS4Device.async_send_command" - ) + mock_func = "homeassistant.components.ps4.media_player.PS4Device.async_send_command" mock_devices = hass.data[PS4_DATA].devices assert len(mock_devices) == 1 diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 5268306c87af66..737cc3c9f1bef9 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -194,10 +194,7 @@ async def test_state_standby_is_set(hass: HomeAssistant) -> None: async def test_state_playing_is_set(hass: HomeAssistant) -> None: """Test that state is set to playing.""" mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", - "pyps4.Ps4Async.async_get_ps_store_data", - ) + mock_func = "homeassistant.components.ps4.media_player.pyps4.Ps4Async.async_get_ps_store_data" with patch(mock_func, return_value=None): await mock_ddp_response(hass, MOCK_STATUS_PLAYING) @@ -224,10 +221,7 @@ async def test_state_none_is_set(hass: HomeAssistant) -> None: async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None: """Test that media attributes are fetched.""" mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", - "pyps4.Ps4Async.async_get_ps_store_data", - ) + mock_func = "homeassistant.components.ps4.media_player.pyps4.Ps4Async.async_get_ps_store_data" # Mock result from fetching data. mock_result = MagicMock() @@ -276,8 +270,7 @@ async def test_media_attributes_are_loaded( patch_load_json_object.return_value = {MOCK_TITLE_ID: MOCK_GAMES_DATA_LOCKED} with patch( - "homeassistant.components.ps4.media_player." - "pyps4.Ps4Async.async_get_ps_store_data", + "homeassistant.components.ps4.media_player.pyps4.Ps4Async.async_get_ps_store_data", return_value=None, ) as mock_fetch: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) @@ -381,9 +374,7 @@ async def test_device_info_assummed_works( async def test_turn_on(hass: HomeAssistant) -> None: """Test that turn on service calls function.""" mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.wakeup" - ) + mock_func = "homeassistant.components.ps4.media_player.pyps4.Ps4Async.wakeup" with patch(mock_func) as mock_call: await hass.services.async_call( @@ -397,9 +388,7 @@ async def test_turn_on(hass: HomeAssistant) -> None: async def test_turn_off(hass: HomeAssistant) -> None: """Test that turn off service calls function.""" mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.standby" - ) + mock_func = "homeassistant.components.ps4.media_player.pyps4.Ps4Async.standby" with patch(mock_func) as mock_call: await hass.services.async_call( @@ -413,9 +402,7 @@ async def test_turn_off(hass: HomeAssistant) -> None: async def test_toggle(hass: HomeAssistant) -> None: """Test that toggle service calls function.""" mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.toggle" - ) + mock_func = "homeassistant.components.ps4.media_player.pyps4.Ps4Async.toggle" with patch(mock_func) as mock_call: await hass.services.async_call( @@ -429,8 +416,8 @@ async def test_toggle(hass: HomeAssistant) -> None: async def test_media_pause(hass: HomeAssistant) -> None: """Test that media pause service calls function.""" mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" + mock_func = ( + "homeassistant.components.ps4.media_player.pyps4.Ps4Async.remote_control" ) with patch(mock_func) as mock_call: @@ -445,8 +432,8 @@ async def test_media_pause(hass: HomeAssistant) -> None: async def test_media_stop(hass: HomeAssistant) -> None: """Test that media stop service calls function.""" mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" + mock_func = ( + "homeassistant.components.ps4.media_player.pyps4.Ps4Async.remote_control" ) with patch(mock_func) as mock_call: diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 49127aec3471ac..610e741f5f506f 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -121,18 +121,14 @@ async def test_enter_and_exit( req = await client.post(url, params=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['id']}").state assert state_name == STATE_HOME # Enter Home again req = await client.post(url, params=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['id']}").state assert state_name == STATE_HOME data["lon"] = 0 @@ -142,9 +138,7 @@ async def test_enter_and_exit( req = await client.post(url, params=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['id']}").state assert state_name == STATE_NOT_HOME assert len(device_registry.devices) == 1 @@ -171,7 +165,7 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None req = await client.post(url, params=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['id']}") assert state.state == STATE_NOT_HOME assert state.attributes["gps_accuracy"] == 10.5 assert state.attributes["battery_level"] == 10.0 @@ -194,7 +188,7 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None req = await client.post(url, params=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['id']}") assert state.state == STATE_HOME assert state.attributes["gps_accuracy"] == 123 assert state.attributes["battery_level"] == 23 @@ -214,7 +208,7 @@ async def test_two_devices(hass: HomeAssistant, client, webhook_id) -> None: await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["id"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data_device_1['id']}") assert state.state == "not_home" # Enter Home @@ -226,9 +220,9 @@ async def test_two_devices(hass: HomeAssistant, client, webhook_id) -> None: await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_2["id"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data_device_2['id']}") assert state.state == "home" - state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["id"])) + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data_device_1['id']}") assert state.state == "not_home" @@ -244,9 +238,7 @@ async def test_load_unload_entry(hass: HomeAssistant, client, webhook_id) -> Non req = await client.post(url, params=data) await hass.async_block_till_done() assert req.status == HTTPStatus.OK - state_name = hass.states.get( - "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) - ).state + state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['id']}").state assert state_name == STATE_HOME assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 diff --git a/tests/components/trafikverket_ferry/test_config_flow.py b/tests/components/trafikverket_ferry/test_config_flow.py index 916f9c9f2ec80a..5671d9d3fb75f6 100644 --- a/tests/components/trafikverket_ferry/test_config_flow.py +++ b/tests/components/trafikverket_ferry/test_config_flow.py @@ -62,9 +62,7 @@ async def test_form(hass: HomeAssistant) -> None: "weekday": ["mon", "fri"], } assert len(mock_setup_entry.mock_calls) == 1 - assert result2["result"].unique_id == "{}-{}-{}-{}".format( - "eker\u00f6", "slagsta", "10:00", "['mon', 'fri']" - ) + assert result2["result"].unique_id == "eker\u00f6-slagsta-10:00-['mon', 'fri']" @pytest.mark.parametrize( diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 3090a9fe3373b9..9fe02994f05097 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -73,9 +73,7 @@ async def test_form(hass: HomeAssistant) -> None: } assert result["options"] == {"filter_product": None} assert len(mock_setup_entry.mock_calls) == 1 - assert result["result"].unique_id == "{}-{}-{}-{}".format( - "stockholmc", "uppsalac", "10:00", "['mon', 'fri']" - ) + assert result["result"].unique_id == "stockholmc-uppsalac-10:00-['mon', 'fri']" async def test_form_entry_already_exist(hass: HomeAssistant) -> None: diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index 0350b2e6e3a66b..edd18d54db407d 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -73,7 +73,7 @@ def test_funct(data1: str, data2: int) -> None: assert calls == [("Hello", 2)] # Test compatibility with string keys - async_dispatcher_send(hass, "test-{}".format("unique-id"), "x", 4) + async_dispatcher_send(hass, "test-unique-id", "x", 4) await hass.async_block_till_done() assert calls == [("Hello", 2), ("x", 4)] diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index e0dc89f53223fe..ad5c852ded97b9 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -25,12 +25,8 @@ def test_battery_icon() -> None: iconbase = "mdi:battery" for level in range(0, 100, 5): print( # noqa: T201 - "Level: %d. icon: %s, charging: %s" - % ( - level, - icon.icon_for_battery_level(level, False), - icon.icon_for_battery_level(level, True), - ) + f"Level: {level}. icon: {icon.icon_for_battery_level(level, False)}, " + f"charging: {icon.icon_for_battery_level(level, True)}" ) if level <= 10: postfix_charging = "-outline" diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 59a02bff838ac3..1015225491471c 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -19,9 +19,7 @@ TEST_NEW_REQ = "pyhelloworld3==1.0.0" -TEST_ZIP_REQ = "file://{}#{}".format( - os.path.join(RESOURCE_DIR, "pyhelloworld3.zip"), TEST_NEW_REQ -) +TEST_ZIP_REQ = f"file://{RESOURCE_DIR}/pyhelloworld3.zip#{TEST_NEW_REQ}" @pytest.fixture From 8ca33104018581c9419e38c304998a7b9df2f856 Mon Sep 17 00:00:00 2001 From: Arun Philip Date: Thu, 19 Sep 2024 04:34:27 -0400 Subject: [PATCH 05/16] Fix qbittorrent error when torrent count is 0 (#126146) Fix handling of `NoneType` for torrents in `count_torrents_in_states` function Added a check to handle cases where the 'torrents' data is None, avoiding a `TypeError` when attempting to get the length of a `NoneType` object. The function now returns 0 if 'torrents' is None, ensuring robust behavior when no torrent data is available. --- homeassistant/components/qbittorrent/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index cd65fb766e430e..68de7e1d5e5558 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -177,8 +177,12 @@ def count_torrents_in_states( # When torrents are not in the returned data, there are none, return 0. try: torrents = cast(Mapping[str, Mapping], coordinator.data.get("torrents")) + if torrents is None: + return 0 + if not states: return len(torrents) + return len( [torrent for torrent in torrents.values() if torrent.get("state") in states] ) From 3981c878602bb2b8392a85aa23e3db4ceb209855 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 19 Sep 2024 10:45:26 +0200 Subject: [PATCH 06/16] Prevent blocking event loop in ps4 (#126151) * Prevent blocking event loop in ps4 * Process code review comment --- homeassistant/components/ps4/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index ecd20e2d71d46d..8db24beae2047f 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -96,11 +96,10 @@ def __init__( self._retry = 0 self._disconnected = False - @callback def status_callback(self) -> None: """Handle status callback. Parse status.""" self._parse_status() - self.async_write_ha_state() + self.schedule_update_ha_state() @callback def subscribe_to_protocol(self) -> None: @@ -157,7 +156,7 @@ async def async_update(self) -> None: self._ps4.ddp_protocol = self.hass.data[PS4_DATA].protocol self.subscribe_to_protocol() - self._parse_status() + await self.hass.async_add_executor_job(self._parse_status) def _parse_status(self) -> None: """Parse status.""" From 3c99fad6b90e3556033d5a1825e11d893cdfb687 Mon Sep 17 00:00:00 2001 From: Iskra kranj <162285659+iskrakranj@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:48:42 +0200 Subject: [PATCH 07/16] Add counters to iskra integration (#126046) * Added counters to iskra integration * reverted pyiskra bump as reviewed * Fixed iskra integration according to review * fixed iskra integration according to review --- homeassistant/components/iskra/const.py | 4 ++ homeassistant/components/iskra/sensor.py | 57 ++++++++++++++++++++- homeassistant/components/iskra/strings.json | 36 +++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/iskra/const.py b/homeassistant/components/iskra/const.py index 5fc3b501962ebb..a4ed36b50b28be 100644 --- a/homeassistant/components/iskra/const.py +++ b/homeassistant/components/iskra/const.py @@ -21,5 +21,9 @@ ATTR_PHASE2_CURRENT = "phase2_current" ATTR_PHASE3_CURRENT = "phase3_current" +# Counters +ATTR_NON_RESETTABLE_COUNTER = "non_resettable_counter_{}" +ATTR_RESETTABLE_COUNTER = "resettable_counter_{}" + # Frequency ATTR_FREQUENCY = "frequency" diff --git a/homeassistant/components/iskra/sensor.py b/homeassistant/components/iskra/sensor.py index 9e9976749a15be..df9e3ec53f9e5f 100644 --- a/homeassistant/components/iskra/sensor.py +++ b/homeassistant/components/iskra/sensor.py @@ -3,9 +3,10 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import dataclass, replace from pyiskra.devices import Device +from pyiskra.helper import Counter, CounterType from homeassistant.components.sensor import ( SensorDeviceClass, @@ -17,6 +18,7 @@ UnitOfApparentPower, UnitOfElectricCurrent, UnitOfElectricPotential, + UnitOfEnergy, UnitOfFrequency, UnitOfPower, UnitOfReactivePower, @@ -27,6 +29,7 @@ from . import IskraConfigEntry from .const import ( ATTR_FREQUENCY, + ATTR_NON_RESETTABLE_COUNTER, ATTR_PHASE1_CURRENT, ATTR_PHASE1_POWER, ATTR_PHASE1_VOLTAGE, @@ -36,6 +39,7 @@ ATTR_PHASE3_CURRENT, ATTR_PHASE3_POWER, ATTR_PHASE3_VOLTAGE, + ATTR_RESETTABLE_COUNTER, ATTR_TOTAL_ACTIVE_POWER, ATTR_TOTAL_APPARENT_POWER, ATTR_TOTAL_REACTIVE_POWER, @@ -163,6 +167,44 @@ class IskraSensorEntityDescription(SensorEntityDescription): ) +def get_counter_entity_description( + counter: Counter, + index: int, + entity_name: str, +) -> IskraSensorEntityDescription: + """Dynamically create IskraSensor object as energy meter's counters are customizable.""" + + key = entity_name.format(index + 1) + + if entity_name == ATTR_NON_RESETTABLE_COUNTER: + entity_description = IskraSensorEntityDescription( + key=key, + translation_key=key, + state_class=SensorStateClass.TOTAL_INCREASING, + value_func=lambda device: device.counters.non_resettable[index].value, + native_unit_of_measurement=counter.units, + ) + else: + entity_description = IskraSensorEntityDescription( + key=key, + translation_key=key, + state_class=SensorStateClass.TOTAL_INCREASING, + value_func=lambda device: device.counters.resettable[index].value, + native_unit_of_measurement=counter.units, + ) + + # Set unit of measurement and device class based on counter type + # HA's Energy device class supports only active energy + if counter.counter_type in [CounterType.ACTIVE_IMPORT, CounterType.ACTIVE_EXPORT]: + entity_description = replace( + entity_description, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ) + + return entity_description + + async def async_setup_entry( hass: HomeAssistant, entry: IskraConfigEntry, @@ -205,6 +247,19 @@ async def async_setup_entry( if description.key in sensors ) + if device.supports_counters: + for index, counter in enumerate(device.counters.non_resettable[:4]): + description = get_counter_entity_description( + counter, index, ATTR_NON_RESETTABLE_COUNTER + ) + entities.append(IskraSensor(coordinator, description)) + + for index, counter in enumerate(device.counters.resettable[:8]): + description = get_counter_entity_description( + counter, index, ATTR_RESETTABLE_COUNTER + ) + entities.append(IskraSensor(coordinator, description)) + async_add_entities(entities) diff --git a/homeassistant/components/iskra/strings.json b/homeassistant/components/iskra/strings.json index bd70336f637a12..5818cdfa1db1af 100644 --- a/homeassistant/components/iskra/strings.json +++ b/homeassistant/components/iskra/strings.json @@ -86,6 +86,42 @@ }, "phase3_current": { "name": "Phase 3 current" + }, + "non_resettable_counter_1": { + "name": "Non Resettable counter 1" + }, + "non_resettable_counter_2": { + "name": "Non Resettable counter 2" + }, + "non_resettable_counter_3": { + "name": "Non Resettable counter 3" + }, + "non_resettable_counter_4": { + "name": "Non Resettable counter 4" + }, + "resettable_counter_1": { + "name": "Resettable counter 1" + }, + "resettable_counter_2": { + "name": "Resettable counter 2" + }, + "resettable_counter_3": { + "name": "Resettable counter 3" + }, + "resettable_counter_4": { + "name": "Resettable counter 4" + }, + "resettable_counter_5": { + "name": "Resettable counter 5" + }, + "resettable_counter_6": { + "name": "Resettable counter 6" + }, + "resettable_counter_7": { + "name": "Resettable counter 7" + }, + "resettable_counter_8": { + "name": "Resettable counter 8" } } } From b787c2617b97e607e5ae6f107e0ae5ec2c463082 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 19 Sep 2024 10:59:54 +0200 Subject: [PATCH 08/16] Revert "Fix missing id in Habitica completed todos API response" (#126142) Revert "Fix missing id in Habitica completed todos API response (#124565)" This reverts commit c9e7c76ee55c628e59c659bd331ab6bf0352bed6. --- .../components/habitica/coordinator.py | 9 +------ tests/components/habitica/test_init.py | 24 +++++++++---------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index 357643593e4249..4e949b703fb3e7 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -56,14 +56,7 @@ async def _async_update_data(self) -> HabiticaData: try: user_response = await self.api.user.get() tasks_response = await self.api.tasks.user.get() - tasks_response.extend( - [ - {"id": task["_id"], **task} - for task in await self.api.tasks.user.get(type="completedTodos") - if task.get("_id") - ] - ) - + tasks_response.extend(await self.api.tasks.user.get(type="completedTodos")) except ClientResponseError as error: if error.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.debug("Currently rate limited, skipping update") diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 56f17bc98893ce..683472a720fd8d 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -74,31 +74,31 @@ def common_requests(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker: } }, ) + aioclient_mock.get( - "https://habitica.com/api/v3/tasks/user?type=completedTodos", + "https://habitica.com/api/v3/tasks/user", json={ "data": [ { - "text": "this is a mock todo #5", - "id": 5, - "_id": 5, - "type": "todo", - "completed": True, + "text": f"this is a mock {task} #{i}", + "id": f"{i}", + "type": task, + "completed": False, } + for i, task in enumerate(("habit", "daily", "todo", "reward"), start=1) ] }, ) aioclient_mock.get( - "https://habitica.com/api/v3/tasks/user", + "https://habitica.com/api/v3/tasks/user?type=completedTodos", json={ "data": [ { - "text": f"this is a mock {task} #{i}", - "id": f"{i}", - "type": task, - "completed": False, + "text": "this is a mock todo #5", + "id": 5, + "type": "todo", + "completed": True, } - for i, task in enumerate(("habit", "daily", "todo", "reward"), start=1) ] }, ) From c94bb6c1db9daa2aa9028e495c8e2cb5d9576ef2 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 19 Sep 2024 11:00:22 +0200 Subject: [PATCH 09/16] Add new method version_is_newer to Update platform (#124797) * Allow string comparing in update platform * new approach after architecture discussion * cleanup * Update homeassistant/components/update/__init__.py Co-authored-by: Erik Montnemery * Update homeassistant/components/update/__init__.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * add tests * Update tests/components/update/test_init.py Co-authored-by: Erik Montnemery * Update tests/components/update/test_init.py Co-authored-by: Erik Montnemery * Update tests/components/update/test_init.py Co-authored-by: Erik Montnemery * update docstrings * one more docstring --------- Co-authored-by: Erik Montnemery Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/update/__init__.py | 9 ++++- tests/components/update/test_init.py | 41 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index cd52de6550ffff..90495871cb2886 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -181,7 +181,7 @@ class UpdateEntityDescription(EntityDescription, frozen_or_thawed=True): @lru_cache(maxsize=256) def _version_is_newer(latest_version: str, installed_version: str) -> bool: - """Return True if version is newer.""" + """Return True if latest_version is newer than installed_version.""" return AwesomeVersion(latest_version) > installed_version @@ -384,6 +384,11 @@ def release_notes(self) -> str | None: """ raise NotImplementedError + def version_is_newer(self, latest_version: str, installed_version: str) -> bool: + """Return True if latest_version is newer than installed_version.""" + # We don't inline the `_version_is_newer` function because of caching + return _version_is_newer(latest_version, installed_version) + @property @final def state(self) -> str | None: @@ -399,7 +404,7 @@ def state(self) -> str | None: return STATE_OFF try: - newer = _version_is_newer(latest_version, installed_version) + newer = self.version_is_newer(latest_version, installed_version) except AwesomeVersionCompareException: # Can't compare versions, already tried exact match return STATE_ON diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index 7860c679f372f1..6082e0ecfe7ca2 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -3,6 +3,7 @@ from collections.abc import Generator from unittest.mock import MagicMock, patch +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy import pytest from homeassistant.components.update import ( @@ -956,3 +957,43 @@ async def async_setup_entry_platform( }, blocking=True, ) + + +async def test_custom_version_is_newer(hass: HomeAssistant) -> None: + """Test UpdateEntity with overridden version_is_newer method.""" + + class MockUpdateEntity(UpdateEntity): + def version_is_newer(self, latest_version: str, installed_version: str) -> bool: + """Return True if latest_version is newer than installed_version.""" + return AwesomeVersion( + latest_version, + find_first_match=True, + ensure_strategy=[AwesomeVersionStrategy.SEMVER], + ) > AwesomeVersion( + installed_version, + find_first_match=True, + ensure_strategy=[AwesomeVersionStrategy.SEMVER], + ) + + update = MockUpdateEntity() + update.hass = hass + update.platform = MockEntityPlatform(hass) + + STABLE = "20230913-111730/v1.14.0-gcb84623" + BETA = "20231107-162609/v1.14.1-rc1-g0617c15" + + # Set current installed version to STABLE + update._attr_installed_version = STABLE + update._attr_latest_version = BETA + + assert update.installed_version == STABLE + assert update.latest_version == BETA + assert update.state == STATE_ON + + # Set current installed version to BETA + update._attr_installed_version = BETA + update._attr_latest_version = STABLE + + assert update.installed_version == BETA + assert update.latest_version == STABLE + assert update.state == STATE_OFF From e40a853fdb7a4db6cf131dddfe6ed607cfd6b45a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:03:20 +0200 Subject: [PATCH 10/16] Fix set temperature action in AVM FRITZ!SmartHome (#126072) * fix set_temperature logic * improvements --- homeassistant/components/fritzbox/climate.py | 12 +- tests/components/fritzbox/test_climate.py | 141 ++++++++----------- 2 files changed, 67 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 5288682c38878b..61e75bec000901 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -135,14 +135,16 @@ def target_temperature(self) -> float: async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if kwargs.get(ATTR_HVAC_MODE) is not None: - hvac_mode = kwargs[ATTR_HVAC_MODE] + target_temp = kwargs.get(ATTR_TEMPERATURE) + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + if hvac_mode == HVACMode.OFF: await self.async_set_hvac_mode(hvac_mode) - elif kwargs.get(ATTR_TEMPERATURE) is not None: - temperature = kwargs[ATTR_TEMPERATURE] + elif target_temp is not None: await self.hass.async_add_executor_job( - self.data.set_target_temperature, temperature + self.data.set_target_temperature, target_temp ) + else: + return await self.coordinator.async_refresh() @property diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 062ba4f865f156..6bd405aa5ab751 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,7 +1,7 @@ """Tests for AVM Fritz!Box climate component.""" from datetime import timedelta -from unittest.mock import Mock, call +from unittest.mock import Mock, _Call, call from freezegun.api import FrozenDateTimeFactory import pytest @@ -15,6 +15,8 @@ ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_PRESET_MODES, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, PRESET_COMFORT, PRESET_ECO, @@ -270,46 +272,41 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: assert fritz().login.call_count == 4 -async def test_set_temperature_temperature(hass: HomeAssistant, fritz: Mock) -> None: - """Test setting temperature by temperature.""" - device = FritzDeviceClimateMock() - assert await setup_config_entry( - hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz - ) - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 23}, - True, - ) - assert device.set_target_temperature.call_args_list == [call(23)] - - -async def test_set_temperature_mode_off(hass: HomeAssistant, fritz: Mock) -> None: - """Test setting temperature by mode.""" - device = FritzDeviceClimateMock() - assert await setup_config_entry( - hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz - ) - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - { - ATTR_ENTITY_ID: ENTITY_ID, - ATTR_HVAC_MODE: HVACMode.OFF, - ATTR_TEMPERATURE: 23, - }, - True, - ) - assert device.set_target_temperature.call_args_list == [call(0)] - - -async def test_set_temperature_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: - """Test setting temperature by mode.""" +@pytest.mark.parametrize( + ("service_data", "expected_call_args"), + [ + ({ATTR_TEMPERATURE: 23}, [call(23)]), + ( + { + ATTR_HVAC_MODE: HVACMode.OFF, + ATTR_TEMPERATURE: 23, + }, + [call(0)], + ), + ( + { + ATTR_HVAC_MODE: HVACMode.HEAT, + ATTR_TEMPERATURE: 23, + }, + [call(23)], + ), + ( + { + ATTR_TARGET_TEMP_HIGH: 16, + ATTR_TARGET_TEMP_LOW: 10, + }, + [], + ), + ], +) +async def test_set_temperature( + hass: HomeAssistant, + fritz: Mock, + service_data: dict, + expected_call_args: list[_Call], +) -> None: + """Test setting temperature.""" device = FritzDeviceClimateMock() - device.target_temperature = 0.0 assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -317,35 +314,32 @@ async def test_set_temperature_mode_heat(hass: HomeAssistant, fritz: Mock) -> No await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - { - ATTR_ENTITY_ID: ENTITY_ID, - ATTR_HVAC_MODE: HVACMode.HEAT, - ATTR_TEMPERATURE: 23, - }, + {ATTR_ENTITY_ID: ENTITY_ID, **service_data}, True, ) - assert device.set_target_temperature.call_args_list == [call(22)] + assert device.set_target_temperature.call_count == len(expected_call_args) + assert device.set_target_temperature.call_args_list == expected_call_args -async def test_set_hvac_mode_off(hass: HomeAssistant, fritz: Mock) -> None: - """Test setting hvac mode.""" - device = FritzDeviceClimateMock() - assert await setup_config_entry( - hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz - ) - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF}, - True, - ) - assert device.set_target_temperature.call_args_list == [call(0)] - - -async def test_no_reset_hvac_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: +@pytest.mark.parametrize( + ("service_data", "target_temperature", "expected_call_args"), + [ + ({ATTR_HVAC_MODE: HVACMode.OFF}, 22, [call(0)]), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, [call(22)]), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 18, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, []), + ], +) +async def test_set_hvac_mode( + hass: HomeAssistant, + fritz: Mock, + service_data: dict, + target_temperature: float, + expected_call_args: list[_Call], +) -> None: """Test setting hvac mode.""" device = FritzDeviceClimateMock() + device.target_temperature = target_temperature assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -353,27 +347,12 @@ async def test_no_reset_hvac_mode_heat(hass: HomeAssistant, fritz: Mock) -> None await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + {ATTR_ENTITY_ID: ENTITY_ID, **service_data}, True, ) - assert device.set_target_temperature.call_count == 0 - -async def test_set_hvac_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: - """Test setting hvac mode.""" - device = FritzDeviceClimateMock() - device.target_temperature = 0.0 - assert await setup_config_entry( - hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz - ) - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, - True, - ) - assert device.set_target_temperature.call_args_list == [call(22)] + assert device.set_target_temperature.call_count == len(expected_call_args) + assert device.set_target_temperature.call_args_list == expected_call_args async def test_set_preset_mode_comfort(hass: HomeAssistant, fritz: Mock) -> None: From bc3a42c65876548c585290f820901832766cbb37 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:03:54 +0200 Subject: [PATCH 11/16] Fix serial handling in ViCare integration (#125495) * hand down device serial into common entity * fix platforms * Revert "fix platforms" This reverts commit 067af2b567538989f97c5a764be64f8744663daf. * handle event loop issue * hand in serial * Revert "Revert "fix platforms"" This reverts commit 9bbb55ee6da96ea31b98896e82c4b45ab001707b. * fix get serial call * handle other exceptions * also check device model for migration * merge entity and device migration * add test fixture without serial * adjust test cases * add dummy fixture * remove commented code * modify migration * use continue * break comment --- homeassistant/components/vicare/__init__.py | 110 +++++++++--------- .../components/vicare/binary_sensor.py | 15 ++- homeassistant/components/vicare/button.py | 6 +- homeassistant/components/vicare/climate.py | 8 +- homeassistant/components/vicare/entity.py | 6 +- homeassistant/components/vicare/fan.py | 8 +- homeassistant/components/vicare/number.py | 9 +- homeassistant/components/vicare/sensor.py | 15 ++- homeassistant/components/vicare/utils.py | 24 +++- .../components/vicare/water_heater.py | 6 +- .../fixtures/dummy-device-no-serial.json | 3 + tests/components/vicare/test_init.py | 82 +++++++------ 12 files changed, 183 insertions(+), 109 deletions(-) create mode 100644 tests/components/vicare/fixtures/dummy-device-no-serial.json diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index ead210e281646f..d6b9e4b923a287 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -18,7 +18,7 @@ from homeassistant.components.climate import DOMAIN as DOMAIN_CLIMATE from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.storage import STORAGE_DIR @@ -31,7 +31,7 @@ UNSUPPORTED_DEVICES, ) from .types import ViCareDevice -from .utils import get_device +from .utils import get_device, get_device_serial _LOGGER = logging.getLogger(__name__) _TOKEN_FILENAME = "vicare_token.save" @@ -51,9 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for device in hass.data[DOMAIN][entry.entry_id][DEVICE_LIST]: # Migration can be removed in 2025.4.0 - await async_migrate_devices(hass, entry, device) - # Migration can be removed in 2025.4.0 - await async_migrate_entities(hass, entry, device) + await async_migrate_devices_and_entities(hass, entry, device) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -117,70 +115,72 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_migrate_devices( +async def async_migrate_devices_and_entities( hass: HomeAssistant, entry: ConfigEntry, device: ViCareDevice ) -> None: """Migrate old entry.""" - registry = dr.async_get(hass) + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) gateway_serial: str = device.config.getConfig().serial - device_serial: str = device.api.getSerial() + device_id = device.config.getId() + device_serial: str | None = await hass.async_add_executor_job( + get_device_serial, device.api + ) + device_model = device.config.getModel() old_identifier = gateway_serial - new_identifier = f"{gateway_serial}_{device_serial}" + new_identifier = ( + f"{gateway_serial}_{device_serial if device_serial is not None else device_id}" + ) # Migrate devices - for device_entry in dr.async_entries_for_config_entry(registry, entry.entry_id): - if device_entry.identifiers == {(DOMAIN, old_identifier)}: - _LOGGER.debug("Migrating device %s", device_entry.name) - registry.async_update_device( + for device_entry in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ): + if ( + device_entry.identifiers == {(DOMAIN, old_identifier)} + and device_entry.model == device_model + ): + _LOGGER.debug( + "Migrating device %s to new identifier %s", + device_entry.name, + new_identifier, + ) + device_registry.async_update_device( device_entry.id, serial_number=device_serial, new_identifiers={(DOMAIN, new_identifier)}, ) - -async def async_migrate_entities( - hass: HomeAssistant, entry: ConfigEntry, device: ViCareDevice -) -> None: - """Migrate old entry.""" - gateway_serial: str = device.config.getConfig().serial - device_serial: str = device.api.getSerial() - new_identifier = f"{gateway_serial}_{device_serial}" - - @callback - def _update_unique_id( - entity_entry: er.RegistryEntry, - ) -> dict[str, str] | None: - """Update unique ID of entity entry.""" - if not entity_entry.unique_id.startswith(gateway_serial): - # belongs to other device/gateway - return None - if entity_entry.unique_id.startswith(f"{gateway_serial}_"): - # Already correct, nothing to do - return None - - unique_id_parts = entity_entry.unique_id.split("-") - unique_id_parts[0] = new_identifier - - # convert climate entity unique id from `-` to `-heating-` - if entity_entry.domain == DOMAIN_CLIMATE: - unique_id_parts[len(unique_id_parts) - 1] = ( - f"{entity_entry.translation_key}-{unique_id_parts[len(unique_id_parts)-1]}" - ) - - entity_new_unique_id = "-".join(unique_id_parts) - - _LOGGER.debug( - "Migrating entity %s from %s to new id %s", - entity_entry.entity_id, - entity_entry.unique_id, - entity_new_unique_id, - ) - return {"new_unique_id": entity_new_unique_id} - - # Migrate entities - await er.async_migrate_entries(hass, entry.entry_id, _update_unique_id) + # Migrate entities + for entity_entry in er.async_entries_for_device( + entity_registry, device_entry.id, True + ): + if entity_entry.unique_id.startswith(new_identifier): + # already correct, nothing to do + continue + unique_id_parts = entity_entry.unique_id.split("-") + # replace old prefix `` + # with `_` + unique_id_parts[0] = new_identifier + # convert climate entity unique id + # from `-` + # to `-heating-` + if entity_entry.domain == DOMAIN_CLIMATE: + unique_id_parts[len(unique_id_parts) - 1] = ( + f"{entity_entry.translation_key}-{unique_id_parts[len(unique_id_parts)-1]}" + ) + entity_new_unique_id = "-".join(unique_id_parts) + + _LOGGER.debug( + "Migrating entity %s to new unique id %s", + entity_entry.name, + entity_new_unique_id, + ) + entity_registry.async_update_entity( + entity_id=entity_entry.entity_id, new_unique_id=entity_new_unique_id + ) def get_supported_devices( diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 7fe248fa266f59..55f0ab96ed0595 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -31,7 +31,13 @@ from .const import DEVICE_LIST, DOMAIN from .entity import ViCareEntity from .types import ViCareDevice, ViCareRequiredKeysMixin -from .utils import get_burners, get_circuits, get_compressors, is_supported +from .utils import ( + get_burners, + get_circuits, + get_compressors, + get_device_serial, + is_supported, +) _LOGGER = logging.getLogger(__name__) @@ -116,6 +122,7 @@ def _build_entities( entities.extend( ViCareBinarySensor( description, + get_device_serial(device.api), device.config, device.api, ) @@ -131,6 +138,7 @@ def _build_entities( entities.extend( ViCareBinarySensor( description, + get_device_serial(device.api), device.config, device.api, component, @@ -166,12 +174,15 @@ class ViCareBinarySensor(ViCareEntity, BinarySensorEntity): def __init__( self, description: ViCareBinarySensorEntityDescription, + device_serial: str | None, device_config: PyViCareDeviceConfig, device: PyViCareDevice, component: PyViCareHeatingDeviceComponent | None = None, ) -> None: """Initialize the sensor.""" - super().__init__(description.key, device_config, device, component) + super().__init__( + description.key, device_serial, device_config, device, component + ) self.entity_description = description @property diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index 51a763c1fccd49..49d142c1edba34 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -24,7 +24,7 @@ from .const import DEVICE_LIST, DOMAIN from .entity import ViCareEntity from .types import ViCareDevice, ViCareRequiredKeysMixinWithSet -from .utils import is_supported +from .utils import get_device_serial, is_supported _LOGGER = logging.getLogger(__name__) @@ -55,6 +55,7 @@ def _build_entities( return [ ViCareButton( description, + get_device_serial(device.api), device.config, device.api, ) @@ -88,11 +89,12 @@ class ViCareButton(ViCareEntity, ButtonEntity): def __init__( self, description: ViCareButtonEntityDescription, + device_serial: str | None, device_config: PyViCareDeviceConfig, device: PyViCareDevice, ) -> None: """Initialize the button.""" - super().__init__(description.key, device_config, device) + super().__init__(description.key, device_serial, device_config, device) self.entity_description = description def press(self) -> None: diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 410395760eabd4..b742ad257fa862 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -40,7 +40,7 @@ from .const import DEVICE_LIST, DOMAIN from .entity import ViCareEntity from .types import HeatingProgram, ViCareDevice -from .utils import get_burners, get_circuits, get_compressors +from .utils import get_burners, get_circuits, get_compressors, get_device_serial _LOGGER = logging.getLogger(__name__) @@ -87,6 +87,7 @@ def _build_entities( """Create ViCare climate entities for a device.""" return [ ViCareClimate( + get_device_serial(device.api), device.config, device.api, circuit, @@ -143,12 +144,15 @@ class ViCareClimate(ViCareEntity, ClimateEntity): def __init__( self, + device_serial: str | None, device_config: PyViCareDeviceConfig, device: PyViCareDevice, circuit: PyViCareHeatingCircuit, ) -> None: """Initialize the climate device.""" - super().__init__(self._attr_translation_key, device_config, device, circuit) + super().__init__( + self._attr_translation_key, device_serial, device_config, device, circuit + ) self._device = device self._attributes: dict[str, Any] = {} self._attributes["vicare_programs"] = self._api.getPrograms() diff --git a/homeassistant/components/vicare/entity.py b/homeassistant/components/vicare/entity.py index f48243e83e165c..dfb8c48dfc3e19 100644 --- a/homeassistant/components/vicare/entity.py +++ b/homeassistant/components/vicare/entity.py @@ -20,14 +20,16 @@ class ViCareEntity(Entity): def __init__( self, unique_id_suffix: str, + device_serial: str | None, device_config: PyViCareDeviceConfig, device: PyViCareDevice, component: PyViCareHeatingDeviceComponent | None = None, ) -> None: """Initialize the entity.""" gateway_serial = device_config.getConfig().serial - device_serial = device.getSerial() - identifier = f"{gateway_serial}_{device_serial}" + device_id = device_config.getId() + + identifier = f"{gateway_serial}_{device_serial if device_serial is not None else device_id}" self._api: PyViCareDevice | PyViCareHeatingDeviceComponent = ( component if component else device diff --git a/homeassistant/components/vicare/fan.py b/homeassistant/components/vicare/fan.py index d7dbd037b569a4..b787de207736a8 100644 --- a/homeassistant/components/vicare/fan.py +++ b/homeassistant/components/vicare/fan.py @@ -29,6 +29,7 @@ from .const import DEVICE_LIST, DOMAIN from .entity import ViCareEntity +from .utils import get_device_serial _LOGGER = logging.getLogger(__name__) @@ -100,7 +101,7 @@ async def async_setup_entry( async_add_entities( [ - ViCareFan(device.config, device.api) + ViCareFan(get_device_serial(device.api), device.config, device.api) for device in device_list if isinstance(device.api, PyViCareVentilationDevice) ] @@ -125,11 +126,14 @@ class ViCareFan(ViCareEntity, FanEntity): def __init__( self, + device_serial: str | None, device_config: PyViCareDeviceConfig, device: PyViCareDevice, ) -> None: """Initialize the fan entity.""" - super().__init__(self._attr_translation_key, device_config, device) + super().__init__( + self._attr_translation_key, device_serial, device_config, device + ) def update(self) -> None: """Update state of fan.""" diff --git a/homeassistant/components/vicare/number.py b/homeassistant/components/vicare/number.py index a7f679f7224e71..529caca6a873d1 100644 --- a/homeassistant/components/vicare/number.py +++ b/homeassistant/components/vicare/number.py @@ -33,7 +33,7 @@ from .const import DEVICE_LIST, DOMAIN from .entity import ViCareEntity from .types import HeatingProgram, ViCareDevice, ViCareRequiredKeysMixin -from .utils import get_circuits, is_supported +from .utils import get_circuits, get_device_serial, is_supported _LOGGER = logging.getLogger(__name__) @@ -279,6 +279,7 @@ def _build_entities( entities.extend( ViCareNumber( description, + get_device_serial(device.api), device.config, device.api, ) @@ -289,6 +290,7 @@ def _build_entities( entities.extend( ViCareNumber( description, + get_device_serial(device.api), device.config, device.api, circuit, @@ -324,12 +326,15 @@ class ViCareNumber(ViCareEntity, NumberEntity): def __init__( self, description: ViCareNumberEntityDescription, + device_serial: str | None, device_config: PyViCareDeviceConfig, device: PyViCareDevice, component: PyViCareHeatingDeviceComponent | None = None, ) -> None: """Initialize the number.""" - super().__init__(description.key, device_config, device, component) + super().__init__( + description.key, device_serial, device_config, device, component + ) self.entity_description = description @property diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index bdcb6dfa3aa01d..79a93ffa345824 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -51,7 +51,13 @@ ) from .entity import ViCareEntity from .types import ViCareDevice, ViCareRequiredKeysMixin -from .utils import get_burners, get_circuits, get_compressors, is_supported +from .utils import ( + get_burners, + get_circuits, + get_compressors, + get_device_serial, + is_supported, +) _LOGGER = logging.getLogger(__name__) @@ -868,6 +874,7 @@ def _build_entities( entities.extend( ViCareSensor( description, + get_device_serial(device.api), device.config, device.api, ) @@ -883,6 +890,7 @@ def _build_entities( entities.extend( ViCareSensor( description, + get_device_serial(device.api), device.config, device.api, component, @@ -920,12 +928,15 @@ class ViCareSensor(ViCareEntity, SensorEntity): def __init__( self, description: ViCareSensorEntityDescription, + device_serial: str | None, device_config: PyViCareDeviceConfig, device: PyViCareDevice, component: PyViCareHeatingDeviceComponent | None = None, ) -> None: """Initialize the sensor.""" - super().__init__(description.key, device_config, device, component) + super().__init__( + description.key, device_serial, device_config, device, component + ) self.entity_description = description @property diff --git a/homeassistant/components/vicare/utils.py b/homeassistant/components/vicare/utils.py index 2ba5ddbfb0ab97..5156ea4a41e833 100644 --- a/homeassistant/components/vicare/utils.py +++ b/homeassistant/components/vicare/utils.py @@ -7,7 +7,12 @@ from PyViCare.PyViCareHeatingDevice import ( HeatingDeviceWithComponent as PyViCareHeatingDeviceComponent, ) -from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) +import requests from homeassistant.config_entries import ConfigEntry @@ -27,6 +32,23 @@ def get_device( )() +def get_device_serial(device: PyViCareDevice) -> str | None: + """Get device serial for device if supported.""" + try: + return device.getSerial() + except PyViCareNotSupportedFeatureError: + _LOGGER.debug("Device does not offer a 'device.serial' data point") + except PyViCareRateLimitError as limit_exception: + _LOGGER.debug("Vicare API rate limit exceeded: %s", limit_exception) + except PyViCareInvalidDataError as invalid_data_exception: + _LOGGER.debug("Invalid data from Vicare server: %s", invalid_data_exception) + except requests.exceptions.ConnectionError: + _LOGGER.debug("Unable to retrieve data from ViCare server") + except ValueError: + _LOGGER.debug("Unable to decode data from ViCare server") + return None + + def is_supported( name: str, entity_description: ViCareRequiredKeysMixin, diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 621d2f2a09ba90..5e241c9a3be01b 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -28,7 +28,7 @@ from .const import DEVICE_LIST, DOMAIN from .entity import ViCareEntity from .types import ViCareDevice -from .utils import get_circuits +from .utils import get_circuits, get_device_serial _LOGGER = logging.getLogger(__name__) @@ -69,6 +69,7 @@ def _build_entities( return [ ViCareWater( + get_device_serial(device.api), device.config, device.api, circuit, @@ -108,12 +109,13 @@ class ViCareWater(ViCareEntity, WaterHeaterEntity): def __init__( self, + device_serial: str | None, device_config: PyViCareDeviceConfig, device: PyViCareDevice, circuit: PyViCareHeatingCircuit, ) -> None: """Initialize the DHW water_heater device.""" - super().__init__(circuit.id, device_config, device) + super().__init__(circuit.id, device_serial, device_config, device) self._circuit = circuit self._attributes: dict[str, Any] = {} diff --git a/tests/components/vicare/fixtures/dummy-device-no-serial.json b/tests/components/vicare/fixtures/dummy-device-no-serial.json new file mode 100644 index 00000000000000..268c73f0e37f83 --- /dev/null +++ b/tests/components/vicare/fixtures/dummy-device-no-serial.json @@ -0,0 +1,3 @@ +{ + "data": [] +} diff --git a/tests/components/vicare/test_init.py b/tests/components/vicare/test_init.py index fea7b5985f1cb7..62bec7f50c52c8 100644 --- a/tests/components/vicare/test_init.py +++ b/tests/components/vicare/test_init.py @@ -14,74 +14,78 @@ # Device migration test can be removed in 2025.4.0 -async def test_device_migration( +async def test_device_and_entity_migration( hass: HomeAssistant, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, mock_config_entry: MockConfigEntry, ) -> None: """Test that the device registry is updated correctly.""" - fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] + fixtures: list[Fixture] = [ + Fixture({"type:boiler"}, "vicare/Vitodens300W.json"), + Fixture({"type:boiler"}, "vicare/dummy-device-no-serial.json"), + ] with ( patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.CLIMATE]), ): mock_config_entry.add_to_hass(hass) - device_registry.async_get_or_create( + # device with serial data point + device0 = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={ (DOMAIN, "gateway0"), }, + model="model0", ) - - await hass.config_entries.async_setup(mock_config_entry.entry_id) - - await hass.async_block_till_done() - - assert device_registry.async_get_device(identifiers={(DOMAIN, "gateway0")}) is None - - assert ( - device_registry.async_get_device( - identifiers={(DOMAIN, "gateway0_deviceSerialVitodens300W")} + entry0 = entity_registry.async_get_or_create( + domain=Platform.CLIMATE, + platform=DOMAIN, + config_entry=mock_config_entry, + unique_id="gateway0-0", + translation_key="heating", + device_id=device0.id, ) - is not None - ) - - -# Entity migration test can be removed in 2025.4.0 -async def test_climate_entity_migration( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that the climate entity unique_id gets migrated correctly.""" - fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] - with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), - patch(f"{MODULE}.PLATFORMS", [Platform.CLIMATE]), - ): - mock_config_entry.add_to_hass(hass) - entry1 = entity_registry.async_get_or_create( domain=Platform.CLIMATE, platform=DOMAIN, config_entry=mock_config_entry, - unique_id="gateway0-0", + unique_id="gateway0_deviceSerialVitodens300W-heating-1", translation_key="heating", + device_id=device0.id, + ) + # device without serial data point + device1 = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={ + (DOMAIN, "gateway1"), + }, + model="model1", ) entry2 = entity_registry.async_get_or_create( domain=Platform.CLIMATE, platform=DOMAIN, config_entry=mock_config_entry, - unique_id="gateway0_deviceSerialVitodens300W-heating-1", + unique_id="gateway1-0", translation_key="heating", + device_id=device1.id, + ) + # device is not provided by api + device2 = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={ + (DOMAIN, "gateway2"), + }, + model="model2", ) entry3 = entity_registry.async_get_or_create( domain=Platform.CLIMATE, platform=DOMAIN, config_entry=mock_config_entry, - unique_id="gateway1-0", + unique_id="gateway2-0", translation_key="heating", + device_id=device2.id, ) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -89,11 +93,15 @@ async def test_climate_entity_migration( await hass.async_block_till_done() assert ( - entity_registry.async_get(entry1.entity_id).unique_id + entity_registry.async_get(entry0.entity_id).unique_id == "gateway0_deviceSerialVitodens300W-heating-0" ) assert ( - entity_registry.async_get(entry2.entity_id).unique_id + entity_registry.async_get(entry1.entity_id).unique_id == "gateway0_deviceSerialVitodens300W-heating-1" ) - assert entity_registry.async_get(entry3.entity_id).unique_id == "gateway1-0" + assert ( + entity_registry.async_get(entry2.entity_id).unique_id + == "gateway1_deviceId1-heating-0" + ) + assert entity_registry.async_get(entry3.entity_id).unique_id == "gateway2-0" From d90cdf24f595c88ccc90ceea5b0f64686b2b325c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 19 Sep 2024 19:04:27 +1000 Subject: [PATCH 12/16] Fix wall connector state in Teslemetry (#124149) * Fix wall connector state * review feedback * Rename None to Disconnected * Translate disconnected --- homeassistant/components/teslemetry/entity.py | 7 +++++++ homeassistant/components/teslemetry/sensor.py | 19 ++++++++++--------- .../components/teslemetry/strings.json | 5 ++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 74c1fdd52b1489..bba678f754bc90 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -192,3 +192,10 @@ def _value(self) -> int: .get(self.din, {}) .get(self.key) ) + + @property + def exists(self) -> bool: + """Return True if it exists in the wall connector coordinator data.""" + return self.key in self.coordinator.data.get("wall_connectors", {}).get( + self.din, {} + ) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 90b37cc1dace10..b63f6b905b497c 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -379,18 +379,18 @@ class TeslemetryTimeEntityDescription(SensorEntityDescription): SensorEntityDescription(key="island_status", device_class=SensorDeviceClass.ENUM), ) -WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +WALL_CONNECTOR_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = ( + TeslemetrySensorEntityDescription( key="wall_connector_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + TeslemetrySensorEntityDescription( key="wall_connector_fault_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + TeslemetrySensorEntityDescription( key="wall_connector_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, @@ -398,8 +398,9 @@ class TeslemetryTimeEntityDescription(SensorEntityDescription): suggested_display_precision=2, device_class=SensorDeviceClass.POWER, ), - SensorEntityDescription( + TeslemetrySensorEntityDescription( key="vin", + value_fn=lambda vin: vin or "disconnected", ), ) @@ -525,13 +526,13 @@ def _async_update_attrs(self) -> None: class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorEntity): """Base class for Teslemetry energy site metric sensors.""" - entity_description: SensorEntityDescription + entity_description: TeslemetrySensorEntityDescription def __init__( self, data: TeslemetryEnergyData, din: str, - description: SensorEntityDescription, + description: TeslemetrySensorEntityDescription, ) -> None: """Initialize the sensor.""" self.entity_description = description @@ -543,8 +544,8 @@ def __init__( def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" - self._attr_available = not self.is_none - self._attr_native_value = self._value + if self.exists: + self._attr_native_value = self.entity_description.value_fn(self._value) class TeslemetryEnergyInfoSensorEntity(TeslemetryEnergyInfoEntity, SensorEntity): diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 48eb4aae8bcd19..29c9ef3bbb7d19 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -420,7 +420,10 @@ "name": "version" }, "vin": { - "name": "Vehicle" + "name": "Vehicle", + "state": { + "disconnected": "Disconnected" + } }, "vpp_backup_reserve_percent": { "name": "VPP backup reserve" From b471a6e519e7660e6fb9a6432cc516dbbd70f87f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 19 Sep 2024 11:35:44 +0200 Subject: [PATCH 13/16] Add has_entity_name to entity display dict and fix name (#125832) * Add has_entity_name to entity display dict and fix name * Fix tests --- homeassistant/helpers/entity_registry.py | 7 +++++-- tests/components/config/test_entity_registry.py | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 5d17c0c46b1225..6f4647030ddf36 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -235,8 +235,11 @@ def _as_display_dict(self) -> dict[str, Any] | None: display_dict["ec"] = ENTITY_CATEGORY_VALUE_TO_INDEX[category] if self.hidden_by is not None: display_dict["hb"] = True - if not self.name and self.has_entity_name: - display_dict["en"] = self.original_name + if self.has_entity_name: + display_dict["hn"] = True + name = self.name or self.original_name + if name is not None: + display_dict["en"] = name if self.domain == "sensor" and (sensor_options := self.options.get("sensor")): if (precision := sensor_options.get("display_precision")) is not None or ( precision := sensor_options.get("suggested_display_precision") diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 60657d4a77b9cd..bfbd69ec9bdf4c 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -245,6 +245,7 @@ async def test_list_entities_for_display( "ec": 1, "ei": "test_domain.test", "en": "Hello World", + "hn": True, "ic": "mdi:icon", "lb": [], "pl": "test_platform", @@ -254,7 +255,7 @@ async def test_list_entities_for_display( "ai": "area52", "di": "device123", "ei": "test_domain.nameless", - "en": None, + "hn": True, "lb": [], "pl": "test_platform", }, @@ -262,6 +263,8 @@ async def test_list_entities_for_display( "ai": "area52", "di": "device123", "ei": "test_domain.renamed", + "en": "User name", + "hn": True, "lb": [], "pl": "test_platform", }, @@ -326,6 +329,7 @@ class Unserializable: "ai": "area52", "di": "device123", "ei": "test_domain.test", + "hn": True, "lb": [], "en": "Hello World", "pl": "test_platform", From b2401bf2e307eaf325c2c1a92e2cee74bcf17efe Mon Sep 17 00:00:00 2001 From: Alberto Montes Date: Thu, 19 Sep 2024 11:38:25 +0200 Subject: [PATCH 14/16] Update string formatting to use f-string on components (#125987) * Update string formatting to use f-string on components * Update code given review feedback * Use f-string --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/buienradar/sensor.py | 2 +- homeassistant/components/buienradar/util.py | 2 +- .../components/emoncms_history/__init__.py | 6 ++--- homeassistant/components/graphite/__init__.py | 3 +-- homeassistant/components/home_connect/api.py | 2 +- homeassistant/components/kira/__init__.py | 2 +- .../components/limitlessled/light.py | 4 +-- homeassistant/components/mysensors/light.py | 6 +++-- homeassistant/components/netio/switch.py | 5 ++-- homeassistant/components/numato/__init__.py | 13 +++++----- homeassistant/components/recorder/executor.py | 2 +- .../components/recorder/migration.py | 26 +++++++------------ .../components/sense/binary_sensor.py | 2 +- homeassistant/components/sense/sensor.py | 2 +- .../seven_segments/image_processing.py | 2 +- .../components/shopping_list/intent.py | 6 ++--- .../components/signal_messenger/notify.py | 9 +++---- homeassistant/components/skybeacon/sensor.py | 2 +- homeassistant/components/snips/__init__.py | 2 +- .../components/starlingbank/sensor.py | 5 ++-- homeassistant/components/statsd/__init__.py | 2 +- homeassistant/components/stream/worker.py | 12 +++++---- homeassistant/components/supla/entity.py | 7 +++-- .../swiss_hydrological_data/sensor.py | 2 +- .../components/system_log/__init__.py | 4 +-- homeassistant/components/ted5000/sensor.py | 4 +-- .../components/tellduslive/sensor.py | 2 +- .../components/tensorflow/image_processing.py | 2 +- .../components/tomato/device_tracker.py | 3 ++- homeassistant/components/venstar/__init__.py | 3 ++- homeassistant/components/verisure/camera.py | 8 ++---- .../components/viaggiatreno/sensor.py | 2 +- .../components/yeelight/config_flow.py | 4 +-- homeassistant/components/zha/helpers.py | 18 ++++++------- homeassistant/components/zwave_me/light.py | 4 +-- 35 files changed, 81 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 69c762c1bc1925..c61d8e10b852a2 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -888,7 +888,7 @@ def _load_data(self, data): # noqa: C901 if sensor_type.startswith(PRECIPITATION_FORECAST): result = {ATTR_ATTRIBUTION: data.get(ATTRIBUTION)} if self._timeframe is not None: - result[TIMEFRAME_LABEL] = "%d min" % (self._timeframe) + result[TIMEFRAME_LABEL] = f"{self._timeframe} min" self._attr_extra_state_attributes = result diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index f089fce89b7405..a7267320de39c7 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -101,7 +101,7 @@ async def get_data(self, url): if resp.status == HTTPStatus.OK: result[SUCCESS] = True else: - result[MESSAGE] = "Got http statuscode: %d" % (resp.status) + result[MESSAGE] = f"Got http statuscode: {resp.status}" return result except (TimeoutError, aiohttp.ClientError) as err: diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 7de3a4f2ef8fb9..00af1fec6c6cad 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -86,15 +86,13 @@ def update_emoncms(time): continue if payload_dict: - payload = "{{{}}}".format( - ",".join(f"{key}:{val}" for key, val in payload_dict.items()) - ) + payload = ",".join(f"{key}:{val}" for key, val in payload_dict.items()) send_data( conf.get(CONF_URL), conf.get(CONF_API_KEY), str(conf.get(CONF_INPUTNODE)), - payload, + f"{{{payload}}}", ) track_point_in_time( diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index b0672e1f8536a4..336ca6ba2cb97e 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -138,8 +138,7 @@ def _report_attributes(self, entity_id, new_state): with suppress(ValueError): things["state"] = state.state_as_number(new_state) lines = [ - "%s.%s.%s %f %i" - % (self._prefix, entity_id, key.replace(" ", "_"), value, now) + f"{self._prefix}.{entity_id}.{key.replace(' ', '_')} {value:f} {now}" for key, value in things.items() if isinstance(value, (float, int)) ] diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index 10dc2d360fa2ac..33b1a462e43290 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -180,7 +180,7 @@ def get_program_sensors(self): ATTR_DEVICE: self, ATTR_DESC: k, ATTR_UNIT: unit, - ATTR_KEY: "BSH.Common.Option.{}".format(k.replace(" ", "")), + ATTR_KEY: f"BSH.Common.Option.{k.replace(' ', '')}", ATTR_ICON: icon, ATTR_DEVICE_CLASS: device_class, ATTR_SIGN: sign, diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index b41961f64ee61b..52618a125b6418 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -111,7 +111,7 @@ def load_module(platform, idx, module_conf): """Set up the KIRA module and load platform.""" # note: module_name is not the HA device name. it's just a unique name # to ensure the component and platform can share information - module_name = ("%s_%d" % (DOMAIN, idx)) if idx else DOMAIN + module_name = f"{DOMAIN}_{idx}" if idx else DOMAIN device_name = module_conf.get(CONF_NAME, DOMAIN) port = module_conf.get(CONF_PORT, DEFAULT_PORT) host = module_conf.get(CONF_HOST, DEFAULT_HOST) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 4456d112d0fea3..c6b3301081df61 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -119,13 +119,13 @@ def rewrite_legacy(config: ConfigType) -> ConfigType: else: _LOGGER.warning("Legacy configuration format detected") for i in range(1, 5): - name_key = "group_%d_name" % i + name_key = f"group_{i}_name" if name_key in bridge_conf: groups.append( { "number": i, "type": bridge_conf.get( - "group_%d_type" % i, DEFAULT_LED_TYPE + f"group_{i}_type", DEFAULT_LED_TYPE ), "name": bridge_conf.get(name_key), } diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index a76b42359c1cf1..87f60174caba90 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -173,7 +173,8 @@ def _turn_on_rgb(self, **kwargs: Any) -> None: new_rgb: tuple[int, int, int] | None = kwargs.get(ATTR_RGB_COLOR) if new_rgb is None: return - hex_color = "{:02x}{:02x}{:02x}".format(*new_rgb) + red, green, blue = new_rgb + hex_color = f"{red:02x}{green:02x}{blue:02x}" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, hex_color, ack=1 ) @@ -220,7 +221,8 @@ def _turn_on_rgbw(self, **kwargs: Any) -> None: new_rgbw: tuple[int, int, int, int] | None = kwargs.get(ATTR_RGBW_COLOR) if new_rgbw is None: return - hex_color = "{:02x}{:02x}{:02x}{:02x}".format(*new_rgbw) + red, green, blue, white = new_rgbw + hex_color = f"{red:02x}{green:02x}{blue:02x}{white:02x}" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, hex_color, ack=1 ) diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 54bfef5e1da8dd..5c2b93bcae77b2 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -109,7 +109,7 @@ def get(self, request, host): states, consumptions, cumulated_consumptions, start_dates = [], [], [], [] for i in range(1, 5): - out = "output%d" % i + out = f"output{i}" states.append(data.get(f"{out}_state") == STATE_ON) consumptions.append(float(data.get(f"{out}_consumption", 0))) cumulated_consumptions.append( @@ -168,7 +168,8 @@ def turn_off(self, **kwargs: Any) -> None: def _set(self, value): val = list("uuuu") val[int(self.outlet) - 1] = "1" if value else "0" - self.netio.get("port list {}".format("".join(val))) + val = "".join(val) + self.netio.get(f"port list {val}") self.netio.states[int(self.outlet) - 1] = value self.schedule_update_ha_state() diff --git a/homeassistant/components/numato/__init__.py b/homeassistant/components/numato/__init__.py index 28aa8623a7efe2..00122132d443dd 100644 --- a/homeassistant/components/numato/__init__.py +++ b/homeassistant/components/numato/__init__.py @@ -185,14 +185,13 @@ def check_port_free(self, device_id: int, port: int, direction: int) -> None: if (device_id, port) not in self.ports_registered: self.ports_registered[(device_id, port)] = direction else: + io = ( + "input" + if self.ports_registered[(device_id, port)] == gpio.IN + else "output" + ) raise gpio.NumatoGpioError( - "Device {} port {} already in use as {}.".format( - device_id, - port, - "input" - if self.ports_registered[(device_id, port)] == gpio.IN - else "output", - ) + f"Device {device_id} port {port} already in use as {io}." ) def check_device_id(self, device_id: int) -> None: diff --git a/homeassistant/components/recorder/executor.py b/homeassistant/components/recorder/executor.py index 8102c769ac1dfd..6b8192d1e146bd 100644 --- a/homeassistant/components/recorder/executor.py +++ b/homeassistant/components/recorder/executor.py @@ -55,7 +55,7 @@ def weakref_cb( # type: ignore[no-untyped-def] num_threads = len(self._threads) if num_threads < self._max_workers: - thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads) + thread_name = f"{self._thread_name_prefix or self}_{num_threads}" executor_thread = threading.Thread( name=thread_name, target=_worker_with_shutdown_hook, diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index df7ff5c4fedfab..9a27a44d706185 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -288,9 +288,11 @@ def _migrate_schema( "The database is about to upgrade from schema version %s to %s%s", current_version, end_version, - f". {MIGRATION_NOTE_OFFLINE}" - if current_version < LIVE_MIGRATION_MIN_SCHEMA_VERSION - else "", + ( + f". {MIGRATION_NOTE_OFFLINE}" + if current_version < LIVE_MIGRATION_MIN_SCHEMA_VERSION + else "" + ), ) schema_status = dataclass_replace(schema_status, current_version=end_version) @@ -475,11 +477,7 @@ def _add_columns( try: connection = session.connection() connection.execute( - text( - "ALTER TABLE {table} {columns_def}".format( - table=table_name, columns_def=", ".join(columns_def) - ) - ) + text(f"ALTER TABLE {table_name} {', '.join(columns_def)}") ) except (InternalError, OperationalError, ProgrammingError): # Some engines support adding all columns at once, @@ -530,10 +528,8 @@ def _modify_columns( if engine.dialect.name == SupportedDialect.POSTGRESQL: columns_def = [ - "ALTER {column} TYPE {type}".format( - **dict(zip(["column", "type"], col_def.split(" ", 1), strict=False)) - ) - for col_def in columns_def + f"ALTER {column} TYPE {type_}" + for column, type_ in (col_def.split(" ", 1) for col_def in columns_def) ] elif engine.dialect.name == "mssql": columns_def = [f"ALTER COLUMN {col_def}" for col_def in columns_def] @@ -544,11 +540,7 @@ def _modify_columns( try: connection = session.connection() connection.execute( - text( - "ALTER TABLE {table} {columns_def}".format( - table=table_name, columns_def=", ".join(columns_def) - ) - ) + text(f"ALTER TABLE {table_name} {', '.join(columns_def)}") ) except (InternalError, OperationalError): _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1") diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index 5640dd19961677..8317f8458b3449 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -56,7 +56,7 @@ async def _migrate_old_unique_ids(hass, devices): def sense_to_mdi(sense_icon): """Convert sense icon to mdi icon.""" - return "mdi:{}".format(MDI_ICONS.get(sense_icon, "power-plug")) + return f"mdi:{MDI_ICONS.get(sense_icon, "power-plug")}" class SenseDevice(BinarySensorEntity): diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 129b1262fd05fe..bc9dd470f5eed7 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -78,7 +78,7 @@ def __init__(self, name, sensor_type): def sense_to_mdi(sense_icon): """Convert sense icon to mdi icon.""" - return "mdi:{}".format(MDI_ICONS.get(sense_icon, "power-plug")) + return f"mdi:{MDI_ICONS.get(sense_icon, 'power-plug')}" async def async_setup_entry( diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 7b41a1702c063c..63fd27e0dd04c7 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -82,7 +82,7 @@ def __init__(self, hass, camera_entity, config, name): self.filepath = os.path.join( self.hass.config.config_dir, - "ssocr-{}.png".format(self._name.replace(" ", "_")), + f"ssocr-{self._name.replace(' ', '_')}.png", ) crop = [ "crop", diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py index d45085be5fa8e8..84ea3971293688 100644 --- a/homeassistant/components/shopping_list/intent.py +++ b/homeassistant/components/shopping_list/intent.py @@ -53,10 +53,8 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse if not items: response.async_set_speech("There are no items on your shopping list") else: + items_list = ", ".join(itm["name"] for itm in reversed(items)) response.async_set_speech( - "These are the top {} items on your shopping list: {}".format( - min(len(items), 5), - ", ".join(itm["name"] for itm in reversed(items)), - ) + f"These are the top {min(len(items), 5)} items on your shopping list: {items_list}" ) return response diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index 9321bc3232fcd0..53a255da5ff8c7 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -166,12 +166,11 @@ def get_attachments_as_bytes( and int(str(resp.headers.get("Content-Length"))) > attachment_size_limit ): + content_length = int(str(resp.headers.get("Content-Length"))) raise ValueError( # noqa: TRY301 - "Attachment too large (Content-Length reports {}). Max size: {}" - " bytes".format( - int(str(resp.headers.get("Content-Length"))), - CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES, - ) + "Attachment too large (Content-Length reports " + f"{content_length}). Max size: " + f"{CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES} bytes" ) size = 0 diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 5fa62d06fc2183..6cb5064b40e654 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -184,7 +184,7 @@ def _update(self, handle, value): value[2], value[1], ) - self.data["temp"] = float("%d.%d" % (value[0], value[2])) + self.data["temp"] = float(f"{value[0]}.{value[2]}") self.data["humid"] = value[1] def terminate(self): diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 4731a0f324a307..70837b95ec535a 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -140,7 +140,7 @@ async def message_received(msg): slots = {} for slot in request.get("slots", []): slots[slot["slotName"]] = {"value": resolve_slot_values(slot)} - slots["{}_raw".format(slot["slotName"])] = {"value": slot["rawValue"]} + slots[f"{slot['slotName']}_raw"] = {"value": slot["rawValue"]} slots["site_id"] = {"value": request.get("siteId")} slots["session_id"] = {"value": request.get("sessionId")} slots["confidenceScore"] = {"value": request["intent"]["confidenceScore"]} diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index fd351416c288b1..282323d8b7bd50 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -92,9 +92,8 @@ def __init__(self, starling_account, account_name, balance_data_type): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format( - self._account_name, self._balance_data_type.replace("_", " ").capitalize() - ) + balance_data_type = self._balance_data_type.replace("_", " ").capitalize() + return f"{self._account_name} {balance_data_type}" @property def native_value(self): diff --git a/homeassistant/components/statsd/__init__.py b/homeassistant/components/statsd/__init__.py index efe1c8180257c9..50b74b20028734 100644 --- a/homeassistant/components/statsd/__init__.py +++ b/homeassistant/components/statsd/__init__.py @@ -80,7 +80,7 @@ def statsd_event_listener(event): # Send attribute values for key, value in states.items(): if isinstance(value, (float, int)): - stat = "{}.{}".format(state.entity_id, key.replace(" ", "_")) + stat = f"{state.entity_id}.{key.replace(' ', '_')}" statsd_client.gauge(stat, value, sample_rate) elif isinstance(_state, (float, int)): diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 354cc476186367..0d72a9b081871c 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -367,12 +367,14 @@ def flush(self, packet: av.Packet, last_part: bool) -> None: data=self._memory_file.read(), ), ( - segment_duration := float( - (adjusted_dts - self._segment_start_dts) * packet.time_base + ( + segment_duration := float( + (adjusted_dts - self._segment_start_dts) * packet.time_base + ) ) - ) - if last_part - else 0, + if last_part + else 0 + ), ) if last_part: # If we've written the last part, we can close the memory_file. diff --git a/homeassistant/components/supla/entity.py b/homeassistant/components/supla/entity.py index fa257e39a06cc5..446d67d19d64e9 100644 --- a/homeassistant/components/supla/entity.py +++ b/homeassistant/components/supla/entity.py @@ -27,10 +27,9 @@ def channel_data(self): @property def unique_id(self) -> str: """Return a unique ID.""" - return "supla-{}-{}".format( - self.channel_data["iodevice"]["gUIDString"].lower(), - self.channel_data["channelNumber"], - ) + uid = self.channel_data["iodevice"]["gUIDString"].lower() + channel_number = self.channel_data["channelNumber"] + return f"supla-{uid}-{channel_number}" @property def name(self) -> str | None: diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index c67045521b5cf7..3d88182eaa471e 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -103,7 +103,7 @@ def __init__(self, hydro_data, station, condition): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._data["water-body-name"], self._condition) + return f"{self._data['water-body-name']} {self._condition}" @property def unique_id(self) -> str: diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 0749f87a67fde8..22950aa9f1ecf1 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -299,9 +299,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass_path: str = HOMEASSISTANT_PATH[0] config_dir = hass.config.config_dir - paths_re = re.compile( - r"(?:{})/(.*)".format("|".join([re.escape(x) for x in (hass_path, config_dir)])) - ) + paths_re = re.compile(rf"(?:{re.escape(hass_path)}|{re.escape(config_dir)})/(.*)") handler = LogErrorHandler( hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT], paths_re ) diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 68f4520a7e31c4..26f469349b463c 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -136,8 +136,8 @@ def update(self) -> None: mtus = int(doc["LiveData"]["System"]["NumberMTU"]) for mtu in range(1, mtus + 1): - power = int(doc["LiveData"]["Power"]["MTU%d" % mtu]["PowerNow"]) - voltage = int(doc["LiveData"]["Voltage"]["MTU%d" % mtu]["VoltageNow"]) + power = int(doc["LiveData"]["Power"][f"MTU{mtu}"]["PowerNow"]) + voltage = int(doc["LiveData"]["Voltage"][f"MTU{mtu}"]["VoltageNow"]) self.data[mtu] = { UnitOfPower.WATT: power, diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 70c83bb0038962..e588ea6318fa99 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -194,4 +194,4 @@ def native_value(self): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}-{}-{}".format(*self._id) + return "-".join(self._id) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index cf8e293161a438..f4a3a7bfe07e62 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -324,7 +324,7 @@ def _save_image(self, image, matches, paths): # Draw detected objects for instance in values: - label = "{} {:.1f}%".format(category, instance["score"]) + label = f"{category} {instance['score']:.1f}%" draw_box( draw, instance["box"], img_width, img_height, label, (255, 255, 0) ) diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index b705363944fd57..dfa8d2bd4e10d4 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -61,9 +61,10 @@ def __init__(self, config): if port is None: port = 443 if self.ssl else 80 + protocol = "https" if self.ssl else "http" self.req = requests.Request( "POST", - "http{}://{}:{}/update.cgi".format("s" if self.ssl else "", host, port), + f"{protocol}://{host}:{port}/update.cgi", data={"_http_id": http_id, "exec": "devlist"}, auth=requests.auth.HTTPBasicAuth(username, password), ).prepare() diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index cbcfd3dff90d05..563a974fad6fcd 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -84,10 +84,11 @@ def _handle_coordinator_update(self) -> None: @property def device_info(self) -> DeviceInfo: """Return the device information for this entity.""" + fw_ver_major, fw_ver_minor = self._client.get_firmware_ver() return DeviceInfo( identifiers={(DOMAIN, self._config.entry_id)}, name=self._client.name, manufacturer="Venstar", model=f"{self._client.model}-{self._client.get_type()}", - sw_version="{}.{}".format(*(self._client.get_firmware_ver())), + sw_version=f"{fw_ver_major}.{fw_ver_minor}", ) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 50606a49eab1a2..70cd436d24cf1c 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -110,9 +110,7 @@ def check_imagelist(self) -> None: return LOGGER.debug("Download new image %s", new_image_id) - new_image_path = os.path.join( - self._directory_path, "{}{}".format(new_image_id, ".jpg") - ) + new_image_path = os.path.join(self._directory_path, f"{new_image_id}.jpg") new_image_url = new_image["contentUrl"] self.coordinator.verisure.download_image(new_image_url, new_image_path) LOGGER.debug("Old image_id=%s", self._image_id) @@ -123,9 +121,7 @@ def check_imagelist(self) -> None: def delete_image(self, _=None) -> None: """Delete an old image.""" - remove_image = os.path.join( - self._directory_path, "{}{}".format(self._image_id, ".jpg") - ) + remove_image = os.path.join(self._directory_path, f"{self._image_id}.jpg") try: os.remove(remove_image) LOGGER.debug("Deleting old image %s", remove_image) diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 1ea12ed6a41897..cb652270c69320 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -174,7 +174,7 @@ async def async_update(self) -> None: self._state = NO_INFORMATION_STRING self._unit = "" else: - self._state = "Error: {}".format(res["error"]) + self._state = f"Error: {res['error']}" self._unit = "" else: for i in MONITORED_INFO: diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index b22774c68c3a59..cafed622300565 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -85,9 +85,7 @@ async def async_step_zeroconf( ) -> ConfigFlowResult: """Handle discovery from zeroconf.""" self._discovered_ip = discovery_info.host - await self.async_set_unique_id( - "{0:#0{1}x}".format(int(discovery_info.name[-26:-18]), 18) - ) + await self.async_set_unique_id(f"{int(discovery_info.name[-26:-18]):#018x}") return await self._async_handle_discovery_with_unique_id() async def async_step_ssdp( diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 4ca2f5d172bf0b..dc999f13693573 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -617,9 +617,11 @@ def handle_raw_device_initialized(self, event: RawDeviceInitializedEvent) -> Non ATTR_NWK: str(event.device_info.nwk), ATTR_IEEE: str(event.device_info.ieee), DEVICE_PAIRING_STATUS: event.device_info.pairing_status.name, - ATTR_MODEL: event.device_info.model - if event.device_info.model - else UNKNOWN_MODEL, + ATTR_MODEL: ( + event.device_info.model + if event.device_info.model + else UNKNOWN_MODEL + ), ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER, ATTR_SIGNATURE: event.device_info.signature, }, @@ -922,9 +924,7 @@ def __init__(self, hass: HomeAssistant, gateway: ZHAGatewayProxy) -> None: hass_path: str = HOMEASSISTANT_PATH[0] config_dir = self.hass.config.config_dir self.paths_re = re.compile( - r"(?:{})/(.*)".format( - "|".join([re.escape(x) for x in (hass_path, config_dir)]) - ) + rf"(?:{re.escape(hass_path)}|{re.escape(config_dir)})/(.*)" ) def emit(self, record: LogRecord) -> None: @@ -1025,9 +1025,9 @@ def cluster_command_schema_to_vol_schema(schema: CommandSchema) -> vol.Schema: """Convert a cluster command schema to a voluptuous schema.""" return vol.Schema( { - vol.Optional(field.name) - if field.optional - else vol.Required(field.name): schema_type_to_vol(field.type) + ( + vol.Optional(field.name) if field.optional else vol.Required(field.name) + ): schema_type_to_vol(field.type) for field in schema.fields } ) diff --git a/homeassistant/components/zwave_me/light.py b/homeassistant/components/zwave_me/light.py index f111c04e928b63..ef3eca5d38971b 100644 --- a/homeassistant/components/zwave_me/light.py +++ b/homeassistant/components/zwave_me/light.py @@ -85,8 +85,8 @@ def turn_on(self, **kwargs: Any) -> None: self.device.id, f"exact?level={round(brightness / 2.55)}" ) return - cmd = "exact?red={}&green={}&blue={}" - cmd = cmd.format(*color) if any(color) else cmd.format(*(255, 255, 255)) + red, green, blue = color if any(color) else (255, 255, 255) + cmd = f"exact?red={red}&green={green}&blue={blue}" self.controller.zwave_api.send_command(self.device.id, cmd) @property From c81d10482200113251e332e2ada4eb277d36f733 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:12:37 +0200 Subject: [PATCH 15/16] Sort values in Platform enum (#126259) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index acbef5c58cc269..aaffcc9aa84186 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -75,9 +75,9 @@ class Platform(StrEnum): TIME = "time" TODO = "todo" TTS = "tts" + UPDATE = "update" VACUUM = "vacuum" VALVE = "valve" - UPDATE = "update" WAKE_WORD = "wake_word" WATER_HEATER = "water_heater" WEATHER = "weather" From 5864591150434cf8ace221cf3141a1797a031625 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:28:09 +0200 Subject: [PATCH 16/16] Mark tag as entity component in pylint plugin (#126183) * Move tag base entity to separate module * Add tag to _ENTITY_COMPONENTS * Move Entity back in * Add tag to base platforms * Adjust core_files * Revert "Adjust core_files" This reverts commit 180c5034de5c4e80afeeb8149c6fa22395b215a4. * Revert "Add tag to base platforms" This reverts commit 381bcf12f0b52a5df665086862e715bbc7e90b79. --- homeassistant/components/tag/__init__.py | 2 +- pylint/plugins/hass_enforce_class_module.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 160408732c9530..0462c5bec3488e 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -360,7 +360,7 @@ async def async_scan_tag( _LOGGER.debug("Tag: %s scanned by device: %s", tag_id, device_id) -class TagEntity(Entity): # pylint: disable=hass-enforce-class-module +class TagEntity(Entity): """Representation of a Tag entity.""" _unrecorded_attributes = frozenset({TAG_ID}) diff --git a/pylint/plugins/hass_enforce_class_module.py b/pylint/plugins/hass_enforce_class_module.py index 0fce0e13f630f6..c0b363bbddf2a3 100644 --- a/pylint/plugins/hass_enforce_class_module.py +++ b/pylint/plugins/hass_enforce_class_module.py @@ -65,7 +65,8 @@ "WeatherEntityDescription", }, } -_PLATFORMS: set[str] = {platform.value for platform in Platform} +_ENTITY_COMPONENTS: set[str] = {platform.value for platform in Platform} +_ENTITY_COMPONENTS.add("tag") class HassEnforceClassModule(BaseChecker): @@ -92,7 +93,7 @@ def visit_classdef(self, node: nodes.ClassDef) -> None: current_integration = parts[2] current_module = parts[3] if len(parts) > 3 else "" - if current_module != "entity" and current_integration not in _PLATFORMS: + if current_module != "entity" and current_integration not in _ENTITY_COMPONENTS: top_level_ancestors = list(node.ancestors(recurs=False)) for ancestor in top_level_ancestors: