diff --git a/homeassistant/components/google_cloud/helpers.py b/homeassistant/components/google_cloud/helpers.py index f71ccea00ccaf9..952a10482e7d8c 100644 --- a/homeassistant/components/google_cloud/helpers.py +++ b/homeassistant/components/google_cloud/helpers.py @@ -48,6 +48,8 @@ async def async_tts_voices( list_voices_response = await client.list_voices() for voice in list_voices_response.voices: language_code = voice.language_codes[0] + if not voice.name.startswith(language_code): + continue if language_code not in voices: voices[language_code] = [] voices[language_code].append(voice.name) diff --git a/homeassistant/components/vesync/quality_scale.yaml b/homeassistant/components/vesync/quality_scale.yaml index e747a7e2ddef15..b56c9ccde237e4 100644 --- a/homeassistant/components/vesync/quality_scale.yaml +++ b/homeassistant/components/vesync/quality_scale.yaml @@ -5,9 +5,7 @@ rules: brands: done common-modules: done config-flow-test-coverage: done - config-flow: - status: todo - comment: Missing data descriptions + config-flow: done dependency-transparency: done docs-actions: done docs-high-level-description: done diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index 938a84a73d07be..c6d38456e9e9ac 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -15,6 +15,10 @@ "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::email%]" }, + "data_description": { + "password": "[%key:component::vesync::config::step::user::data_description::password%]", + "username": "[%key:component::vesync::config::step::user::data_description::username%]" + }, "description": "The VeSync integration needs to re-authenticate your account", "title": "[%key:common::config_flow::title::reauth%]" }, @@ -23,6 +27,11 @@ "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::email%]" }, + "data_description": { + "password": "Password associated with your VeSync account", + "username": "Email address associated with your VeSync account" + }, + "description": "Enter the account used in the vesync app. 2FA is not supported and must be disabled.", "title": "Enter username and password" } } @@ -106,6 +115,9 @@ } }, "switch": { + "auto_off_config": { + "name": "Auto Off" + }, "child_lock": { "name": "Child lock" }, diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index baea16838c00f0..938e5abd9e44fa 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -64,6 +64,16 @@ class VeSyncSwitchEntityDescription(SwitchEntityDescription): on_fn=lambda device: device.toggle_child_lock(True), off_fn=lambda device: device.toggle_child_lock(False), ), + VeSyncSwitchEntityDescription( + key="auto_off_config", + is_on=lambda device: device.state.automatic_stop_config, + exists_fn=( + lambda device: rgetattr(device, "state.automatic_stop_config") is not None + ), + translation_key="auto_off_config", + on_fn=lambda device: device.toggle_automatic_stop(True), + off_fn=lambda device: device.toggle_automatic_stop(False), + ), ) diff --git a/tests/components/google_cloud/conftest.py b/tests/components/google_cloud/conftest.py index 897c352b402d83..7e604910f6dbeb 100644 --- a/tests/components/google_cloud/conftest.py +++ b/tests/components/google_cloud/conftest.py @@ -81,6 +81,7 @@ def mock_api_tts() -> AsyncMock: voices=[ cloud_tts.Voice(language_codes=["en-US"], name="en-US-Standard-A"), cloud_tts.Voice(language_codes=["en-US"], name="en-US-Standard-B"), + cloud_tts.Voice(language_codes=["en-US"], name="Standard-A"), cloud_tts.Voice(language_codes=["el-GR"], name="el-GR-Standard-A"), ] ) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index e4803140029140..70f7ab0ad7a6c2 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -175,6 +175,20 @@ async def close(self) -> None: """Close the device.""" +def set_trait_attributes( + trait: AsyncMock, + dataclass_template: RoborockBase, + init_none: bool = False, +) -> None: + """Set attributes on a mock roborock trait.""" + template_copy = deepcopy(dataclass_template) + for attr_name in dir(template_copy): + if attr_name.startswith("_"): + continue + attr_value = getattr(template_copy, attr_name) if not init_none else None + setattr(trait, attr_name, attr_value) + + def make_mock_trait( trait_spec: type[V1TraitMixin] | None = None, dataclass_template: RoborockBase | None = None, @@ -183,12 +197,14 @@ def make_mock_trait( trait = AsyncMock(spec=trait_spec or V1TraitMixin) if dataclass_template is not None: # Copy all attributes and property methods (e.g. computed properties) - template_copy = deepcopy(dataclass_template) - for attr_name in dir(template_copy): - if attr_name.startswith("_"): - continue - setattr(trait, attr_name, getattr(template_copy, attr_name)) - trait.refresh = AsyncMock() + # on the first call to refresh(). The object starts uninitialized. + set_trait_attributes(trait, dataclass_template, init_none=True) + + async def refresh() -> None: + if dataclass_template is not None: + set_trait_attributes(trait, dataclass_template) + + trait.refresh = AsyncMock(side_effect=refresh) return trait diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index 0f331402aad8a3..a3a3b0875bbff7 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -31,7 +31,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import FakeDevice +from .conftest import FakeDevice, set_trait_attributes +from .mock_data import STATUS from tests.common import MockConfigEntry @@ -134,8 +135,14 @@ async def test_resume_cleaning( vacuum_command: Mock, ) -> None: """Test resuming clean on start button when a clean is paused.""" - fake_vacuum.v1_properties.status.in_cleaning = in_cleaning_int - fake_vacuum.v1_properties.status.in_returning = in_returning_int + + async def refresh_properties() -> None: + set_trait_attributes(fake_vacuum.v1_properties.status, STATUS) + fake_vacuum.v1_properties.status.in_cleaning = in_cleaning_int + fake_vacuum.v1_properties.status.in_returning = in_returning_int + + fake_vacuum.v1_properties.status.refresh.side_effect = refresh_properties + await async_setup_component(hass, DOMAIN, {}) vacuum = hass.states.get(ENTITY_ID) assert vacuum diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index 6ef7376623b7b7..290bb0dcdb183a 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -341,6 +341,30 @@ }), 'unit_of_measurement': 'μg/m³', }), + dict({ + 'device_class': None, + 'disabled': False, + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.test_fan_auto_off', + 'icon': None, + 'name': None, + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto Off', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'Test Fan Auto Off', + }), + 'entity_id': 'switch.test_fan_auto_off', + 'last_changed': str, + 'last_reported': str, + 'last_updated': str, + 'state': 'unavailable', + }), + 'unit_of_measurement': None, + }), dict({ 'device_class': None, 'disabled': False, diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 3a577d1486de68..83827858db08b0 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -611,8 +611,54 @@ 'unique_id': '200s-humidifier4321-display', 'unit_of_measurement': None, }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.humidifier_200s_auto_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto Off', + 'platform': 'vesync', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_config', + 'unique_id': '200s-humidifier4321-auto_off_config', + 'unit_of_measurement': None, + }), ]) # --- +# name: test_switch_state[Humidifier 200s][switch.humidifier_200s_auto_off] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Humidifier 200s Auto Off', + }), + 'context': , + 'entity_id': 'switch.humidifier_200s_auto_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch_state[Humidifier 200s][switch.humidifier_200s_display] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -727,8 +773,54 @@ 'unique_id': '6000s-child_lock', 'unit_of_measurement': None, }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.humidifier_6000s_auto_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto Off', + 'platform': 'vesync', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_config', + 'unique_id': '6000s-auto_off_config', + 'unit_of_measurement': None, + }), ]) # --- +# name: test_switch_state[Humidifier 6000s][switch.humidifier_6000s_auto_off] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Humidifier 6000s Auto Off', + }), + 'context': , + 'entity_id': 'switch.humidifier_6000s_auto_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch_state[Humidifier 6000s][switch.humidifier_6000s_child_lock] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -823,8 +915,54 @@ 'unique_id': '600s-humidifier-display', 'unit_of_measurement': None, }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.humidifier_600s_auto_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto Off', + 'platform': 'vesync', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_config', + 'unique_id': '600s-humidifier-auto_off_config', + 'unit_of_measurement': None, + }), ]) # --- +# name: test_switch_state[Humidifier 600S][switch.humidifier_600s_auto_off] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Humidifier 600S Auto Off', + }), + 'context': , + 'entity_id': 'switch.humidifier_600s_auto_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch_state[Humidifier 600S][switch.humidifier_600s_display] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/vesync/test_init.py b/tests/components/vesync/test_init.py index 33884231ff3bed..5886f19dd2b8e5 100644 --- a/tests/components/vesync/test_init.py +++ b/tests/components/vesync/test_init.py @@ -144,7 +144,7 @@ async def test_migrate_config_entry( switch_entities = [ e for e in entity_registry.entities.values() if e.domain == "switch" ] - assert len(switch_entities) == 2 + assert len(switch_entities) == 3 humidifier_entities = [ e for e in entity_registry.entities.values() if e.domain == "humidifier"