From ed726db97408616eb1105f8cb8e8820dabab8b6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Feb 2024 12:34:23 -0600 Subject: [PATCH 001/125] Fix race in loading service descriptions (#109316) --- homeassistant/helpers/service.py | 5 ++ tests/helpers/test_service.py | 79 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 5a9786eb0fa..30516e3a099 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -608,6 +608,11 @@ async def async_get_all_descriptions( # Files we loaded for missing descriptions loaded: dict[str, JSON_TYPE] = {} + # We try to avoid making a copy in the event the cache is good, + # but now we must make a copy in case new services get added + # while we are loading the missing ones so we do not + # add the new ones to the cache without their descriptions + services = {domain: service.copy() for domain, service in services.items()} if domains_with_missing_services: ints_or_excs = await async_get_integrations(hass, domains_with_missing_services) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 07e68e081b3..90f9b65aaba 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1,4 +1,5 @@ """Test service helpers.""" +import asyncio from collections.abc import Iterable from copy import deepcopy from typing import Any @@ -782,6 +783,84 @@ async def test_async_get_all_descriptions_dynamically_created_services( } +async def test_async_get_all_descriptions_new_service_added_while_loading( + hass: HomeAssistant, +) -> None: + """Test async_get_all_descriptions when a new service is added while loading translations.""" + group = hass.components.group + group_config = {group.DOMAIN: {}} + await async_setup_component(hass, group.DOMAIN, group_config) + descriptions = await service.async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert "description" in descriptions["group"]["reload"] + assert "fields" in descriptions["group"]["reload"] + + logger = hass.components.logger + logger_domain = logger.DOMAIN + logger_config = {logger_domain: {}} + + translations_called = asyncio.Event() + translations_wait = asyncio.Event() + + async def async_get_translations( + hass: HomeAssistant, + language: str, + category: str, + integrations: Iterable[str] | None = None, + config_flow: bool | None = None, + ) -> dict[str, Any]: + """Return all backend translations.""" + translations_called.set() + await translations_wait.wait() + translation_key_prefix = f"component.{logger_domain}.services.set_default_level" + return { + f"{translation_key_prefix}.name": "Translated name", + f"{translation_key_prefix}.description": "Translated description", + f"{translation_key_prefix}.fields.level.name": "Field name", + f"{translation_key_prefix}.fields.level.description": "Field description", + f"{translation_key_prefix}.fields.level.example": "Field example", + } + + with patch( + "homeassistant.helpers.service.translation.async_get_translations", + side_effect=async_get_translations, + ): + await async_setup_component(hass, logger_domain, logger_config) + task = asyncio.create_task(service.async_get_all_descriptions(hass)) + await translations_called.wait() + # Now register a new service while translations are being loaded + hass.services.async_register(logger_domain, "new_service", lambda x: None, None) + service.async_set_service_schema( + hass, logger_domain, "new_service", {"description": "new service"} + ) + translations_wait.set() + descriptions = await task + + # Two domains should be present + assert len(descriptions) == 2 + + logger_descriptions = descriptions[logger_domain] + + # The new service was loaded after the translations were loaded + # so it should not appear until the next time we fetch + assert "new_service" not in logger_descriptions + + set_default_level = logger_descriptions["set_default_level"] + + assert set_default_level["name"] == "Translated name" + assert set_default_level["description"] == "Translated description" + set_default_level_fields = set_default_level["fields"] + assert set_default_level_fields["level"]["name"] == "Field name" + assert set_default_level_fields["level"]["description"] == "Field description" + assert set_default_level_fields["level"]["example"] == "Field example" + + descriptions = await service.async_get_all_descriptions(hass) + assert "description" in descriptions[logger_domain]["new_service"] + assert descriptions[logger_domain]["new_service"]["description"] == "new service" + + async def test_register_with_mixed_case(hass: HomeAssistant) -> None: """Test registering a service with mixed case. From c2c98bd04c8c3cfc8ae948ec5c2a2be3ce117e26 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Feb 2024 13:40:29 -0600 Subject: [PATCH 002/125] Move default response out of sentence trigger registration and into agent (#109317) * Move default response out of trigger and into agent * Add test --- .../components/conversation/default_agent.py | 7 ++- .../components/conversation/trigger.py | 7 ++- tests/components/conversation/test_trigger.py | 58 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c9119935213..123dc7aba1d 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -238,7 +238,10 @@ class DefaultAgent(AbstractConversationAgent): ) ) - # Use last non-empty result as response + # Use last non-empty result as response. + # + # There may be multiple copies of a trigger running when editing in + # the UI, so it's critical that we filter out empty responses here. response_text: str | None = None for trigger_response in trigger_responses: response_text = response_text or trigger_response @@ -246,7 +249,7 @@ class DefaultAgent(AbstractConversationAgent): # Convert to conversation result response = intent.IntentResponse(language=language) response.response_type = intent.IntentResponseType.ACTION_DONE - response.async_set_speech(response_text or "") + response.async_set_speech(response_text or "Done") return ConversationResult(response=response) diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index d38bb69f3e1..4600135c1e5 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -98,7 +98,12 @@ async def async_attach_trigger( # mypy does not understand the type narrowing, unclear why return automation_result.conversation_response # type: ignore[return-value] - return "Done" + # It's important to return None here instead of a string. + # + # When editing in the UI, a copy of this trigger is registered. + # If we return a string from here, there is a race condition between the + # two trigger copies for who will provide a response. + return None default_agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) assert isinstance(default_agent, DefaultAgent) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index e40c7554fdd..26626a04079 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -7,6 +7,7 @@ from homeassistant.helpers import trigger from homeassistant.setup import async_setup_component from tests.common import async_mock_service +from tests.typing import WebSocketGenerator @pytest.fixture @@ -99,6 +100,63 @@ async def test_response(hass: HomeAssistant, setup_comp) -> None: assert service_response["response"]["speech"]["plain"]["speech"] == response +async def test_subscribe_trigger_does_not_interfere_with_responses( + hass: HomeAssistant, setup_comp, hass_ws_client: WebSocketGenerator +) -> None: + """Test that subscribing to a trigger from the websocket API does not interfere with responses.""" + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 5, + "type": "subscribe_trigger", + "trigger": {"platform": "conversation", "command": ["test sentence"]}, + } + ) + + service_response = await hass.services.async_call( + "conversation", + "process", + { + "text": "test sentence", + }, + blocking=True, + return_response=True, + ) + + # Default response, since no automations with responses are registered + assert service_response["response"]["speech"]["plain"]["speech"] == "Done" + + # Now register a trigger with a response + assert await async_setup_component( + hass, + "automation", + { + "automation test1": { + "trigger": { + "platform": "conversation", + "command": ["test sentence"], + }, + "action": { + "set_conversation_response": "test response", + }, + } + }, + ) + + service_response = await hass.services.async_call( + "conversation", + "process", + { + "text": "test sentence", + }, + blocking=True, + return_response=True, + ) + + # Response will now come through + assert service_response["response"]["speech"]["plain"]["speech"] == "test response" + + async def test_same_trigger_multiple_sentences( hass: HomeAssistant, calls, setup_comp ) -> None: From 0cc8b2edf90a86862ebfe7824597d8b15cd211b5 Mon Sep 17 00:00:00 2001 From: Josh Pettersen <12600312+bubonicbob@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:01:05 -0800 Subject: [PATCH 003/125] Remove battery charge sensor from powerwall (#109271) --- homeassistant/components/powerwall/sensor.py | 16 ---------------- tests/components/powerwall/test_sensor.py | 2 -- 2 files changed, 18 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 24aeb9e4f4e..9e17cd32e9c 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -113,12 +113,6 @@ POWERWALL_INSTANT_SENSORS = ( ) -def _get_battery_charge(battery_data: BatteryResponse) -> float: - """Get the current value in %.""" - ratio = float(battery_data.energy_remaining) / float(battery_data.capacity) - return round(100 * ratio, 1) - - BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ PowerwallSensorEntityDescription[BatteryResponse, int]( key="battery_capacity", @@ -202,16 +196,6 @@ BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ suggested_display_precision=1, value_fn=lambda battery_data: battery_data.energy_remaining, ), - PowerwallSensorEntityDescription[BatteryResponse, float]( - key="charge", - translation_key="charge", - entity_category=EntityCategory.DIAGNOSTIC, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.BATTERY, - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=_get_battery_charge, - ), PowerwallSensorEntityDescription[BatteryResponse, str]( key="grid_state", translation_key="grid_state", diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 11b4f25e4a3..2de79a6a6dc 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -157,7 +157,6 @@ async def test_sensors( float(hass.states.get("sensor.mysite_tg0123456789ab_battery_remaining").state) == 14.715 ) - assert float(hass.states.get("sensor.mysite_tg0123456789ab_charge").state) == 100.0 assert ( str(hass.states.get("sensor.mysite_tg0123456789ab_grid_state").state) == "grid_compliant" @@ -187,7 +186,6 @@ async def test_sensors( float(hass.states.get("sensor.mysite_tg9876543210ba_battery_remaining").state) == 15.137 ) - assert float(hass.states.get("sensor.mysite_tg9876543210ba_charge").state) == 100.0 assert ( str(hass.states.get("sensor.mysite_tg9876543210ba_grid_state").state) == "grid_compliant" From c2525d53dd26ba5081c6287a1479e512b77ba886 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:07:47 -0500 Subject: [PATCH 004/125] Add translations for zwave_js entities and services (#109188) --- homeassistant/components/zwave_js/button.py | 2 +- homeassistant/components/zwave_js/icons.json | 53 +- homeassistant/components/zwave_js/sensor.py | 70 +- .../components/zwave_js/strings.json | 791 ++++++++++-------- 4 files changed, 513 insertions(+), 403 deletions(-) diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index 33d1e6dfa63..876cf60b4cb 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -86,13 +86,13 @@ class ZWaveNodePingButton(ButtonEntity): _attr_should_poll = False _attr_entity_category = EntityCategory.CONFIG _attr_has_entity_name = True + _attr_translation_key = "ping" def __init__(self, driver: Driver, node: ZwaveNode) -> None: """Initialize a ping Z-Wave device button entity.""" self.node = node # Entity class attributes - self._attr_name = "Ping" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.ping" # device may not be precreated in main handler yet diff --git a/homeassistant/components/zwave_js/icons.json b/homeassistant/components/zwave_js/icons.json index 2280811d3da..2956cf2c6e0 100644 --- a/homeassistant/components/zwave_js/icons.json +++ b/homeassistant/components/zwave_js/icons.json @@ -1,14 +1,34 @@ { "entity": { + "button": { + "ping": { + "default": "mdi:crosshairs-gps" + } + }, "sensor": { + "can": { + "default": "mdi:car-brake-alert" + }, + "commands_dropped": { + "default": "mdi:trash-can" + }, "controller_status": { "default": "mdi:help-rhombus", "state": { + "jammed": "mdi:lock", "ready": "mdi:check", - "unresponsive": "mdi:bell-off", - "jammed": "mdi:lock" + "unresponsive": "mdi:bell-off" } }, + "last_seen": { + "default": "mdi:timer-sync" + }, + "messages_dropped": { + "default": "mdi:trash-can" + }, + "nak": { + "default": "mdi:hand-back-left-off" + }, "node_status": { "default": "mdi:help-rhombus", "state": { @@ -18,7 +38,36 @@ "dead": "mdi:robot-dead", "unknown": "mdi:help-rhombus" } + }, + "successful_commands": { + "default": "mdi:check" + }, + "successful_messages": { + "default": "mdi:check" + }, + "timeout_ack": { + "default": "mdi:ear-hearing-off" + }, + "timeout_callback": { + "default": "mdi:timer-sand-empty" + }, + "timeout_response": { + "default": "mdi:timer-sand-empty" } } + }, + "services": { + "bulk_set_partial_config_parameters": "mdi:cogs", + "clear_lock_usercode": "mdi:eraser", + "invoke_cc_api": "mdi:api", + "multicast_set_value": "mdi:list-box", + "ping": "mdi:crosshairs-gps", + "refresh_notifications": "mdi:bell", + "refresh_value": "mdi:refresh", + "reset_meter": "mdi:meter-electric", + "set_config_parameter": "mdi:cog", + "set_lock_configuration": "mdi:shield-lock", + "set_lock_usercode": "mdi:lock-smart", + "set_value": "mdi:form-textbox" } } diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 0b9defc5f62..0240725ca2d 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -350,55 +350,61 @@ class ZWaveJSStatisticsSensorEntityDescription(SensorEntityDescription): ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ZWaveJSStatisticsSensorEntityDescription( key="messagesTX", - name="Successful messages (TX)", + translation_key="successful_messages", + translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messagesRX", - name="Successful messages (RX)", + translation_key="successful_messages", + translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messagesDroppedTX", - name="Messages dropped (TX)", + translation_key="messages_dropped", + translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messagesDroppedRX", - name="Messages dropped (RX)", + translation_key="messages_dropped", + translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( - key="NAK", - name="Messages not accepted", + key="NAK", translation_key="nak", state_class=SensorStateClass.TOTAL + ), + ZWaveJSStatisticsSensorEntityDescription( + key="CAN", translation_key="can", state_class=SensorStateClass.TOTAL + ), + ZWaveJSStatisticsSensorEntityDescription( + key="timeoutACK", + translation_key="timeout_ack", state_class=SensorStateClass.TOTAL, ), - ZWaveJSStatisticsSensorEntityDescription( - key="CAN", name="Collisions", state_class=SensorStateClass.TOTAL - ), - ZWaveJSStatisticsSensorEntityDescription( - key="timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL - ), ZWaveJSStatisticsSensorEntityDescription( key="timeoutResponse", - name="Timed out responses", + translation_key="timeout_response", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="timeoutCallback", - name="Timed out callbacks", + translation_key="timeout_callback", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel0.average", - name="Average background RSSI (channel 0)", + translation_key="average_background_rssi", + translation_placeholders={"channel": "0"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_dict_of_dicts, ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel0.current", - name="Current background RSSI (channel 0)", + translation_key="current_background_rssi", + translation_placeholders={"channel": "0"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -406,14 +412,16 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel1.average", - name="Average background RSSI (channel 1)", + translation_key="average_background_rssi", + translation_placeholders={"channel": "1"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_dict_of_dicts, ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel1.current", - name="Current background RSSI (channel 1)", + translation_key="current_background_rssi", + translation_placeholders={"channel": "1"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -421,14 +429,16 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel2.average", - name="Average background RSSI (channel 2)", + translation_key="average_background_rssi", + translation_placeholders={"channel": "2"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_dict_of_dicts, ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel2.current", - name="Current background RSSI (channel 2)", + translation_key="current_background_rssi", + translation_placeholders={"channel": "2"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -440,46 +450,50 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [ ZWaveJSStatisticsSensorEntityDescription( key="commandsRX", - name="Successful commands (RX)", + translation_key="successful_commands", + translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commandsTX", - name="Successful commands (TX)", + translation_key="successful_commands", + translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commandsDroppedRX", - name="Commands dropped (RX)", + translation_key="commands_dropped", + translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commandsDroppedTX", - name="Commands dropped (TX)", + translation_key="commands_dropped", + translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="timeoutResponse", - name="Timed out responses", + translation_key="timeout_response", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="rtt", - name="Round Trip Time", + translation_key="rtt", native_unit_of_measurement=UnitOfTime.MILLISECONDS, device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, ), ZWaveJSStatisticsSensorEntityDescription( key="rssi", - name="RSSI", + translation_key="rssi", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, ), ZWaveJSStatisticsSensorEntityDescription( key="lastSeen", - name="Last Seen", + translation_key="last_seen", device_class=SensorDeviceClass.TIMESTAMP, convert=( lambda statistics, key: ( diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index db19c0fceeb..9e2317ba728 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -1,6 +1,133 @@ { + "config": { + "abort": { + "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", + "addon_info_failed": "Failed to get Z-Wave JS add-on info.", + "addon_install_failed": "Failed to install the Z-Wave JS add-on.", + "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", + "addon_start_failed": "Failed to start the Z-Wave JS add-on.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "discovery_requires_supervisor": "Discovery requires the supervisor.", + "not_zwave_device": "Discovered device is not a Z-Wave device.", + "not_zwave_js_addon": "Discovered add-on is not the official Z-Wave JS add-on." + }, + "error": { + "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_ws_url": "Invalid websocket URL", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "flow_title": "{name}", + "progress": { + "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." + }, + "step": { + "configure_addon": { + "data": { + "s0_legacy_key": "S0 Key (Legacy)", + "s2_access_control_key": "S2 Access Control Key", + "s2_authenticated_key": "S2 Authenticated Key", + "s2_unauthenticated_key": "S2 Unauthenticated Key", + "usb_path": "[%key:common::config_flow::data::usb_path%]" + }, + "description": "The add-on will generate security keys if those fields are left empty.", + "title": "Enter the Z-Wave JS add-on configuration" + }, + "hassio_confirm": { + "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" + }, + "install_addon": { + "title": "The Z-Wave JS add-on installation has started" + }, + "manual": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Use the Z-Wave JS Supervisor add-on" + }, + "description": "Do you want to use the Z-Wave JS Supervisor add-on?", + "title": "Select connection method" + }, + "start_addon": { + "title": "The Z-Wave JS add-on is starting." + }, + "usb_confirm": { + "description": "Do you want to set up {name} with the Z-Wave JS add-on?" + }, + "zeroconf_confirm": { + "description": "Do you want to add the Z-Wave JS Server with home ID {home_id} found at {url} to Home Assistant?", + "title": "Discovered Z-Wave JS Server" + } + } + }, + "device_automation": { + "action_type": { + "clear_lock_usercode": "Clear usercode on {entity_name}", + "ping": "Ping device", + "refresh_value": "Refresh the value(s) for {entity_name}", + "reset_meter": "Reset meters on {subtype}", + "set_config_parameter": "Set value of config parameter {subtype}", + "set_lock_usercode": "Set a usercode on {entity_name}", + "set_value": "Set value of a Z-Wave Value" + }, + "condition_type": { + "config_parameter": "Config parameter {subtype} value", + "node_status": "Node status", + "value": "Current value of a Z-Wave Value" + }, + "trigger_type": { + "event.notification.entry_control": "Sent an Entry Control notification", + "event.notification.notification": "Sent a notification", + "event.value_notification.basic": "Basic CC event on {subtype}", + "event.value_notification.central_scene": "Central Scene action on {subtype}", + "event.value_notification.scene_activation": "Scene Activation on {subtype}", + "state.node_status": "Node status changed", + "zwave_js.value_updated.config_parameter": "Value change on config parameter {subtype}", + "zwave_js.value_updated.value": "Value change on a Z-Wave JS Value" + } + }, "entity": { + "button": { + "ping": { + "name": "Ping" + } + }, "sensor": { + "average_background_rssi": { + "name": "Average background RSSI (channel {channel})" + }, + "can": { + "name": "Collisions" + }, + "commands_dropped": { + "name": "Commands dropped ({direction})" + }, + "controller_status": { + "name": "Status", + "state": { + "jammed": "Jammed", + "ready": "Ready", + "unresponsive": "Unresponsive" + } + }, + "current_background_rssi": { + "name": "Current background RSSI (channel {channel})" + }, + "last_seen": { + "name": "Last seen" + }, + "messages_dropped": { + "name": "Messages dropped ({direction})" + }, + "nak": { + "name": "Messages not accepted" + }, "node_status": { "name": "Node status", "state": { @@ -11,434 +138,354 @@ "unknown": "Unknown" } }, - "controller_status": { - "name": "Status", - "state": { - "ready": "Ready", - "unresponsive": "Unresponsive", - "jammed": "Jammed" - } + "rssi": { + "name": "RSSI" + }, + "rtt": { + "name": "Round trip time" + }, + "successful_commands": { + "name": "Successful commands ({direction})" + }, + "successful_messages": { + "name": "Successful messages ({direction})" + }, + "timeout_ack": { + "name": "Missing ACKs" + }, + "timeout_callback": { + "name": "Timed out callbacks" + }, + "timeout_response": { + "name": "Timed out responses" } } }, - "config": { - "flow_title": "{name}", - "step": { - "manual": { - "data": { - "url": "[%key:common::config_flow::data::url%]" - } - }, - "usb_confirm": { - "description": "Do you want to set up {name} with the Z-Wave JS add-on?" - }, - "on_supervisor": { - "title": "Select connection method", - "description": "Do you want to use the Z-Wave JS Supervisor add-on?", - "data": { - "use_addon": "Use the Z-Wave JS Supervisor add-on" - } - }, - "install_addon": { - "title": "The Z-Wave JS add-on installation has started" - }, - "configure_addon": { - "title": "Enter the Z-Wave JS add-on configuration", - "description": "The add-on will generate security keys if those fields are left empty.", - "data": { - "usb_path": "[%key:common::config_flow::data::usb_path%]", - "s0_legacy_key": "S0 Key (Legacy)", - "s2_authenticated_key": "S2 Authenticated Key", - "s2_unauthenticated_key": "S2 Unauthenticated Key", - "s2_access_control_key": "S2 Access Control Key" - } - }, - "start_addon": { - "title": "The Z-Wave JS add-on is starting." - }, - "hassio_confirm": { - "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" - }, - "zeroconf_confirm": { - "description": "Do you want to add the Z-Wave JS Server with home ID {home_id} found at {url} to Home Assistant?", - "title": "Discovered Z-Wave JS Server" - } - }, - "error": { - "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.", - "invalid_ws_url": "Invalid websocket URL", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "addon_info_failed": "Failed to get Z-Wave JS add-on info.", - "addon_install_failed": "Failed to install the Z-Wave JS add-on.", - "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", - "addon_start_failed": "Failed to start the Z-Wave JS add-on.", - "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "discovery_requires_supervisor": "Discovery requires the supervisor.", - "not_zwave_device": "Discovered device is not a Z-Wave device.", - "not_zwave_js_addon": "Discovered add-on is not the official Z-Wave JS add-on." - }, - "progress": { - "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", - "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." - } - }, - "options": { - "step": { - "manual": { - "data": { - "url": "[%key:common::config_flow::data::url%]" - } - }, - "on_supervisor": { - "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]", - "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", - "data": { - "use_addon": "[%key:component::zwave_js::config::step::on_supervisor::data::use_addon%]" - } - }, - "install_addon": { - "title": "[%key:component::zwave_js::config::step::install_addon::title%]" - }, - "configure_addon": { - "title": "[%key:component::zwave_js::config::step::configure_addon::title%]", - "description": "[%key:component::zwave_js::config::step::configure_addon::description%]", - "data": { - "usb_path": "[%key:common::config_flow::data::usb_path%]", - "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]", - "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]", - "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]", - "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]", - "log_level": "Log level", - "emulate_hardware": "Emulate Hardware" - } - }, - "start_addon": { - "title": "[%key:component::zwave_js::config::step::start_addon::title%]" - } - }, - "error": { - "invalid_ws_url": "[%key:component::zwave_js::config::error::invalid_ws_url%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "addon_info_failed": "[%key:component::zwave_js::config::abort::addon_info_failed%]", - "addon_install_failed": "[%key:component::zwave_js::config::abort::addon_install_failed%]", - "addon_set_config_failed": "[%key:component::zwave_js::config::abort::addon_set_config_failed%]", - "addon_start_failed": "[%key:component::zwave_js::config::abort::addon_start_failed%]", - "addon_get_discovery_info_failed": "[%key:component::zwave_js::config::abort::addon_get_discovery_info_failed%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device." - }, - "progress": { - "install_addon": "[%key:component::zwave_js::config::progress::install_addon%]", - "start_addon": "[%key:component::zwave_js::config::progress::start_addon%]" - } - }, - "device_automation": { - "trigger_type": { - "event.notification.entry_control": "Sent an Entry Control notification", - "event.notification.notification": "Sent a notification", - "event.value_notification.basic": "Basic CC event on {subtype}", - "event.value_notification.central_scene": "Central Scene action on {subtype}", - "event.value_notification.scene_activation": "Scene Activation on {subtype}", - "zwave_js.value_updated.config_parameter": "Value change on config parameter {subtype}", - "zwave_js.value_updated.value": "Value change on a Z-Wave JS Value", - "state.node_status": "Node status changed" - }, - "condition_type": { - "node_status": "Node status", - "config_parameter": "Config parameter {subtype} value", - "value": "Current value of a Z-Wave Value" - }, - "action_type": { - "clear_lock_usercode": "Clear usercode on {entity_name}", - "set_lock_usercode": "Set a usercode on {entity_name}", - "set_config_parameter": "Set value of config parameter {subtype}", - "set_value": "Set value of a Z-Wave Value", - "refresh_value": "Refresh the value(s) for {entity_name}", - "ping": "Ping device", - "reset_meter": "Reset meters on {subtype}" - } - }, "issues": { - "invalid_server_version": { - "title": "Newer version of Z-Wave JS Server needed", - "description": "The version of Z-Wave JS Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave JS Server to the latest version to fix this issue." - }, "device_config_file_changed": { - "title": "Device configuration file changed: {device_name}", "fix_flow": { + "abort": { + "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant.", + "issue_ignored": "Device config file update for {device_name} ignored." + }, "step": { "init": { + "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you decide to proceed with the re-interview, it will take place in the background.", "menu_options": { "confirm": "Re-interview device", "ignore": "Ignore device config update" }, - "title": "Device configuration file changed: {device_name}", - "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you decide to proceed with the re-interview, it will take place in the background." + "title": "Device configuration file changed: {device_name}" } - }, - "abort": { - "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant.", - "issue_ignored": "Device config file update for {device_name} ignored." } + }, + "title": "Device configuration file changed: {device_name}" + }, + "invalid_server_version": { + "description": "The version of Z-Wave JS Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave JS Server to the latest version to fix this issue.", + "title": "Newer version of Z-Wave JS Server needed" + } + }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "[%key:component::zwave_js::config::abort::addon_get_discovery_info_failed%]", + "addon_info_failed": "[%key:component::zwave_js::config::abort::addon_info_failed%]", + "addon_install_failed": "[%key:component::zwave_js::config::abort::addon_install_failed%]", + "addon_set_config_failed": "[%key:component::zwave_js::config::abort::addon_set_config_failed%]", + "addon_start_failed": "[%key:component::zwave_js::config::abort::addon_start_failed%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device." + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_ws_url": "[%key:component::zwave_js::config::error::invalid_ws_url%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "progress": { + "install_addon": "[%key:component::zwave_js::config::progress::install_addon%]", + "start_addon": "[%key:component::zwave_js::config::progress::start_addon%]" + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulate Hardware", + "log_level": "Log level", + "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]", + "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]", + "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]", + "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]", + "usb_path": "[%key:common::config_flow::data::usb_path%]" + }, + "description": "[%key:component::zwave_js::config::step::configure_addon::description%]", + "title": "[%key:component::zwave_js::config::step::configure_addon::title%]" + }, + "install_addon": { + "title": "[%key:component::zwave_js::config::step::install_addon::title%]" + }, + "manual": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + }, + "on_supervisor": { + "data": { + "use_addon": "[%key:component::zwave_js::config::step::on_supervisor::data::use_addon%]" + }, + "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", + "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]" + }, + "start_addon": { + "title": "[%key:component::zwave_js::config::step::start_addon::title%]" } } }, "services": { - "clear_lock_usercode": { - "name": "Clear lock user code", - "description": "Clears a user code from a lock.", - "fields": { - "code_slot": { - "name": "Code slot", - "description": "Code slot to clear code from." - } - } - }, - "set_lock_usercode": { - "name": "Set lock user code", - "description": "Sets a user code on a lock.", - "fields": { - "code_slot": { - "name": "[%key:component::zwave_js::services::clear_lock_usercode::fields::code_slot::name%]", - "description": "Code slot to set the code." - }, - "usercode": { - "name": "Code", - "description": "Lock code to set." - } - } - }, - "set_config_parameter": { - "name": "Set device configuration parameter", - "description": "Changes the configuration parameters of your Z-Wave devices.", - "fields": { - "endpoint": { - "name": "Endpoint", - "description": "The configuration parameter's endpoint." - }, - "parameter": { - "name": "Parameter", - "description": "The name (or ID) of the configuration parameter you want to configure." - }, - "bitmask": { - "name": "Bitmask", - "description": "Target a specific bitmask (see the documentation for more information). Cannot be combined with value_size or value_format." - }, - "value": { - "name": "Value", - "description": "The new value to set for this configuration parameter." - }, - "value_size": { - "name": "Value size", - "description": "Size of the value, either 1, 2, or 4. Used in combination with value_format when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask." - }, - "value_format": { - "name": "Value format", - "description": "Format of the value, 0 for signed integer, 1 for unsigned integer, 2 for enumerated, 3 for bitfield. Used in combination with value_size when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask." - } - } - }, "bulk_set_partial_config_parameters": { - "name": "Bulk set partial configuration parameters (advanced).", "description": "Allows for bulk setting partial parameters. Useful when multiple partial parameters have to be set at the same time.", "fields": { "endpoint": { - "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]", - "description": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::description%]" + "description": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::description%]", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]" }, "parameter": { - "name": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::name%]", - "description": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::description%]" + "description": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::description%]", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::name%]" }, "value": { - "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]", - "description": "The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter." + "description": "The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter.", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]" } - } + }, + "name": "Bulk set partial configuration parameters (advanced)." }, - "refresh_value": { - "name": "Refresh values", - "description": "Force updates the values of a Z-Wave entity.", + "clear_lock_usercode": { + "description": "Clears a user code from a lock.", "fields": { - "entity_id": { - "name": "Entities", - "description": "Entities to refresh." - }, - "refresh_all_values": { - "name": "Refresh all values?", - "description": "Whether to refresh all values (true) or just the primary value (false)." + "code_slot": { + "description": "Code slot to clear code from.", + "name": "Code slot" } - } - }, - "set_value": { - "name": "Set a value (advanced)", - "description": "Changes any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing.", - "fields": { - "command_class": { - "name": "Command class", - "description": "The ID of the command class for the value." - }, - "endpoint": { - "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]", - "description": "The endpoint for the value." - }, - "property": { - "name": "Property", - "description": "The ID of the property for the value." - }, - "property_key": { - "name": "Property key", - "description": "The ID of the property key for the value." - }, - "value": { - "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]", - "description": "The new value to set." - }, - "options": { - "name": "Options", - "description": "Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set." - }, - "wait_for_result": { - "name": "Wait for result?", - "description": "Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device." - } - } - }, - "multicast_set_value": { - "name": "Set a value on multiple devices via multicast (advanced)", - "description": "Changes any value that Z-Wave JS recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This service has minimal validation so only use this service if you know what you are doing.", - "fields": { - "broadcast": { - "name": "Broadcast?", - "description": "Whether command should be broadcast to all devices on the network." - }, - "command_class": { - "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]", - "description": "[%key:component::zwave_js::services::set_value::fields::command_class::description%]" - }, - "endpoint": { - "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]", - "description": "[%key:component::zwave_js::services::set_value::fields::endpoint::description%]" - }, - "property": { - "name": "[%key:component::zwave_js::services::set_value::fields::property::name%]", - "description": "[%key:component::zwave_js::services::set_value::fields::property::description%]" - }, - "property_key": { - "name": "[%key:component::zwave_js::services::set_value::fields::property_key::name%]", - "description": "[%key:component::zwave_js::services::set_value::fields::property_key::description%]" - }, - "options": { - "name": "[%key:component::zwave_js::services::set_value::fields::options::name%]", - "description": "[%key:component::zwave_js::services::set_value::fields::options::description%]" - }, - "value": { - "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]", - "description": "[%key:component::zwave_js::services::set_value::fields::value::description%]" - } - } - }, - "ping": { - "name": "Ping a node", - "description": "Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep." - }, - "reset_meter": { - "name": "Reset meters on a node", - "description": "Resets the meters on a node.", - "fields": { - "meter_type": { - "name": "Meter type", - "description": "The type of meter to reset. Not all meters support the ability to pick a meter type to reset." - }, - "value": { - "name": "Target value", - "description": "The value that meters should be reset to. Not all meters support the ability to be reset to a specific value." - } - } + }, + "name": "Clear lock user code" }, "invoke_cc_api": { - "name": "Invoke a Command Class API on a node (advanced)", "description": "Calls a Command Class API on a node. Some Command Classes can't be fully controlled via the `set_value` service and require direct calls to the Command Class API.", "fields": { "command_class": { - "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]", - "description": "The ID of the command class that you want to issue a command to." + "description": "The ID of the command class that you want to issue a command to.", + "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]" }, "endpoint": { - "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]", - "description": "The endpoint to call the API on. If an endpoint is specified, that endpoint will be targeted for all nodes associated with the target areas, devices, and/or entities. If an endpoint is not specified, the root endpoint (0) will be targeted for nodes associated with target areas and devices, and the endpoint for the primary value of each entity will be targeted." + "description": "The endpoint to call the API on. If an endpoint is specified, that endpoint will be targeted for all nodes associated with the target areas, devices, and/or entities. If an endpoint is not specified, the root endpoint (0) will be targeted for nodes associated with target areas and devices, and the endpoint for the primary value of each entity will be targeted.", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]" }, "method_name": { - "name": "Method name", - "description": "The name of the API method to call. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods." + "description": "The name of the API method to call. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods.", + "name": "Method name" }, "parameters": { - "name": "Parameters", - "description": "A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters." + "description": "A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters.", + "name": "Parameters" } - } + }, + "name": "Invoke a Command Class API on a node (advanced)" + }, + "multicast_set_value": { + "description": "Changes any value that Z-Wave JS recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This service has minimal validation so only use this service if you know what you are doing.", + "fields": { + "broadcast": { + "description": "Whether command should be broadcast to all devices on the network.", + "name": "Broadcast?" + }, + "command_class": { + "description": "[%key:component::zwave_js::services::set_value::fields::command_class::description%]", + "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]" + }, + "endpoint": { + "description": "[%key:component::zwave_js::services::set_value::fields::endpoint::description%]", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]" + }, + "options": { + "description": "[%key:component::zwave_js::services::set_value::fields::options::description%]", + "name": "[%key:component::zwave_js::services::set_value::fields::options::name%]" + }, + "property": { + "description": "[%key:component::zwave_js::services::set_value::fields::property::description%]", + "name": "[%key:component::zwave_js::services::set_value::fields::property::name%]" + }, + "property_key": { + "description": "[%key:component::zwave_js::services::set_value::fields::property_key::description%]", + "name": "[%key:component::zwave_js::services::set_value::fields::property_key::name%]" + }, + "value": { + "description": "[%key:component::zwave_js::services::set_value::fields::value::description%]", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]" + } + }, + "name": "Set a value on multiple devices via multicast (advanced)" + }, + "ping": { + "description": "Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep.", + "name": "Ping a node" }, "refresh_notifications": { - "name": "Refresh notifications on a node (advanced)", "description": "Refreshes notifications on a node based on notification type and optionally notification event.", "fields": { - "notification_type": { - "name": "Notification Type", - "description": "The Notification Type number as defined in the Z-Wave specs." - }, "notification_event": { - "name": "Notification Event", - "description": "The Notification Event number as defined in the Z-Wave specs." + "description": "The Notification Event number as defined in the Z-Wave specs.", + "name": "Notification Event" + }, + "notification_type": { + "description": "The Notification Type number as defined in the Z-Wave specs.", + "name": "Notification Type" } - } + }, + "name": "Refresh notifications on a node (advanced)" + }, + "refresh_value": { + "description": "Force updates the values of a Z-Wave entity.", + "fields": { + "entity_id": { + "description": "Entities to refresh.", + "name": "Entities" + }, + "refresh_all_values": { + "description": "Whether to refresh all values (true) or just the primary value (false).", + "name": "Refresh all values?" + } + }, + "name": "Refresh values" + }, + "reset_meter": { + "description": "Resets the meters on a node.", + "fields": { + "meter_type": { + "description": "The type of meter to reset. Not all meters support the ability to pick a meter type to reset.", + "name": "Meter type" + }, + "value": { + "description": "The value that meters should be reset to. Not all meters support the ability to be reset to a specific value.", + "name": "Target value" + } + }, + "name": "Reset meters on a node" + }, + "set_config_parameter": { + "description": "Changes the configuration parameters of your Z-Wave devices.", + "fields": { + "bitmask": { + "description": "Target a specific bitmask (see the documentation for more information). Cannot be combined with value_size or value_format.", + "name": "Bitmask" + }, + "endpoint": { + "description": "The configuration parameter's endpoint.", + "name": "Endpoint" + }, + "parameter": { + "description": "The name (or ID) of the configuration parameter you want to configure.", + "name": "Parameter" + }, + "value": { + "description": "The new value to set for this configuration parameter.", + "name": "Value" + }, + "value_format": { + "description": "Format of the value, 0 for signed integer, 1 for unsigned integer, 2 for enumerated, 3 for bitfield. Used in combination with value_size when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask.", + "name": "Value format" + }, + "value_size": { + "description": "Size of the value, either 1, 2, or 4. Used in combination with value_format when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask.", + "name": "Value size" + } + }, + "name": "Set device configuration parameter" }, "set_lock_configuration": { - "name": "Set lock configuration", "description": "Sets the configuration for a lock.", "fields": { - "operation_type": { - "name": "Operation Type", - "description": "The operation type of the lock." - }, - "lock_timeout": { - "name": "Lock timeout", - "description": "Seconds until lock mode times out. Should only be used if operation type is `timed`." - }, - "outside_handles_can_open_door_configuration": { - "name": "Outside handles can open door configuration", - "description": "A list of four booleans which indicate which outside handles can open the door." - }, - "inside_handles_can_open_door_configuration": { - "name": "Inside handles can open door configuration", - "description": "A list of four booleans which indicate which inside handles can open the door." - }, "auto_relock_time": { - "name": "Auto relock time", - "description": "Duration in seconds until lock returns to secure state. Only enforced when operation type is `constant`." - }, - "hold_and_release_time": { - "name": "Hold and release time", - "description": "Duration in seconds the latch stays retracted." - }, - "twist_assist": { - "name": "Twist assist", - "description": "Enable Twist Assist." + "description": "Duration in seconds until lock returns to secure state. Only enforced when operation type is `constant`.", + "name": "Auto relock time" }, "block_to_block": { - "name": "Block to block", - "description": "Enable block-to-block functionality." + "description": "Enable block-to-block functionality.", + "name": "Block to block" + }, + "hold_and_release_time": { + "description": "Duration in seconds the latch stays retracted.", + "name": "Hold and release time" + }, + "inside_handles_can_open_door_configuration": { + "description": "A list of four booleans which indicate which inside handles can open the door.", + "name": "Inside handles can open door configuration" + }, + "lock_timeout": { + "description": "Seconds until lock mode times out. Should only be used if operation type is `timed`.", + "name": "Lock timeout" + }, + "operation_type": { + "description": "The operation type of the lock.", + "name": "Operation Type" + }, + "outside_handles_can_open_door_configuration": { + "description": "A list of four booleans which indicate which outside handles can open the door.", + "name": "Outside handles can open door configuration" + }, + "twist_assist": { + "description": "Enable Twist Assist.", + "name": "Twist assist" } - } + }, + "name": "Set lock configuration" + }, + "set_lock_usercode": { + "description": "Sets a user code on a lock.", + "fields": { + "code_slot": { + "description": "Code slot to set the code.", + "name": "[%key:component::zwave_js::services::clear_lock_usercode::fields::code_slot::name%]" + }, + "usercode": { + "description": "Lock code to set.", + "name": "Code" + } + }, + "name": "Set lock user code" + }, + "set_value": { + "description": "Changes any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing.", + "fields": { + "command_class": { + "description": "The ID of the command class for the value.", + "name": "Command class" + }, + "endpoint": { + "description": "The endpoint for the value.", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]" + }, + "options": { + "description": "Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set.", + "name": "Options" + }, + "property": { + "description": "The ID of the property for the value.", + "name": "Property" + }, + "property_key": { + "description": "The ID of the property key for the value.", + "name": "Property key" + }, + "value": { + "description": "The new value to set.", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]" + }, + "wait_for_result": { + "description": "Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device.", + "name": "Wait for result?" + } + }, + "name": "Set a value (advanced)" } } } From a1eaa5cbf2633754d157324a8a40f6100fcb4d05 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Feb 2024 14:10:24 -0600 Subject: [PATCH 005/125] Migrate to new intent error response keys (#109269) --- .../components/conversation/default_agent.py | 120 +++++++++--------- .../components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/snapshots/test_init.ambr | 18 +-- .../conversation/test_default_agent.py | 110 ++++++++++++++-- 7 files changed, 174 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 123dc7aba1d..a2cb3b68041 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -12,22 +12,15 @@ import re from typing import IO, Any from hassil.expression import Expression, ListReference, Sequence -from hassil.intents import ( - Intents, - ResponseType, - SlotList, - TextSlotList, - WildcardSlotList, -) +from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList from hassil.recognize import ( MISSING_ENTITY, RecognizeResult, - UnmatchedEntity, UnmatchedTextEntity, recognize_all, ) from hassil.util import merge_dict -from home_assistant_intents import get_intents, get_languages +from home_assistant_intents import ErrorKey, get_intents, get_languages import yaml from homeassistant import core, setup @@ -262,7 +255,7 @@ class DefaultAgent(AbstractConversationAgent): return _make_error_result( language, intent.IntentResponseErrorCode.NO_INTENT_MATCH, - self._get_error_text(ResponseType.NO_INTENT, lang_intents), + self._get_error_text(ErrorKey.NO_INTENT, lang_intents), conversation_id, ) @@ -276,9 +269,7 @@ class DefaultAgent(AbstractConversationAgent): else "", result.unmatched_entities_list, ) - error_response_type, error_response_args = _get_unmatched_response( - result.unmatched_entities - ) + error_response_type, error_response_args = _get_unmatched_response(result) return _make_error_result( language, intent.IntentResponseErrorCode.NO_VALID_TARGETS, @@ -328,7 +319,7 @@ class DefaultAgent(AbstractConversationAgent): return _make_error_result( language, intent.IntentResponseErrorCode.FAILED_TO_HANDLE, - self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents), + self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents), conversation_id, ) except intent.IntentUnexpectedError: @@ -336,7 +327,7 @@ class DefaultAgent(AbstractConversationAgent): return _make_error_result( language, intent.IntentResponseErrorCode.UNKNOWN, - self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents), + self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents), conversation_id, ) @@ -798,7 +789,7 @@ class DefaultAgent(AbstractConversationAgent): def _get_error_text( self, - response_type: ResponseType, + error_key: ErrorKey, lang_intents: LanguageIntents | None, **response_args, ) -> str: @@ -806,7 +797,7 @@ class DefaultAgent(AbstractConversationAgent): if lang_intents is None: return _DEFAULT_ERROR_TEXT - response_key = response_type.value + response_key = error_key.value response_str = ( lang_intents.error_responses.get(response_key) or _DEFAULT_ERROR_TEXT ) @@ -919,59 +910,72 @@ def _make_error_result( return ConversationResult(response, conversation_id) -def _get_unmatched_response( - unmatched_entities: dict[str, UnmatchedEntity], -) -> tuple[ResponseType, dict[str, Any]]: - error_response_type = ResponseType.NO_INTENT - error_response_args: dict[str, Any] = {} +def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str, Any]]: + """Get key and template arguments for error when there are unmatched intent entities/slots.""" - if unmatched_name := unmatched_entities.get("name"): - # Unmatched device or entity - assert isinstance(unmatched_name, UnmatchedTextEntity) - error_response_type = ResponseType.NO_ENTITY - error_response_args["entity"] = unmatched_name.text + # Filter out non-text and missing context entities + unmatched_text: dict[str, str] = { + key: entity.text.strip() + for key, entity in result.unmatched_entities.items() + if isinstance(entity, UnmatchedTextEntity) and entity.text != MISSING_ENTITY + } - elif unmatched_area := unmatched_entities.get("area"): - # Unmatched area - assert isinstance(unmatched_area, UnmatchedTextEntity) - error_response_type = ResponseType.NO_AREA - error_response_args["area"] = unmatched_area.text + if unmatched_area := unmatched_text.get("area"): + # area only + return ErrorKey.NO_AREA, {"area": unmatched_area} - return error_response_type, error_response_args + # Area may still have matched + matched_area: str | None = None + if matched_area_entity := result.entities.get("area"): + matched_area = matched_area_entity.text.strip() + + if unmatched_name := unmatched_text.get("name"): + if matched_area: + # device in area + return ErrorKey.NO_ENTITY_IN_AREA, { + "entity": unmatched_name, + "area": matched_area, + } + + # device only + return ErrorKey.NO_ENTITY, {"entity": unmatched_name} + + # Default error + return ErrorKey.NO_INTENT, {} def _get_no_states_matched_response( no_states_error: intent.NoStatesMatchedError, -) -> tuple[ResponseType, dict[str, Any]]: - """Return error response type and template arguments for error.""" - if not ( - no_states_error.area - and (no_states_error.device_classes or no_states_error.domains) - ): - # Device class and domain must be paired with an area for the error - # message. - return ResponseType.NO_INTENT, {} +) -> tuple[ErrorKey, dict[str, Any]]: + """Return key and template arguments for error when intent returns no matching states.""" - error_response_args: dict[str, Any] = {"area": no_states_error.area} - - # Check device classes first, since it's more specific than domain + # Device classes should be checked before domains if no_states_error.device_classes: - # No exposed entities of a particular class in an area. - # Example: "close the bedroom windows" - # - # Only use the first device class for the error message - error_response_args["device_class"] = next(iter(no_states_error.device_classes)) + device_class = next(iter(no_states_error.device_classes)) # first device class + if no_states_error.area: + # device_class in area + return ErrorKey.NO_DEVICE_CLASS_IN_AREA, { + "device_class": device_class, + "area": no_states_error.area, + } - return ResponseType.NO_DEVICE_CLASS, error_response_args + # device_class only + return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class} - # No exposed entities of a domain in an area. - # Example: "turn on lights in kitchen" - assert no_states_error.domains - # - # Only use the first domain for the error message - error_response_args["domain"] = next(iter(no_states_error.domains)) + if no_states_error.domains: + domain = next(iter(no_states_error.domains)) # first domain + if no_states_error.area: + # domain in area + return ErrorKey.NO_DOMAIN_IN_AREA, { + "domain": domain, + "area": no_states_error.area, + } - return ResponseType.NO_DOMAIN, error_response_args + # domain only + return ErrorKey.NO_DOMAIN, {"domain": domain} + + # Default error + return ErrorKey.NO_INTENT, {} def _collect_list_references(expression: Expression, list_names: set[str]) -> None: diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 89dd880f69e..ea0a11ae657 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.0", "home-assistant-intents==2024.1.29"] + "requirements": ["hassil==1.6.0", "home-assistant-intents==2024.2.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c5cea22795a..1b47b2693b0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ hass-nabucasa==0.76.0 hassil==1.6.0 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240131.0 -home-assistant-intents==2024.1.29 +home-assistant-intents==2024.2.1 httpx==0.26.0 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index da3092eab7a..2b804e3720a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1062,7 +1062,7 @@ holidays==0.41 home-assistant-frontend==20240131.0 # homeassistant.components.conversation -home-assistant-intents==2024.1.29 +home-assistant-intents==2024.2.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8da20de5f7..10c89637f25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -858,7 +858,7 @@ holidays==0.41 home-assistant-frontend==20240131.0 # homeassistant.components.conversation -home-assistant-intents==2024.1.29 +home-assistant-intents==2024.2.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 23dab0902a9..468f3215cb7 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -339,7 +339,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'An unexpected error occurred while handling the intent', + 'speech': 'An unexpected error occurred', }), }), }), @@ -379,7 +379,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'An unexpected error occurred while handling the intent', + 'speech': 'An unexpected error occurred', }), }), }), @@ -519,7 +519,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called late added alias', + 'speech': 'Sorry, I am not aware of any device called late added alias', }), }), }), @@ -539,7 +539,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -679,7 +679,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called late added light', + 'speech': 'Sorry, I am not aware of any device called late added light', }), }), }), @@ -759,7 +759,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -779,7 +779,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called my cool light', + 'speech': 'Sorry, I am not aware of any device called my cool light', }), }), }), @@ -919,7 +919,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -969,7 +969,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called renamed light', + 'speech': 'Sorry, I am not aware of any device called renamed light', }), }), }), diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index b992b0086d7..d7182aa3c2f 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -2,6 +2,7 @@ from collections import defaultdict from unittest.mock import AsyncMock, patch +from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult import pytest from homeassistant.components import conversation @@ -430,8 +431,8 @@ async def test_device_area_context( ) -async def test_error_missing_entity(hass: HomeAssistant, init_components) -> None: - """Test error message when entity is missing.""" +async def test_error_no_device(hass: HomeAssistant, init_components) -> None: + """Test error message when device/entity is missing.""" result = await conversation.async_converse( hass, "turn on missing entity", None, Context(), None ) @@ -440,11 +441,11 @@ async def test_error_missing_entity(hass: HomeAssistant, init_components) -> Non assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS assert ( result.response.speech["plain"]["speech"] - == "Sorry, I am not aware of any device or entity called missing entity" + == "Sorry, I am not aware of any device called missing entity" ) -async def test_error_missing_area(hass: HomeAssistant, init_components) -> None: +async def test_error_no_area(hass: HomeAssistant, init_components) -> None: """Test error message when area is missing.""" result = await conversation.async_converse( hass, "turn on the lights in missing area", None, Context(), None @@ -458,10 +459,60 @@ async def test_error_missing_area(hass: HomeAssistant, init_components) -> None: ) -async def test_error_no_exposed_for_domain( +async def test_error_no_device_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: - """Test error message when no entities for a domain are exposed in an area.""" + """Test error message when area is missing a device/entity.""" + area_registry.async_get_or_create("kitchen") + result = await conversation.async_converse( + hass, "turn on missing entity in the kitchen", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any device called missing entity in the kitchen area" + ) + + +async def test_error_no_domain( + hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry +) -> None: + """Test error message when no devices/entities exist for a domain.""" + + # We don't have a sentence for turning on all fans + fan_domain = MatchEntity(name="domain", value="fan", text="") + recognize_result = RecognizeResult( + intent=Intent("HassTurnOn"), + intent_data=IntentData([]), + entities={"domain": fan_domain}, + entities_list=[fan_domain], + ) + + with patch( + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], + ): + result = await conversation.async_converse( + hass, "turn on the fans", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert ( + result.response.error_code + == intent.IntentResponseErrorCode.NO_VALID_TARGETS + ) + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any fan" + ) + + +async def test_error_no_domain_in_area( + hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry +) -> None: + """Test error message when no devices/entities for a domain exist in an area.""" area_registry.async_get_or_create("kitchen") result = await conversation.async_converse( hass, "turn on the lights in the kitchen", None, Context(), None @@ -475,10 +526,43 @@ async def test_error_no_exposed_for_domain( ) -async def test_error_no_exposed_for_device_class( +async def test_error_no_device_class( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: - """Test error message when no entities of a device class are exposed in an area.""" + """Test error message when no entities of a device class exist.""" + + # We don't have a sentence for opening all windows + window_class = MatchEntity(name="device_class", value="window", text="") + recognize_result = RecognizeResult( + intent=Intent("HassTurnOn"), + intent_data=IntentData([]), + entities={"device_class": window_class}, + entities_list=[window_class], + ) + + with patch( + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], + ): + result = await conversation.async_converse( + hass, "open the windows", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert ( + result.response.error_code + == intent.IntentResponseErrorCode.NO_VALID_TARGETS + ) + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any window" + ) + + +async def test_error_no_device_class_in_area( + hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry +) -> None: + """Test error message when no entities of a device class exist in an area.""" area_registry.async_get_or_create("bedroom") result = await conversation.async_converse( hass, "open bedroom windows", None, Context(), None @@ -492,8 +576,8 @@ async def test_error_no_exposed_for_device_class( ) -async def test_error_match_failure(hass: HomeAssistant, init_components) -> None: - """Test response with complete match failure.""" +async def test_error_no_intent(hass: HomeAssistant, init_components) -> None: + """Test response with an intent match failure.""" with patch( "homeassistant.components.conversation.default_agent.recognize_all", return_value=[], @@ -506,6 +590,10 @@ async def test_error_match_failure(hass: HomeAssistant, init_components) -> None assert ( result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH ) + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I couldn't understand that" + ) async def test_no_states_matched_default_error( @@ -601,5 +689,5 @@ async def test_all_domains_loaded( assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS assert ( result.response.speech["plain"]["speech"] - == "Sorry, I am not aware of any device or entity called test light" + == "Sorry, I am not aware of any device called test light" ) From c1f883519daa235e6ead9db768ecf550356cc763 Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Thu, 1 Feb 2024 15:15:41 -0500 Subject: [PATCH 006/125] Add connect octoprint printer service (#99899) * Add connect octoprint printer service * Review changes * String updates * Swap exception type --- .../components/octoprint/__init__.py | 56 +++++++++++++++- homeassistant/components/octoprint/const.py | 3 + .../components/octoprint/services.yaml | 27 ++++++++ .../components/octoprint/strings.json | 29 ++++++++ tests/components/octoprint/test_servics.py | 66 +++++++++++++++++++ 5 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/octoprint/services.yaml create mode 100644 tests/components/octoprint/test_servics.py diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 50ba6c964f3..1a96078c003 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast import aiohttp from pyoctoprintapi import OctoprintClient @@ -11,24 +12,28 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_BINARY_SENSORS, + CONF_DEVICE_ID, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PATH, CONF_PORT, + CONF_PROFILE_NAME, CONF_SENSORS, CONF_SSL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify as util_slugify from homeassistant.util.ssl import get_default_context, get_default_no_verify_context -from .const import DOMAIN +from .const import CONF_BAUDRATE, DOMAIN, SERVICE_CONNECT from .coordinator import OctoprintDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -122,6 +127,15 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_CONNECT_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_PROFILE_NAME): cv.string, + vol.Optional(CONF_PORT): cv.string, + vol.Optional(CONF_BAUDRATE): cv.positive_int, + } +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the OctoPrint component.""" @@ -194,6 +208,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + async def async_printer_connect(call: ServiceCall) -> None: + """Connect to a printer.""" + client = async_get_client_for_service_call(hass, call) + await client.connect( + printer_profile=call.data.get(CONF_PROFILE_NAME), + port=call.data.get(CONF_PORT), + baud_rate=call.data.get(CONF_BAUDRATE), + ) + + if not hass.services.has_service(DOMAIN, SERVICE_CONNECT): + hass.services.async_register( + DOMAIN, + SERVICE_CONNECT, + async_printer_connect, + schema=SERVICE_CONNECT_SCHEMA, + ) + return True @@ -205,3 +236,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +def async_get_client_for_service_call( + hass: HomeAssistant, call: ServiceCall +) -> OctoprintClient: + """Get the client related to a service call (by device ID).""" + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(hass) + + if device_entry := device_registry.async_get(device_id): + for entry_id in device_entry.config_entries: + if data := hass.data[DOMAIN].get(entry_id): + return cast(OctoprintClient, data["client"]) + + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="missing_client", + translation_placeholders={ + "device_id": device_id, + }, + ) diff --git a/homeassistant/components/octoprint/const.py b/homeassistant/components/octoprint/const.py index df22cb8d8f8..2d2a9e4a907 100644 --- a/homeassistant/components/octoprint/const.py +++ b/homeassistant/components/octoprint/const.py @@ -3,3 +3,6 @@ DOMAIN = "octoprint" DEFAULT_NAME = "OctoPrint" + +SERVICE_CONNECT = "printer_connect" +CONF_BAUDRATE = "baudrate" diff --git a/homeassistant/components/octoprint/services.yaml b/homeassistant/components/octoprint/services.yaml new file mode 100644 index 00000000000..2cb4a6f3c2d --- /dev/null +++ b/homeassistant/components/octoprint/services.yaml @@ -0,0 +1,27 @@ +printer_connect: + fields: + device_id: + required: true + selector: + device: + integration: octoprint + profile_name: + required: false + selector: + text: + port: + required: false + selector: + text: + baudrate: + required: false + selector: + select: + options: + - "9600" + - "19200" + - "38400" + - "57600" + - "115200" + - "230400" + - "250000" diff --git a/homeassistant/components/octoprint/strings.json b/homeassistant/components/octoprint/strings.json index 63d9753ee1d..e9df0ed755c 100644 --- a/homeassistant/components/octoprint/strings.json +++ b/homeassistant/components/octoprint/strings.json @@ -35,5 +35,34 @@ "progress": { "get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'." } + }, + "exceptions": { + "missing_client": { + "message": "No client for device ID: {device_id}" + } + }, + "services": { + "printer_connect": { + "name": "Connect to a printer", + "description": "Instructs the octoprint server to connect to a printer.", + "fields": { + "device_id": { + "name": "Server", + "description": "The server that should connect." + }, + "profile_name": { + "name": "Profile name", + "description": "Printer profile to connect with." + }, + "port": { + "name": "Serial port", + "description": "Port name to connect on." + }, + "baudrate": { + "name": "Baudrate", + "description": "Baud rate." + } + } + } } } diff --git a/tests/components/octoprint/test_servics.py b/tests/components/octoprint/test_servics.py new file mode 100644 index 00000000000..70e983c4bb4 --- /dev/null +++ b/tests/components/octoprint/test_servics.py @@ -0,0 +1,66 @@ +"""Test the OctoPrint services.""" +from unittest.mock import patch + +from homeassistant.components.octoprint.const import ( + CONF_BAUDRATE, + DOMAIN, + SERVICE_CONNECT, +) +from homeassistant.const import ATTR_DEVICE_ID, CONF_PORT, CONF_PROFILE_NAME +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get as async_get_dev_reg, +) + +from . import init_integration + + +async def test_connect_default(hass) -> None: + """Test the connect to printer service.""" + await init_integration(hass, "sensor") + + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, "uuid")[0] + + # Test pausing the printer when it is printing + with patch("pyoctoprintapi.OctoprintClient.connect") as connect_command: + await hass.services.async_call( + DOMAIN, + SERVICE_CONNECT, + { + ATTR_DEVICE_ID: device.id, + }, + blocking=True, + ) + + assert len(connect_command.mock_calls) == 1 + connect_command.assert_called_with( + port=None, printer_profile=None, baud_rate=None + ) + + +async def test_connect_all_arguments(hass) -> None: + """Test the connect to printer service.""" + await init_integration(hass, "sensor") + + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, "uuid")[0] + + # Test pausing the printer when it is printing + with patch("pyoctoprintapi.OctoprintClient.connect") as connect_command: + await hass.services.async_call( + DOMAIN, + SERVICE_CONNECT, + { + ATTR_DEVICE_ID: device.id, + CONF_PROFILE_NAME: "Test Profile", + CONF_PORT: "VIRTUAL", + CONF_BAUDRATE: 9600, + }, + blocking=True, + ) + + assert len(connect_command.mock_calls) == 1 + connect_command.assert_called_with( + port="VIRTUAL", printer_profile="Test Profile", baud_rate=9600 + ) From 3511f3541835187988056a1ac9bee6eb3c1d6eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 1 Feb 2024 22:06:34 +0100 Subject: [PATCH 007/125] Fix custom attribute lookup in Traccar Server (#109331) --- .../components/traccar_server/coordinator.py | 14 ++++++++------ .../components/traccar_server/device_tracker.py | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/traccar_server/coordinator.py b/homeassistant/components/traccar_server/coordinator.py index 337d0dcafbb..90c910e6062 100644 --- a/homeassistant/components/traccar_server/coordinator.py +++ b/homeassistant/components/traccar_server/coordinator.py @@ -93,10 +93,9 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat skip_accuracy_filter = False for custom_attr in self.custom_attributes: - attr[custom_attr] = getattr( - device["attributes"], + attr[custom_attr] = device["attributes"].get( custom_attr, - getattr(position["attributes"], custom_attr, None), + position["attributes"].get(custom_attr, None), ) if custom_attr in self.skip_accuracy_filter_for: skip_accuracy_filter = True @@ -151,13 +150,16 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat device = get_device(event["deviceId"], devices) self.hass.bus.async_fire( # This goes against two of the HA core guidelines: - # 1. Event names should be prefixed with the domain name of the integration + # 1. Event names should be prefixed with the domain name of + # the integration # 2. This should be event entities - # However, to not break it for those who currently use the "old" integration, this is kept as is. + # + # However, to not break it for those who currently use + # the "old" integration, this is kept as is. f"traccar_{EVENTS[event['type']]}", { "device_traccar_id": event["deviceId"], - "device_name": getattr(device, "name", None), + "device_name": device["name"] if device else None, "type": event["type"], "serverTime": event["eventTime"], "attributes": event["attributes"], diff --git a/homeassistant/components/traccar_server/device_tracker.py b/homeassistant/components/traccar_server/device_tracker.py index 2abcc6398fb..226d942e465 100644 --- a/homeassistant/components/traccar_server/device_tracker.py +++ b/homeassistant/components/traccar_server/device_tracker.py @@ -51,12 +51,13 @@ class TraccarServerDeviceTracker(TraccarServerEntity, TrackerEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return device specific attributes.""" + geofence_name = self.traccar_geofence["name"] if self.traccar_geofence else None return { **self.traccar_attributes, ATTR_ADDRESS: self.traccar_position["address"], ATTR_ALTITUDE: self.traccar_position["altitude"], ATTR_CATEGORY: self.traccar_device["category"], - ATTR_GEOFENCE: getattr(self.traccar_geofence, "name", None), + ATTR_GEOFENCE: geofence_name, ATTR_MOTION: self.traccar_position["attributes"].get("motion", False), ATTR_SPEED: self.traccar_position["speed"], ATTR_STATUS: self.traccar_device["status"], From 26be6a677c73a07d68074fa2abcaad85d3bbacd2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Feb 2024 22:13:41 +0100 Subject: [PATCH 008/125] Update Home Assistant base image to 2024.02.0 (#109329) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 824d580913d..d0baa4ac18e 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.01.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.01.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.01.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.01.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.01.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.02.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.02.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 8038d833e8f82fa7d381fd153cb153924412d29a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 1 Feb 2024 22:28:02 +0100 Subject: [PATCH 009/125] Add device class to tesla wall connector session energy (#109333) --- homeassistant/components/tesla_wall_connector/sensor.py | 2 ++ tests/components/tesla_wall_connector/test_sensor.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index 09933d628fe..67d3d4ba22e 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -153,7 +153,9 @@ WALL_CONNECTOR_SENSORS = [ key="session_energy_wh", translation_key="session_energy_wh", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].session_energy_wh, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.MEASUREMENT, ), WallConnectorSensorDescription( diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index 684d7de0e82..28b50ba72ea 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -47,7 +47,7 @@ async def test_sensors(hass: HomeAssistant) -> None: "sensor.tesla_wall_connector_phase_c_voltage", "232.1", "230" ), EntityAndExpectedValues( - "sensor.tesla_wall_connector_session_energy", "1234.56", "112.2" + "sensor.tesla_wall_connector_session_energy", "1.23456", "0.1122" ), ] From 5a35c2e1e99ebee62655561eaef405c988ed437a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Feb 2024 15:39:44 -0600 Subject: [PATCH 010/125] Fix stale camera error message in img_util (#109325) --- homeassistant/components/camera/img_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/img_util.py b/homeassistant/components/camera/img_util.py index dcb321d5ebb..e41e43c3a3c 100644 --- a/homeassistant/components/camera/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -98,6 +98,6 @@ class TurboJPEGSingleton: TurboJPEGSingleton.__instance = TurboJPEG() except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error loading libturbojpeg; Cameras may impact HomeKit performance" + "Error loading libturbojpeg; Camera snapshot performance will be sub-optimal" ) TurboJPEGSingleton.__instance = False From b32371b5c94a40d70044de9298c58f7d50c328a2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 07:39:54 +0100 Subject: [PATCH 011/125] Add TURN_ON/OFF ClimateEntityFeature for Matter (#108974) * Add TURN_ON/OFF ClimateEntityFeature for Matter * Adjust matter --- homeassistant/components/matter/climate.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index a22f9174d2a..8769fc430d8 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -73,11 +73,8 @@ class MatterClimate(MatterEntity, ClimateEntity): """Representation of a Matter climate entity.""" _attr_temperature_unit: str = UnitOfTemperature.CELSIUS - _attr_supported_features: ClimateEntityFeature = ( - ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - ) _attr_hvac_mode: HVACMode = HVACMode.OFF + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -99,6 +96,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_hvac_modes.append(HVACMode.COOL) if feature_map & ThermostatFeature.kAutoMode: self._attr_hvac_modes.append(HVACMode.HEAT_COOL) + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TURN_OFF + ) + if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF): + self._attr_supported_features |= ClimateEntityFeature.TURN_ON async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" From b471b9926d4a4b812c96e872857050214ab2a644 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 2 Feb 2024 02:58:03 -0500 Subject: [PATCH 012/125] Missing template helper translation keys (#109347) --- homeassistant/components/template/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 19ad9e5ddeb..79cd0289724 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -75,6 +75,7 @@ "safety": "[%key:component::binary_sensor::entity_component::safety::name%]", "smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]", "sound": "[%key:component::binary_sensor::entity_component::sound::name%]", + "tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]", "update": "[%key:component::binary_sensor::entity_component::update::name%]", "vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]", "window": "[%key:component::binary_sensor::entity_component::window::name%]" @@ -127,6 +128,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From 1c84997c5cd64a7527e6b7a12dcfa40414668cc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 01:58:55 -0600 Subject: [PATCH 013/125] Reduce lock contention when all icons are already cached (#109352) --- homeassistant/helpers/icon.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index 3486925b095..dd216a78648 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -13,7 +13,6 @@ from homeassistant.util.json import load_json_object from .translation import build_resources -ICON_LOAD_LOCK = "icon_load_lock" ICON_CACHE = "icon_cache" _LOGGER = logging.getLogger(__name__) @@ -73,13 +72,14 @@ async def _async_get_component_icons( class _IconsCache: """Cache for icons.""" - __slots__ = ("_hass", "_loaded", "_cache") + __slots__ = ("_hass", "_loaded", "_cache", "_lock") def __init__(self, hass: HomeAssistant) -> None: """Initialize the cache.""" self._hass = hass self._loaded: set[str] = set() self._cache: dict[str, dict[str, Any]] = {} + self._lock = asyncio.Lock() async def async_fetch( self, @@ -88,7 +88,13 @@ class _IconsCache: ) -> dict[str, dict[str, Any]]: """Load resources into the cache.""" if components_to_load := components - self._loaded: - await self._async_load(components_to_load) + # Icons are never unloaded so if there are no components to load + # we can skip the lock which reduces contention + async with self._lock: + # Check components to load again, as another task might have loaded + # them while we were waiting for the lock. + if components_to_load := components - self._loaded: + await self._async_load(components_to_load) return { component: result @@ -143,21 +149,19 @@ async def async_get_icons( """Return all icons of integrations. If integration specified, load it for that one; otherwise default to loaded - intgrations. + integrations. """ - lock = hass.data.setdefault(ICON_LOAD_LOCK, asyncio.Lock()) - if integrations: components = set(integrations) else: components = { component for component in hass.config.components if "." not in component } - async with lock: - if ICON_CACHE in hass.data: - cache: _IconsCache = hass.data[ICON_CACHE] - else: - cache = hass.data[ICON_CACHE] = _IconsCache(hass) + + if ICON_CACHE in hass.data: + cache: _IconsCache = hass.data[ICON_CACHE] + else: + cache = hass.data[ICON_CACHE] = _IconsCache(hass) return await cache.async_fetch(category, components) From 155499fafe6eaffd943bd6320db7185b08ae3ac4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 02:00:46 -0600 Subject: [PATCH 014/125] Load json file as binary instead of decoding to string (#109351) --- homeassistant/util/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 630c39b3ad4..65f93020cc6 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -74,7 +74,7 @@ def load_json( Defaults to returning empty dict if file is not found. """ try: - with open(filename, encoding="utf-8") as fdesc: + with open(filename, mode="rb") as fdesc: return orjson.loads(fdesc.read()) # type: ignore[no-any-return] except FileNotFoundError: # This is not a fatal error From 9204e85b61af3d8b05bf4c0741a2680e0ea50f15 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:01:38 +0100 Subject: [PATCH 015/125] Add sensibo migrated ClimateEntityFeatures (#109340) Adds sensibo migrated ClimateEntityFeatures --- homeassistant/components/sensibo/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index a718cac88fb..bcc851e02ae 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -191,6 +191,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): _attr_name = None _attr_precision = PRECISION_TENTHS _attr_translation_key = "climate_device" + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str From 582d6968b281dd1cbf29fa31a3dbf2600ea5515e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 02:02:26 -0600 Subject: [PATCH 016/125] Avoid de/recode of bytes to string to bytes when writing json files (#109348) --- homeassistant/helpers/json.py | 21 +++++++++++++-------- homeassistant/util/file.py | 14 +++++--------- tests/util/test_file.py | 5 +++++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index b9862907960..ba2486a196e 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -148,12 +148,17 @@ JSON_DUMP: Final = json_dumps def _orjson_default_encoder(data: Any) -> str: - """JSON encoder that uses orjson with hass defaults.""" + """JSON encoder that uses orjson with hass defaults and returns a str.""" + return _orjson_bytes_default_encoder(data).decode("utf-8") + + +def _orjson_bytes_default_encoder(data: Any) -> bytes: + """JSON encoder that uses orjson with hass defaults and returns bytes.""" return orjson.dumps( data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS, default=json_encoder_default, - ).decode("utf-8") + ) def save_json( @@ -173,11 +178,13 @@ def save_json( if encoder and encoder is not JSONEncoder: # If they pass a custom encoder that is not the # default JSONEncoder, we use the slow path of json.dumps + mode = "w" dump = json.dumps - json_data = json.dumps(data, indent=2, cls=encoder) + json_data: str | bytes = json.dumps(data, indent=2, cls=encoder) else: + mode = "wb" dump = _orjson_default_encoder - json_data = _orjson_default_encoder(data) + json_data = _orjson_bytes_default_encoder(data) except TypeError as error: formatted_data = format_unserializable_data( find_paths_unserializable_data(data, dump=dump) @@ -186,10 +193,8 @@ def save_json( _LOGGER.error(msg) raise SerializationError(msg) from error - if atomic_writes: - write_utf8_file_atomic(filename, json_data, private) - else: - write_utf8_file(filename, json_data, private) + method = write_utf8_file_atomic if atomic_writes else write_utf8_file + method(filename, json_data, private, mode=mode) def find_paths_unserializable_data( diff --git a/homeassistant/util/file.py b/homeassistant/util/file.py index 06471eaca6a..1af65fa51d7 100644 --- a/homeassistant/util/file.py +++ b/homeassistant/util/file.py @@ -17,9 +17,7 @@ class WriteError(HomeAssistantError): def write_utf8_file_atomic( - filename: str, - utf8_data: str, - private: bool = False, + filename: str, utf8_data: bytes | str, private: bool = False, mode: str = "w" ) -> None: """Write a file and rename it into place using atomicwrites. @@ -34,7 +32,7 @@ def write_utf8_file_atomic( negatively impact performance. """ try: - with AtomicWriter(filename, overwrite=True).open() as fdesc: + with AtomicWriter(filename, mode=mode, overwrite=True).open() as fdesc: if not private: os.fchmod(fdesc.fileno(), 0o644) fdesc.write(utf8_data) @@ -44,20 +42,18 @@ def write_utf8_file_atomic( def write_utf8_file( - filename: str, - utf8_data: str, - private: bool = False, + filename: str, utf8_data: bytes | str, private: bool = False, mode: str = "w" ) -> None: """Write a file and rename it into place. Writes all or nothing. """ - tmp_filename = "" + encoding = "utf-8" if "b" not in mode else None try: # Modern versions of Python tempfile create this file with mode 0o600 with tempfile.NamedTemporaryFile( - mode="w", encoding="utf-8", dir=os.path.dirname(filename), delete=False + mode=mode, encoding=encoding, dir=os.path.dirname(filename), delete=False ) as fdesc: fdesc.write(utf8_data) tmp_filename = fdesc.name diff --git a/tests/util/test_file.py b/tests/util/test_file.py index 0b87985fe13..dc09ff83e9e 100644 --- a/tests/util/test_file.py +++ b/tests/util/test_file.py @@ -25,6 +25,11 @@ def test_write_utf8_file_atomic_private(tmpdir: py.path.local, func) -> None: assert fh.read() == '{"some":"data"}' assert os.stat(test_file).st_mode & 0o777 == 0o600 + func(test_file, b'{"some":"data"}', True, mode="wb") + with open(test_file) as fh: + assert fh.read() == '{"some":"data"}' + assert os.stat(test_file).st_mode & 0o777 == 0o600 + def test_write_utf8_file_fails_at_creation(tmpdir: py.path.local) -> None: """Test that failed creation of the temp file does not create an empty file.""" From 449790c1782f1ca52de28855c57d8f3018932f3e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:02:41 +0100 Subject: [PATCH 017/125] Add Adax migrated ClimateEntityFeatures (#109341) --- homeassistant/components/adax/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 2ce8adc30d6..6b0adcb52cf 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -74,6 +74,7 @@ class AdaxDevice(ClimateEntity): ) _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None: """Initialize the heater.""" From 72f1d8ec15714122530683d0ae8adf511bb44fd5 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 2 Feb 2024 03:10:51 -0500 Subject: [PATCH 018/125] Add Duquesne Light virtual integration supported by opower (#109272) Co-authored-by: Dan Swartz <3066652+swartzd@users.noreply.github.com> --- homeassistant/components/duquesne_light/__init__.py | 1 + homeassistant/components/duquesne_light/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/duquesne_light/__init__.py create mode 100644 homeassistant/components/duquesne_light/manifest.json diff --git a/homeassistant/components/duquesne_light/__init__.py b/homeassistant/components/duquesne_light/__init__.py new file mode 100644 index 00000000000..33c35ecb4cd --- /dev/null +++ b/homeassistant/components/duquesne_light/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Duquesne Light.""" diff --git a/homeassistant/components/duquesne_light/manifest.json b/homeassistant/components/duquesne_light/manifest.json new file mode 100644 index 00000000000..3cb01757950 --- /dev/null +++ b/homeassistant/components/duquesne_light/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "duquesne_light", + "name": "Duquesne Light", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 21186272bb6..9ae87cbd706 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1342,6 +1342,11 @@ "config_flow": true, "iot_class": "local_push" }, + "duquesne_light": { + "name": "Duquesne Light", + "integration_type": "virtual", + "supported_by": "opower" + }, "dwd_weather_warnings": { "name": "Deutscher Wetterdienst (DWD) Weather Warnings", "integration_type": "service", From d5d74005f42178885610af117d6874373d906732 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:31:09 +0100 Subject: [PATCH 019/125] Add migrated ClimateEntityFeature for Nibe Heat Pump (#109140) --- homeassistant/components/nibe_heatpump/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nibe_heatpump/climate.py b/homeassistant/components/nibe_heatpump/climate.py index 38a3a5f825c..3a89f4f6022 100644 --- a/homeassistant/components/nibe_heatpump/climate.py +++ b/homeassistant/components/nibe_heatpump/climate.py @@ -72,6 +72,7 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): _attr_target_temperature_step = 0.5 _attr_max_temp = 35.0 _attr_min_temp = 5.0 + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 2788576dc956fed01ec0bdb5939f045a5148be69 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:31:35 +0100 Subject: [PATCH 020/125] Add TURN_ON/OFF ClimateEntityFeature for Modbus (#109133) --- homeassistant/components/modbus/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 71c01d20205..637478fffd4 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -97,7 +97,12 @@ async def async_setup_platform( class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): """Representation of a Modbus Thermostat.""" - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 67e6febde47b6ef3c830a2034a427d3ed55a44fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 02:32:17 -0600 Subject: [PATCH 021/125] Ensure the purge entities service cleans up the states_meta table (#109344) --- homeassistant/components/recorder/purge.py | 2 ++ tests/components/recorder/test_purge.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 8dd539f84f3..0b63bb8daa2 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -794,4 +794,6 @@ def purge_entity_data( _LOGGER.debug("Purging entity data hasn't fully completed yet") return False + _purge_old_entity_ids(instance, session) + return True diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1696c9018b4..2a9260a28a4 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1424,6 +1424,18 @@ async def test_purge_entities( ) assert states_sensor_kept.count() == 10 + # sensor.keep should remain in the StatesMeta table + states_meta_remain = session.query(StatesMeta).filter( + StatesMeta.entity_id == "sensor.keep" + ) + assert states_meta_remain.count() == 1 + + # sensor.purge_entity should be removed from the StatesMeta table + states_meta_remain = session.query(StatesMeta).filter( + StatesMeta.entity_id == "sensor.purge_entity" + ) + assert states_meta_remain.count() == 0 + _add_purge_records(hass) # Confirm calling service without arguments matches all records (default filter behavior) @@ -1437,6 +1449,10 @@ async def test_purge_entities( states = session.query(States) assert states.count() == 0 + # The states_meta table should be empty + states_meta_remain = session.query(StatesMeta) + assert states_meta_remain.count() == 0 + async def _add_test_states(hass: HomeAssistant, wait_recording_done: bool = True): """Add multiple states to the db for testing.""" From 8a22a8d4ba275cb93a047fd9feef71b085a4ad76 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:32:50 +0100 Subject: [PATCH 022/125] Add migrated ClimateEntityFeature for Atag (#108961) --- homeassistant/components/atag/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 9b2729f141e..a5f119e3a2b 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -46,6 +46,7 @@ class AtagThermostat(AtagEntity, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, atag_id): """Initialize an Atag climate device.""" From b379e9db60ec8dde7689773feb497267e29a1d3e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:33:13 +0100 Subject: [PATCH 023/125] Add migrated ClimateEntityFeature for SwitchBot Cloud (#109136) --- homeassistant/components/switchbot_cloud/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switchbot_cloud/climate.py b/homeassistant/components/switchbot_cloud/climate.py index 803669c806d..d184063939a 100644 --- a/homeassistant/components/switchbot_cloud/climate.py +++ b/homeassistant/components/switchbot_cloud/climate.py @@ -80,6 +80,7 @@ class SwitchBotCloudAirConditionner(SwitchBotCloudEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature = 21 _attr_name = None + _enable_turn_on_off_backwards_compatibility = False async def _do_send_command( self, From 596f61ff07748e1ee896795a8032949fda7d13f3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:33:54 +0100 Subject: [PATCH 024/125] Add TURN_ON/OFF ClimateEntityFeature for Fibaro (#108963) --- homeassistant/components/fibaro/climate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 18fef8dbe7a..42b8a5c0446 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -126,6 +126,8 @@ async def async_setup_entry( class FibaroThermostat(FibaroDevice, ClimateEntity): """Representation of a Fibaro Thermostat.""" + _enable_turn_on_off_backwards_compatibility = False + def __init__(self, fibaro_device: DeviceModel) -> None: """Initialize the Fibaro device.""" super().__init__(fibaro_device) @@ -209,6 +211,11 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): if mode in OPMODES_PRESET: self._attr_preset_modes.append(OPMODES_PRESET[mode]) + if HVACMode.OFF in self._attr_hvac_modes and len(self._attr_hvac_modes) > 1: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" _LOGGER.debug( From 1f466e737e53e105595aa288d780d073b5359d54 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Feb 2024 09:34:43 +0100 Subject: [PATCH 025/125] Use send_json_auto_id in recorder tests (#109355) --- tests/components/recorder/test_statistics.py | 3 +- .../components/recorder/test_websocket_api.py | 291 ++++++------------ 2 files changed, 100 insertions(+), 194 deletions(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 69b7f9316f7..00ffdc21b81 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -785,9 +785,8 @@ async def test_import_statistics( } # Adjust the statistics in a different unit - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 323b81211d7..e902dd49020 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -156,9 +156,8 @@ async def test_statistics_during_period( await async_wait_recording_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "end_time": now.isoformat(), @@ -170,9 +169,8 @@ async def test_statistics_during_period( assert response["success"] assert response["result"] == {} - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -194,9 +192,8 @@ async def test_statistics_during_period( ] } - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -226,13 +223,6 @@ async def test_statistic_during_period( offset, ) -> None: """Test statistic_during_period.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - now = dt_util.utcnow() await async_recorder_block_till_done(hass) @@ -313,9 +303,8 @@ async def test_statistic_during_period( assert run_cache.get_latest_ids({metadata_id}) is not None # No data for this period yet - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": now.isoformat(), @@ -334,9 +323,8 @@ async def test_statistic_during_period( } # This should include imported_statistics_5min[:] - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", } @@ -359,9 +347,8 @@ async def test_statistic_during_period( dt_util.parse_datetime("2022-10-21T07:15:00+00:00") + timedelta(minutes=5 * offset) ).isoformat() - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -388,9 +375,8 @@ async def test_statistic_during_period( dt_util.parse_datetime("2022-10-21T08:20:00+00:00") + timedelta(minutes=5 * offset) ).isoformat() - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -414,9 +400,8 @@ async def test_statistic_during_period( + timedelta(minutes=5 * offset) ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == start_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": start_time, @@ -438,9 +423,8 @@ async def test_statistic_during_period( dt_util.parse_datetime("2022-10-21T06:09:00+00:00") + timedelta(minutes=5 * offset) ).isoformat() - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": start_time, @@ -463,9 +447,8 @@ async def test_statistic_during_period( + timedelta(minutes=5 * offset) ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "end_time": end_time, @@ -493,9 +476,8 @@ async def test_statistic_during_period( + timedelta(minutes=5 * offset) ).isoformat() assert imported_stats_5min[32]["start"].isoformat() == end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": start_time, @@ -517,9 +499,8 @@ async def test_statistic_during_period( start_time = "2022-10-21T06:00:00+00:00" assert imported_stats_5min[24 - offset]["start"].isoformat() == start_time assert imported_stats[2]["start"].isoformat() == start_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": start_time, @@ -538,9 +519,8 @@ async def test_statistic_during_period( } # This should also include imported_statistics[2:] + imported_statistics_5min[36:] - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "rolling_window": { "duration": {"hours": 1, "minutes": 25}, @@ -559,9 +539,8 @@ async def test_statistic_during_period( } # This should include imported_statistics[2:3] - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "rolling_window": { "duration": {"hours": 1}, @@ -585,9 +564,8 @@ async def test_statistic_during_period( } # Test we can get only selected types - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "types": ["max", "change"], @@ -601,9 +579,8 @@ async def test_statistic_during_period( } # Test we can convert units - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "units": {"energy": "MWh"}, @@ -621,9 +598,8 @@ async def test_statistic_during_period( # Test we can automatically convert units hass.states.async_set("sensor.test", None, attributes=ENERGY_SENSOR_WH_ATTRIBUTES) - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", } @@ -707,9 +683,8 @@ async def test_statistic_during_period_hole( await async_wait_recording_done(hass) # This should include imported_stats[:] - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", } @@ -728,9 +703,8 @@ async def test_statistic_during_period_hole( end_time = "2022-10-21T05:00:00+00:00" assert imported_stats[0]["start"].isoformat() == start_time assert imported_stats[-1]["start"].isoformat() < end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -751,9 +725,8 @@ async def test_statistic_during_period_hole( # This should also include imported_stats[:] start_time = "2022-10-20T13:00:00+00:00" end_time = "2022-10-21T08:20:00+00:00" - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -776,9 +749,8 @@ async def test_statistic_during_period_hole( end_time = "2022-10-20T23:00:00+00:00" assert imported_stats[1]["start"].isoformat() == start_time assert imported_stats[3]["start"].isoformat() < end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -801,9 +773,8 @@ async def test_statistic_during_period_hole( end_time = "2022-10-21T00:00:00+00:00" assert imported_stats[1]["start"].isoformat() > start_time assert imported_stats[3]["start"].isoformat() < end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -894,9 +865,8 @@ async def test_statistic_during_period_calendar( "homeassistant.components.recorder.websocket_api.statistic_during_period", return_value={}, ) as statistic_during_period: - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistic_during_period", "calendar": calendar_period, "statistic_id": "sensor.test", @@ -956,9 +926,8 @@ async def test_statistics_during_period_unit_conversion( client = await hass_ws_client() # Query in state unit - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -981,9 +950,8 @@ async def test_statistics_during_period_unit_conversion( } # Query in custom unit - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1044,9 +1012,8 @@ async def test_sum_statistics_during_period_unit_conversion( client = await hass_ws_client() # Query in state unit - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1069,9 +1036,8 @@ async def test_sum_statistics_during_period_unit_conversion( } # Query in custom unit - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1121,9 +1087,8 @@ async def test_statistics_during_period_invalid_unit_conversion( client = await hass_ws_client() # Query in state unit - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1135,9 +1100,8 @@ async def test_statistics_during_period_invalid_unit_conversion( assert response["result"] == {} # Query in custom unit - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1176,9 +1140,8 @@ async def test_statistics_during_period_in_the_past( await async_wait_recording_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "end_time": now.isoformat(), @@ -1190,9 +1153,8 @@ async def test_statistics_during_period_in_the_past( assert response["success"] assert response["result"] == {} - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1204,9 +1166,8 @@ async def test_statistics_during_period_in_the_past( assert response["result"] == {} past = now - timedelta(days=3, hours=1) - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/statistics_during_period", "start_time": past.isoformat(), "statistic_ids": ["sensor.test"], @@ -1229,9 +1190,8 @@ async def test_statistics_during_period_in_the_past( } start_of_day = stats_top_of_hour.replace(hour=0, minute=0) - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/statistics_during_period", "start_time": stats_top_of_hour.isoformat(), "statistic_ids": ["sensor.test"], @@ -1253,9 +1213,8 @@ async def test_statistics_during_period_in_the_past( ] } - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1272,9 +1231,8 @@ async def test_statistics_during_period_bad_start_time( ) -> None: """Test statistics_during_period.""" client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": "cats", "statistic_ids": ["sensor.test"], @@ -1293,9 +1251,8 @@ async def test_statistics_during_period_bad_end_time( now = dt_util.utcnow() client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "end_time": "dogs", @@ -1315,9 +1272,8 @@ async def test_statistics_during_period_no_statistic_ids( now = dt_util.utcnow() client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "end_time": (now + timedelta(seconds=1)).isoformat(), @@ -1336,9 +1292,8 @@ async def test_statistics_during_period_empty_statistic_ids( now = dt_util.utcnow() client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": [], @@ -1428,7 +1383,7 @@ async def test_list_statistic_ids( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [] @@ -1436,7 +1391,7 @@ async def test_list_statistic_ids( hass.states.async_set("sensor.test", 10, attributes=attributes) await async_wait_recording_done(hass) - await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1458,7 +1413,7 @@ async def test_list_statistic_ids( hass.states.async_remove("sensor.test") await hass.async_block_till_done() - await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1474,14 +1429,14 @@ async def test_list_statistic_ids( } ] - await client.send_json( - {"id": 4, "type": "recorder/list_statistic_ids", "statistic_type": "dogs"} + await client.send_json_auto_id( + {"type": "recorder/list_statistic_ids", "statistic_type": "dogs"} ) response = await client.receive_json() assert not response["success"] - await client.send_json( - {"id": 5, "type": "recorder/list_statistic_ids", "statistic_type": "mean"} + await client.send_json_auto_id( + {"type": "recorder/list_statistic_ids", "statistic_type": "mean"} ) response = await client.receive_json() assert response["success"] @@ -1501,8 +1456,8 @@ async def test_list_statistic_ids( else: assert response["result"] == [] - await client.send_json( - {"id": 6, "type": "recorder/list_statistic_ids", "statistic_type": "sum"} + await client.send_json_auto_id( + {"type": "recorder/list_statistic_ids", "statistic_type": "sum"} ) response = await client.receive_json() assert response["success"] @@ -1591,7 +1546,7 @@ async def test_list_statistic_ids_unit_change( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [] @@ -1602,7 +1557,7 @@ async def test_list_statistic_ids_unit_change( do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) - await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1621,7 +1576,7 @@ async def test_list_statistic_ids_unit_change( # Change the state unit hass.states.async_set("sensor.test", 10, attributes=attributes2) - await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1642,17 +1597,9 @@ async def test_validate_statistics( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test validate_statistics can be called.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) + await client.send_json_auto_id({"type": "recorder/validate_statistics"}) response = await client.receive_json() assert response["success"] assert response["result"] == expected_result @@ -1685,9 +1632,8 @@ async def test_clear_statistics( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"], @@ -1730,9 +1676,8 @@ async def test_clear_statistics( } assert response["result"] == expected_response - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/clear_statistics", "statistic_ids": ["sensor.test"], } @@ -1742,9 +1687,8 @@ async def test_clear_statistics( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/statistics_during_period", "statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"], "start_time": now.isoformat(), @@ -1755,9 +1699,8 @@ async def test_clear_statistics( assert response["success"] assert response["result"] == expected_response - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/clear_statistics", "statistic_ids": ["sensor.test1", "sensor.test3"], } @@ -1767,9 +1710,8 @@ async def test_clear_statistics( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/statistics_during_period", "statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"], "start_time": now.isoformat(), @@ -1811,7 +1753,7 @@ async def test_update_statistics_metadata( client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1827,9 +1769,8 @@ async def test_update_statistics_metadata( } ] - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/update_statistics_metadata", "statistic_id": "sensor.test", "unit_of_measurement": new_unit, @@ -1839,7 +1780,7 @@ async def test_update_statistics_metadata( assert response["success"] await async_recorder_block_till_done(hass) - await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1855,9 +1796,8 @@ async def test_update_statistics_metadata( } ] - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1902,7 +1842,7 @@ async def test_change_statistics_unit( client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1918,9 +1858,8 @@ async def test_change_statistics_unit( } ] - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1942,9 +1881,8 @@ async def test_change_statistics_unit( ], } - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/change_statistics_unit", "statistic_id": "sensor.test", "new_unit_of_measurement": "W", @@ -1955,7 +1893,7 @@ async def test_change_statistics_unit( assert response["success"] await async_recorder_block_till_done(hass) - await client.send_json({"id": 4, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1971,9 +1909,8 @@ async def test_change_statistics_unit( } ] - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1997,9 +1934,8 @@ async def test_change_statistics_unit( } # Changing to the same unit is allowed but does nothing - await client.send_json( + await client.send_json_auto_id( { - "id": 6, "type": "recorder/change_statistics_unit", "statistic_id": "sensor.test", "new_unit_of_measurement": "W", @@ -2010,7 +1946,7 @@ async def test_change_statistics_unit( assert response["success"] await async_recorder_block_till_done(hass) - await client.send_json({"id": 7, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -2035,7 +1971,6 @@ async def test_change_statistics_unit_errors( ) -> None: """Test change unit of recorded statistics.""" now = dt_util.utcnow() - ws_id = 0 units = METRIC_SYSTEM attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None} @@ -2068,19 +2003,14 @@ async def test_change_statistics_unit_errors( } async def assert_statistic_ids(expected): - nonlocal ws_id - ws_id += 1 - await client.send_json({"id": ws_id, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == expected async def assert_statistics(expected): - nonlocal ws_id - ws_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": ws_id, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -2106,10 +2036,8 @@ async def test_change_statistics_unit_errors( await assert_statistics(expected_statistics) # Try changing to an invalid unit - ws_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": ws_id, "type": "recorder/change_statistics_unit", "statistic_id": "sensor.test", "old_unit_of_measurement": "kW", @@ -2126,10 +2054,8 @@ async def test_change_statistics_unit_errors( await assert_statistics(expected_statistics) # Try changing from the wrong unit - ws_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": ws_id, "type": "recorder/change_statistics_unit", "statistic_id": "sensor.test", "old_unit_of_measurement": "W", @@ -2155,7 +2081,7 @@ async def test_recorder_info( # Ensure there are no queued events await async_wait_recording_done(hass) - await client.send_json({"id": 1, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert response["success"] assert response["result"] == { @@ -2174,7 +2100,7 @@ async def test_recorder_info_no_recorder( """Test getting recorder status when recorder is not present.""" client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "unknown_command" @@ -2199,7 +2125,7 @@ async def test_recorder_info_bad_recorder_config( # Wait for recorder to shut down await hass.async_add_executor_job(recorder.get_instance(hass).join) - await client.send_json({"id": 1, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert response["success"] assert response["result"]["recording"] is False @@ -2250,7 +2176,7 @@ async def test_recorder_info_migration_queue_exhausted( client = await hass_ws_client() # Check the status - await client.send_json({"id": 1, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert response["success"] assert response["result"]["migration_in_progress"] is True @@ -2262,7 +2188,7 @@ async def test_recorder_info_migration_queue_exhausted( await async_wait_recording_done(hass) # Check the status after migration finished - await client.send_json({"id": 2, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert response["success"] assert response["result"]["migration_in_progress"] is False @@ -2278,7 +2204,7 @@ async def test_backup_start_no_recorder( """Test getting backup start when recorder is not present.""" client = await hass_ws_client(hass, hass_supervisor_access_token) - await client.send_json({"id": 1, "type": "backup/start"}) + await client.send_json_auto_id({"type": "backup/start"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "unknown_command" @@ -2303,12 +2229,12 @@ async def test_backup_start_timeout( with patch.object(recorder.core, "DB_LOCK_TIMEOUT", 0): try: - await client.send_json({"id": 1, "type": "backup/start"}) + await client.send_json_auto_id({"type": "backup/start"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "timeout_error" finally: - await client.send_json({"id": 2, "type": "backup/end"}) + await client.send_json_auto_id({"type": "backup/end"}) async def test_backup_end( @@ -2323,11 +2249,11 @@ async def test_backup_end( # Ensure there are no queued events await async_wait_recording_done(hass) - await client.send_json({"id": 1, "type": "backup/start"}) + await client.send_json_auto_id({"type": "backup/start"}) response = await client.receive_json() assert response["success"] - await client.send_json({"id": 2, "type": "backup/end"}) + await client.send_json_auto_id({"type": "backup/end"}) response = await client.receive_json() assert response["success"] @@ -2349,7 +2275,7 @@ async def test_backup_end_without_start( # Ensure there are no queued events await async_wait_recording_done(hass) - await client.send_json({"id": 1, "type": "backup/end"}) + await client.send_json_auto_id({"type": "backup/end"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "database_unlock_failed" @@ -2393,7 +2319,7 @@ async def test_get_statistics_metadata( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/get_statistics_metadata"}) + await client.send_json_auto_id({"type": "recorder/get_statistics_metadata"}) response = await client.receive_json() assert response["success"] assert response["result"] == [] @@ -2442,9 +2368,8 @@ async def test_get_statistics_metadata( ) await async_wait_recording_done(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/get_statistics_metadata", "statistic_ids": ["test:total_gas"], } @@ -2470,9 +2395,8 @@ async def test_get_statistics_metadata( hass.states.async_set("sensor.test2", 10, attributes=attributes) await async_wait_recording_done(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/get_statistics_metadata", "statistic_ids": ["sensor.test"], } @@ -2498,9 +2422,8 @@ async def test_get_statistics_metadata( hass.states.async_remove("sensor.test") await hass.async_block_till_done() - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/get_statistics_metadata", "statistic_ids": ["sensor.test"], } @@ -2568,9 +2491,8 @@ async def test_import_statistics( "unit_of_measurement": "kWh", } - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [imported_statistics1, imported_statistics2], @@ -2656,9 +2578,8 @@ async def test_import_statistics( "sum": 6, } - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [external_statistics], @@ -2702,9 +2623,8 @@ async def test_import_statistics( "sum": 5, } - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [external_statistics], @@ -2785,9 +2705,8 @@ async def test_adjust_sum_statistics_energy( "unit_of_measurement": "kWh", } - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [imported_statistics1, imported_statistics2], @@ -2852,9 +2771,8 @@ async def test_adjust_sum_statistics_energy( } # Adjust previously inserted statistics in kWh - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -2893,9 +2811,8 @@ async def test_adjust_sum_statistics_energy( } # Adjust previously inserted statistics in MWh - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -2981,9 +2898,8 @@ async def test_adjust_sum_statistics_gas( "unit_of_measurement": "m³", } - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [imported_statistics1, imported_statistics2], @@ -3048,9 +2964,8 @@ async def test_adjust_sum_statistics_gas( } # Adjust previously inserted statistics in m³ - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -3089,9 +3004,8 @@ async def test_adjust_sum_statistics_gas( } # Adjust previously inserted statistics in ft³ - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -3194,9 +3108,8 @@ async def test_adjust_sum_statistics_errors( "unit_of_measurement": statistic_unit, } - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [imported_statistics1, imported_statistics2], @@ -3262,10 +3175,8 @@ async def test_adjust_sum_statistics_errors( } # Try to adjust statistics - msg_id = 2 - await client.send_json( + await client.send_json_auto_id( { - "id": msg_id, "type": "recorder/adjust_sum_statistics", "statistic_id": "sensor.does_not_exist", "start_time": period2.isoformat(), @@ -3282,10 +3193,8 @@ async def test_adjust_sum_statistics_errors( assert stats == previous_stats for unit in invalid_units: - msg_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": msg_id, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -3302,10 +3211,8 @@ async def test_adjust_sum_statistics_errors( assert stats == previous_stats for unit in valid_units: - msg_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": msg_id, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), From 025fe51322824d34fc3f69b3c6bab63d5a183b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 2 Feb 2024 09:36:26 +0100 Subject: [PATCH 026/125] Use a mocked API client in Traccar Server tests (#109358) --- tests/components/traccar_server/conftest.py | 77 ++++++++++- .../traccar_server/fixtures/devices.json | 17 +++ .../traccar_server/fixtures/geofences.json | 10 ++ .../traccar_server/fixtures/positions.json | 24 ++++ .../fixtures/reports_events.json | 12 ++ .../traccar_server/fixtures/server.json | 21 +++ .../traccar_server/test_config_flow.py | 124 ++++++++---------- 7 files changed, 210 insertions(+), 75 deletions(-) create mode 100644 tests/components/traccar_server/fixtures/devices.json create mode 100644 tests/components/traccar_server/fixtures/geofences.json create mode 100644 tests/components/traccar_server/fixtures/positions.json create mode 100644 tests/components/traccar_server/fixtures/reports_events.json create mode 100644 tests/components/traccar_server/fixtures/server.json diff --git a/tests/components/traccar_server/conftest.py b/tests/components/traccar_server/conftest.py index 4141b28849c..10cc6192d38 100644 --- a/tests/components/traccar_server/conftest.py +++ b/tests/components/traccar_server/conftest.py @@ -3,12 +3,79 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from pytraccar import ApiClient + +from homeassistant.components.traccar_server.const import ( + CONF_CUSTOM_ATTRIBUTES, + CONF_EVENTS, + CONF_MAX_ACCURACY, + CONF_SKIP_ACCURACY_FILTER_FOR, + DOMAIN, +) +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: - """Override async_setup_entry.""" +def mock_traccar_api_client() -> Generator[AsyncMock, None, None]: + """Mock a Traccar ApiClient client.""" with patch( - "homeassistant.components.traccar_server.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry + "homeassistant.components.traccar_server.ApiClient", + autospec=True, + ) as mock_client, patch( + "homeassistant.components.traccar_server.config_flow.ApiClient", + new=mock_client, + ): + client: ApiClient = mock_client.return_value + client.get_devices.return_value = load_json_array_fixture( + "traccar_server/devices.json" + ) + client.get_geofences.return_value = load_json_array_fixture( + "traccar_server/geofences.json" + ) + client.get_positions.return_value = load_json_array_fixture( + "traccar_server/positions.json" + ) + client.get_server.return_value = load_json_object_fixture( + "traccar_server/server.json" + ) + client.get_reports_events.return_value = load_json_array_fixture( + "traccar_server/reports_events.json" + ) + + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a Traccar Server config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="1.1.1.1:8082", + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: "8082", + CONF_USERNAME: "test@example.org", + CONF_PASSWORD: "ThisIsNotThePasswordYouAreL00kingFor", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + }, + options={ + CONF_CUSTOM_ATTRIBUTES: ["custom_attr_1"], + CONF_EVENTS: ["device_moving"], + CONF_MAX_ACCURACY: 5.0, + CONF_SKIP_ACCURACY_FILTER_FOR: [], + }, + ) diff --git a/tests/components/traccar_server/fixtures/devices.json b/tests/components/traccar_server/fixtures/devices.json new file mode 100644 index 00000000000..b04d53d9fdf --- /dev/null +++ b/tests/components/traccar_server/fixtures/devices.json @@ -0,0 +1,17 @@ +[ + { + "id": 0, + "name": "X-Wing", + "uniqueId": "abc123", + "status": "unknown", + "disabled": false, + "lastUpdate": "1970-01-01T00:00:00Z", + "positionId": 0, + "groupId": 0, + "phone": null, + "model": "1337", + "contact": null, + "category": "starfighter", + "attributes": {} + } +] diff --git a/tests/components/traccar_server/fixtures/geofences.json b/tests/components/traccar_server/fixtures/geofences.json new file mode 100644 index 00000000000..5452c0485de --- /dev/null +++ b/tests/components/traccar_server/fixtures/geofences.json @@ -0,0 +1,10 @@ +[ + { + "id": 0, + "name": "Tatooine", + "description": "A harsh desert world orbiting twin suns in the galaxy's Outer Rim", + "area": "string", + "calendarId": 0, + "attributes": {} + } +] diff --git a/tests/components/traccar_server/fixtures/positions.json b/tests/components/traccar_server/fixtures/positions.json new file mode 100644 index 00000000000..6b65116e804 --- /dev/null +++ b/tests/components/traccar_server/fixtures/positions.json @@ -0,0 +1,24 @@ +[ + { + "id": 0, + "deviceId": 0, + "protocol": "C-3PO", + "deviceTime": "1970-01-01T00:00:00Z", + "fixTime": "1970-01-01T00:00:00Z", + "serverTime": "1970-01-01T00:00:00Z", + "outdated": true, + "valid": true, + "latitude": 52.0, + "longitude": 25.0, + "altitude": 546841384638, + "speed": 4568795, + "course": 360, + "address": "Mos Espa", + "accuracy": 3.5, + "network": {}, + "geofenceIds": [0], + "attributes": { + "custom_attr_1": "custom_attr_1_value" + } + } +] diff --git a/tests/components/traccar_server/fixtures/reports_events.json b/tests/components/traccar_server/fixtures/reports_events.json new file mode 100644 index 00000000000..e8280471d96 --- /dev/null +++ b/tests/components/traccar_server/fixtures/reports_events.json @@ -0,0 +1,12 @@ +[ + { + "id": 0, + "type": "deviceMoving", + "eventTime": "2019-08-24T14:15:22Z", + "deviceId": 0, + "positionId": 0, + "geofenceId": 0, + "maintenanceId": 0, + "attributes": {} + } +] diff --git a/tests/components/traccar_server/fixtures/server.json b/tests/components/traccar_server/fixtures/server.json new file mode 100644 index 00000000000..039b6bfa1f4 --- /dev/null +++ b/tests/components/traccar_server/fixtures/server.json @@ -0,0 +1,21 @@ +{ + "id": 0, + "registration": true, + "readonly": true, + "deviceReadonly": true, + "limitCommands": true, + "map": null, + "bingKey": null, + "mapUrl": null, + "poiLayer": null, + "latitude": 0, + "longitude": 0, + "zoom": 0, + "twelveHourFormat": true, + "version": "99.99", + "forceSettings": true, + "coordinateFormat": null, + "openIdEnabled": true, + "openIdForce": true, + "attributes": {} +} diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index 028bc99cec5..00a987a4711 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Traccar Server config flow.""" +from collections.abc import Generator from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock import pytest from pytraccar import TraccarException @@ -29,7 +30,10 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: +async def test_form( + hass: HomeAssistant, + mock_traccar_api_client: Generator[AsyncMock, None, None], +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -37,19 +41,15 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.traccar_server.config_flow.ApiClient.get_server", - return_value={"id": "1234"}, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "1.1.1.1:8082" @@ -61,7 +61,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: CONF_SSL: False, CONF_VERIFY_SSL: True, } - assert len(mock_setup_entry.mock_calls) == 1 + assert result["result"].state == config_entries.ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -73,44 +73,40 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) async def test_form_cannot_connect( hass: HomeAssistant, - mock_setup_entry: AsyncMock, side_effect: Exception, error: str, + mock_traccar_api_client: Generator[AsyncMock, None, None], ) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.traccar_server.config_flow.ApiClient.get_server", - side_effect=side_effect, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) + mock_traccar_api_client.get_server.side_effect = side_effect + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} - with patch( - "homeassistant.components.traccar_server.config_flow.ApiClient.get_server", - return_value={"id": "1234"}, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() + mock_traccar_api_client.get_server.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "1.1.1.1:8082" @@ -122,27 +118,23 @@ async def test_form_cannot_connect( CONF_SSL: False, CONF_VERIFY_SSL: True, } - assert len(mock_setup_entry.mock_calls) == 1 + + assert result["result"].state == config_entries.ConfigEntryState.LOADED async def test_options( hass: HomeAssistant, - mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_traccar_api_client: Generator[AsyncMock, None, None], ) -> None: """Test options flow.""" + mock_config_entry.add_to_hass(hass) - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - config_entry.add_to_hass(hass) + assert mock_config_entry.options.get(CONF_MAX_ACCURACY) == 5.0 - assert await hass.config_entries.async_setup(config_entry.entry_id) - - assert CONF_MAX_ACCURACY not in config_entry.options - - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -151,7 +143,7 @@ async def test_options( await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert config_entry.options == { + assert mock_config_entry.options == { CONF_MAX_ACCURACY: 2.0, CONF_EVENTS: [], CONF_CUSTOM_ATTRIBUTES: [], @@ -234,10 +226,10 @@ async def test_options( ) async def test_import_from_yaml( hass: HomeAssistant, - mock_setup_entry: AsyncMock, imported: dict[str, Any], data: dict[str, Any], options: dict[str, Any], + mock_traccar_api_client: Generator[AsyncMock, None, None], ) -> None: """Test importing configuration from YAML.""" result = await hass.config_entries.flow.async_init( @@ -249,12 +241,10 @@ async def test_import_from_yaml( assert result["title"] == f"{data[CONF_HOST]}:{data[CONF_PORT]}" assert result["data"] == data assert result["options"] == options + assert result["result"].state == config_entries.ConfigEntryState.LOADED -async def test_abort_import_already_configured( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, -) -> None: +async def test_abort_import_already_configured(hass: HomeAssistant) -> None: """Test abort for existing server while importing.""" config_entry = MockConfigEntry( @@ -284,18 +274,12 @@ async def test_abort_import_already_configured( async def test_abort_already_configured( hass: HomeAssistant, - mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_traccar_api_client: Generator[AsyncMock, None, None], ) -> None: """Test abort for existing server.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "1.1.1.1", CONF_PORT: "8082"}, - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} From 543870d5f122757832f7463f2731a76a78774ffd Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 2 Feb 2024 09:46:53 +0100 Subject: [PATCH 027/125] Correct modbus commit validation, too strict on integers (#109338) --- .../components/modbus/base_platform.py | 18 ++++++++---------- tests/components/modbus/test_sensor.py | 10 +++++----- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 877d33afbcc..cdc1e7a6986 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -182,7 +182,6 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): self._data_type = config[CONF_DATA_TYPE] self._structure: str = config[CONF_STRUCTURE] self._scale = config[CONF_SCALE] - self._precision = config.get(CONF_PRECISION, 2) self._offset = config[CONF_OFFSET] self._slave_count = config.get(CONF_SLAVE_COUNT, None) or config.get( CONF_VIRTUAL_COUNT, 0 @@ -196,11 +195,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): DataType.UINT32, DataType.UINT64, ) - if self._value_is_int: - if self._min_value: - self._min_value = round(self._min_value) - if self._max_value: - self._max_value = round(self._max_value) + if not self._value_is_int: + self._precision = config.get(CONF_PRECISION, 2) + else: + self._precision = config.get(CONF_PRECISION, 0) def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: """Do swap as needed.""" @@ -235,13 +233,13 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): return None val: float | int = self._scale * entry + self._offset if self._min_value is not None and val < self._min_value: - return str(self._min_value) + val = self._min_value if self._max_value is not None and val > self._max_value: - return str(self._max_value) + val = self._max_value if self._zero_suppress is not None and abs(val) <= self._zero_suppress: return "0" - if self._precision == 0 or self._value_is_int: - return str(int(round(val, 0))) + if self._precision == 0: + return str(round(val)) return f"{float(val):.{self._precision}f}" def unpack_structure_result(self, registers: list[int]) -> str | None: diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 7c58290b143..97571041482 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -357,7 +357,7 @@ async def test_config_wrong_struct_sensor( }, [7], False, - "34", + "34.0000", ), ( { @@ -379,7 +379,7 @@ async def test_config_wrong_struct_sensor( }, [9], False, - "18", + "18.5", ), ( { @@ -390,7 +390,7 @@ async def test_config_wrong_struct_sensor( }, [1], False, - "2", + "2.40", ), ( { @@ -401,7 +401,7 @@ async def test_config_wrong_struct_sensor( }, [2], False, - "-8", + "-8.3", ), ( { @@ -676,7 +676,7 @@ async def test_config_wrong_struct_sensor( }, [0x00AB, 0xCDEF], False, - "112594", + "112593.75", ), ( { From e3f1997b6fdf5425ad476ec9cf0e372e9987f408 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:48:01 +0100 Subject: [PATCH 028/125] Add TURN_ON/OFF ClimateEntityFeature for KNX (#109138) --- homeassistant/components/knx/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 72039e1300f..1038cdde80f 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -134,12 +134,17 @@ class KNXClimate(KnxEntity, ClimateEntity): _device: XknxClimate _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX climate device.""" super().__init__(_create_climate(xknx, config)) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_ON + ) + if self._device.supports_on_off: + self._attr_supported_features |= ClimateEntityFeature.TURN_OFF if self.preset_modes: self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_target_temperature_step = self._device.temperature_step From 4229c35fcd11270dc23ad911fcac746f92f7d520 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Feb 2024 09:49:32 +0100 Subject: [PATCH 029/125] Improve color mode handling in MockLight (#109298) --- tests/components/group/test_light.py | 15 +++++++ tests/components/light/test_init.py | 45 +++++++++++++++++++ .../custom_components/test/light.py | 33 ++++++++++++-- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 3051ec502a0..59f0a5b7d55 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -279,6 +279,9 @@ async def test_brightness( entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None assert await async_setup_component( hass, @@ -350,6 +353,9 @@ async def test_color_hs(hass: HomeAssistant, enable_custom_integrations: None) - entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None assert await async_setup_component( hass, @@ -698,6 +704,9 @@ async def test_color_temp( entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None assert await async_setup_component( hass, @@ -838,6 +847,9 @@ async def test_min_max_mireds( entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity1._attr_min_color_temp_kelvin = 1 entity1._attr_max_color_temp_kelvin = 1234567890 @@ -1015,6 +1027,9 @@ async def test_supported_color_modes( entity2 = platform.ENTITIES[2] entity2.supported_features = SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None assert await async_setup_component( hass, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 69f6a841737..0e3bc1332cf 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -127,6 +127,9 @@ async def test_services( | light.LightEntityFeature.EFFECT | light.LightEntityFeature.TRANSITION ) + # Set color modes to none to trigger backwards compatibility in LightEntity + ent2.supported_color_modes = None + ent2.color_mode = None ent3.supported_features = ( light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION ) @@ -905,9 +908,15 @@ async def test_light_brightness_step( platform.ENTITIES.append(platform.MockLight("Test_1", STATE_ON)) entity0 = platform.ENTITIES[0] entity0.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity0.supported_color_modes = None + entity0.color_mode = None entity0.brightness = 100 entity1 = platform.ENTITIES[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity1.brightness = 50 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -967,6 +976,9 @@ async def test_light_brightness_pct_conversion( platform.init() entity = platform.ENTITIES[0] entity.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity.supported_color_modes = None + entity.color_mode = None entity.brightness = 100 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1133,17 +1145,29 @@ async def test_light_backwards_compatibility_supported_color_modes( entity1 = platform.ENTITIES[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity2 = platform.ENTITIES[2] entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None entity3 = platform.ENTITIES[3] entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None entity4 = platform.ENTITIES[4] entity4.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1204,20 +1228,32 @@ async def test_light_backwards_compatibility_color_mode( entity1 = platform.ENTITIES[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity1.brightness = 100 entity2 = platform.ENTITIES[2] entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None entity2.color_temp_kelvin = 10000 entity3 = platform.ENTITIES[3] entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None entity3.hs_color = (240, 100) entity4 = platform.ENTITIES[4] entity4.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity4.hs_color = (240, 100) entity4.color_temp_kelvin = 10000 @@ -1464,6 +1500,9 @@ async def test_light_service_call_color_conversion( entity4 = platform.ENTITIES[4] entity4.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity5 = platform.ENTITIES[5] entity5.supported_color_modes = {light.ColorMode.RGBW} @@ -1905,6 +1944,9 @@ async def test_light_service_call_color_conversion_named_tuple( entity4 = platform.ENTITIES[4] entity4.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity5 = platform.ENTITIES[5] entity5.supported_color_modes = {light.ColorMode.RGBW} @@ -2330,6 +2372,9 @@ async def test_light_state_color_conversion( entity3 = platform.ENTITIES[3] entity3.hs_color = (240, 100) entity3.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index e84e8cbe390..e22aca289a8 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -2,7 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.light import LightEntity +from homeassistant.components.light import ColorMode, LightEntity from homeassistant.const import STATE_OFF, STATE_ON from tests.common import MockToggleEntity @@ -32,13 +32,21 @@ async def async_setup_platform( async_add_entities_callback(ENTITIES) +TURN_ON_ARG_TO_COLOR_MODE = { + "hs_color": ColorMode.HS, + "xy_color": ColorMode.XY, + "rgb_color": ColorMode.RGB, + "rgbw_color": ColorMode.RGBW, + "rgbww_color": ColorMode.RGBWW, + "color_temp_kelvin": ColorMode.COLOR_TEMP, +} + + class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" - color_mode = None _attr_max_color_temp_kelvin = 6500 _attr_min_color_temp_kelvin = 2000 - supported_color_modes = None supported_features = 0 brightness = None @@ -49,6 +57,23 @@ class MockLight(MockToggleEntity, LightEntity): rgbww_color = None xy_color = None + def __init__( + self, + name, + state, + unique_id=None, + supported_color_modes: set[ColorMode] | None = None, + ): + """Initialize the mock light.""" + super().__init__(name, state, unique_id) + if supported_color_modes is None: + supported_color_modes = {ColorMode.ONOFF} + self._attr_supported_color_modes = supported_color_modes + color_mode = ColorMode.UNKNOWN + if len(supported_color_modes) == 1: + color_mode = next(iter(supported_color_modes)) + self._attr_color_mode = color_mode + def turn_on(self, **kwargs): """Turn the entity on.""" super().turn_on(**kwargs) @@ -65,3 +90,5 @@ class MockLight(MockToggleEntity, LightEntity): setattr(self, key, value) if key == "white": setattr(self, "brightness", value) + if key in TURN_ON_ARG_TO_COLOR_MODE: + self._attr_color_mode = TURN_ON_ARG_TO_COLOR_MODE[key] From effd5b8dddafed1504e525afb034dcd88e8cdace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Fri, 2 Feb 2024 10:24:53 +0100 Subject: [PATCH 030/125] Hide unsupported devices in Airthings BLE config flow (#107648) --- .../components/airthings_ble/config_flow.py | 10 +++++ tests/components/airthings_ble/__init__.py | 44 +++++++++++++++++++ .../airthings_ble/test_config_flow.py | 14 ++++++ 3 files changed, 68 insertions(+) diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py index b562e837ff4..4228fea50d7 100644 --- a/homeassistant/components/airthings_ble/config_flow.py +++ b/homeassistant/components/airthings_ble/config_flow.py @@ -23,6 +23,13 @@ from .const import DOMAIN, MFCT_ID _LOGGER = logging.getLogger(__name__) +SERVICE_UUIDS = [ + "b42e1f6e-ade7-11e4-89d3-123b93f75cba", + "b42e4a8e-ade7-11e4-89d3-123b93f75cba", + "b42e1c08-ade7-11e4-89d3-123b93f75cba", + "b42e3882-ade7-11e4-89d3-123b93f75cba", +] + @dataclasses.dataclass class Discovery: @@ -147,6 +154,9 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): if MFCT_ID not in discovery_info.manufacturer_data: continue + if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids): + continue + try: device = await self._get_device_data(discovery_info) except AirthingsDeviceUpdateError: diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index da0c312bf28..231ec12cb5f 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -93,6 +93,50 @@ WAVE_SERVICE_INFO = BluetoothServiceInfoBleak( time=0, ) +VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak( + name="cc-cc-cc-cc-cc-cc", + address="cc:cc:cc:cc:cc:cc", + device=generate_ble_device( + address="cc:cc:cc:cc:cc:cc", + name="Airthings View Plus", + ), + rssi=-61, + manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, + service_data={ + "b42eb4a6-ade7-11e4-89d3-123b93f75cba": bytearray( + b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0A" + ), + # Manufacturer + "00002a29-0000-1000-8000-00805f9b34fb": bytearray(b"Airthings AS"), + # Model + "00002a24-0000-1000-8000-00805f9b34fb": bytearray(b"2960"), + # Identifier + "00002a25-0000-1000-8000-00805f9b34fb": bytearray(b"123456"), + # SW Version + "00002a26-0000-1000-8000-00805f9b34fb": bytearray(b"A-BLE-1.12.1-master+0"), + # HW Version + "00002a27-0000-1000-8000-00805f9b34fb": bytearray(b"REV 1,0"), + }, + service_uuids=[ + "b42eb4a6-ade7-11e4-89d3-123b93f75cba", + "b42e90a2-ade7-11e4-89d3-123b93f75cba", + "b42e2a68-ade7-11e4-89d3-123b93f75cba", + "00002a29-0000-1000-8000-00805f9b34fb", + "00002a24-0000-1000-8000-00805f9b34fb", + "00002a25-0000-1000-8000-00805f9b34fb", + "00002a26-0000-1000-8000-00805f9b34fb", + "00002a27-0000-1000-8000-00805f9b34fb", + "b42e2d06-ade7-11e4-89d3-123b93f75cba", + ], + source="local", + advertisement=generate_advertisement_data( + manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, + service_uuids=["b42e90a2-ade7-11e4-89d3-123b93f75cba"], + ), + connectable=True, + time=0, +) + UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( name="unknown", address="00:cc:cc:cc:cc:cc", diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index bc009f03027..65ec91e69c2 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( UNKNOWN_SERVICE_INFO, + VIEW_PLUS_SERVICE_INFO, WAVE_DEVICE_INFO, WAVE_SERVICE_INFO, patch_airthings_ble, @@ -204,3 +205,16 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +async def test_unsupported_device(hass: HomeAssistant) -> None: + """Test the user initiated form with an unsupported device.""" + with patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[UNKNOWN_SERVICE_INFO, VIEW_PLUS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" From c868b79b5abbfa1021296ef671e553ee7657bc15 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 10:37:04 +0100 Subject: [PATCH 031/125] Update cryptography to 42.0.2 (#109359) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1b47b2693b0..fbb072db9c0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==42.0.1 +cryptography==42.0.2 dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 diff --git a/pyproject.toml b/pyproject.toml index d7680e5e871..24a50508722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "lru-dict==1.3.0", "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==42.0.1", + "cryptography==42.0.2", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==24.0.0", "orjson==3.9.12", diff --git a/requirements.txt b/requirements.txt index 67aad2e9f0d..066855e718b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ ifaddr==0.2.0 Jinja2==3.1.3 lru-dict==1.3.0 PyJWT==2.8.0 -cryptography==42.0.1 +cryptography==42.0.2 pyOpenSSL==24.0.0 orjson==3.9.12 packaging>=23.1 From f22b71d803840a073c82b25129cda21caeb4b1b3 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Fri, 2 Feb 2024 10:37:49 +0100 Subject: [PATCH 032/125] Follow up swiss_public_transport migration fix of unique ids (#107873) improve migration fix of unique ids - follow up to #107087 --- .../swiss_public_transport/__init__.py | 11 +++++++---- .../swiss_public_transport/test_init.py | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index a510b5b7414..d87b711e376 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -89,7 +89,9 @@ async def async_migrate_entry( device_registry, config_entry_id=config_entry.entry_id ) for dev in device_entries: - device_registry.async_remove_device(dev.id) + device_registry.async_update_device( + dev.id, remove_config_entry_id=config_entry.entry_id + ) entity_id = entity_registry.async_get_entity_id( Platform.SENSOR, DOMAIN, "None_departure" @@ -105,12 +107,13 @@ async def async_migrate_entry( ) # Set a valid unique id for config entries - config_entry.unique_id = new_unique_id config_entry.minor_version = 2 - hass.config_entries.async_update_entry(config_entry) + hass.config_entries.async_update_entry(config_entry, unique_id=new_unique_id) _LOGGER.debug( - "Migration to minor version %s successful", config_entry.minor_version + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, ) return True diff --git a/tests/components/swiss_public_transport/test_init.py b/tests/components/swiss_public_transport/test_init.py index f2b4e41ed71..2c8e12e04bf 100644 --- a/tests/components/swiss_public_transport/test_init.py +++ b/tests/components/swiss_public_transport/test_init.py @@ -45,25 +45,26 @@ CONNECTIONS = [ ] -async def test_migration_1_to_2( +async def test_migration_1_1_to_1_2( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test successful setup.""" + config_entry_faulty = MockConfigEntry( + domain=DOMAIN, + data=MOCK_DATA_STEP, + title="MIGRATION_TEST", + version=1, + minor_version=1, + ) + config_entry_faulty.add_to_hass(hass) + with patch( "homeassistant.components.swiss_public_transport.OpendataTransport", return_value=AsyncMock(), ) as mock: mock().connections = CONNECTIONS - config_entry_faulty = MockConfigEntry( - domain=DOMAIN, - data=MOCK_DATA_STEP, - title="MIGRATION_TEST", - minor_version=1, - ) - config_entry_faulty.add_to_hass(hass) - # Setup the config entry await hass.config_entries.async_setup(config_entry_faulty.entry_id) await hass.async_block_till_done() From 61e6882b91d4d1dc40fcfdc017b6842464cfe3bd Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Feb 2024 11:02:00 +0100 Subject: [PATCH 033/125] Bump deebot_client to 5.1.0 (#109360) --- homeassistant/components/ecovacs/config_flow.py | 6 +++++- homeassistant/components/ecovacs/controller.py | 2 +- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecovacs/test_init.py | 1 + 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index 39c61b3ce23..db3c60fa9e7 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -75,7 +75,7 @@ async def _validate_input( rest_config = create_rest_config( aiohttp_client.async_get_clientsession(hass), device_id=device_id, - country=country, + alpha_2_country=country, override_rest_url=rest_url, ) @@ -266,6 +266,10 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): # If not we will inform the user about the mismatch. error = None placeholders = None + + # Convert the country to upper case as ISO 3166-1 alpha-2 country codes are upper case + user_input[CONF_COUNTRY] = user_input[CONF_COUNTRY].upper() + if len(user_input[CONF_COUNTRY]) != 2: error = "invalid_country_length" placeholders = {"countries_url": "https://www.iso.org/obp/ui/#search/code/"} diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 27a1996c3e9..27b64db20b6 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -49,7 +49,7 @@ class EcovacsController: create_rest_config( aiohttp_client.async_get_clientsession(self._hass), device_id=self._device_id, - country=country, + alpha_2_country=country, override_rest_url=config.get(CONF_OVERRIDE_REST_URL), ), config[CONF_USERNAME], diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 3472e4746f8..34760ea6aca 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.8", "deebot-client==5.0.0"] + "requirements": ["py-sucks==0.9.8", "deebot-client==5.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b804e3720a..4aa0c0e08d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -684,7 +684,7 @@ debugpy==1.8.0 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==5.0.0 +deebot-client==5.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10c89637f25..8c0889cdf01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ dbus-fast==2.21.1 debugpy==1.8.0 # homeassistant.components.ecovacs -deebot-client==5.0.0 +deebot-client==5.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index 8557ccb983c..e76001fbaeb 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -80,6 +80,7 @@ async def test_invalid_auth( ({}, 0), ({DOMAIN: IMPORT_DATA.copy()}, 1), ], + ids=["no_config", "import_config"], ) async def test_async_setup_import( hass: HomeAssistant, From 03daeda9dbf07df1607c09fed24e94565d278497 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:28:51 +0100 Subject: [PATCH 034/125] Disable less interesting sensors by default in ViCare integration (#109014) --- homeassistant/components/vicare/sensor.py | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 39b4bd032dc..6c794b548ad 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -145,6 +145,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getDomesticHotWaterMaxTemperature(), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_min_temperature", @@ -153,6 +154,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getDomesticHotWaterMinTemperature(), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_consumption_today", @@ -167,6 +169,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_consumption_heating_this_month", @@ -174,6 +177,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_consumption_heating_this_year", @@ -181,6 +185,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_consumption_heating_today", @@ -195,6 +200,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_consumption_heating_this_month", @@ -202,6 +208,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_consumption_heating_this_year", @@ -209,6 +216,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionHeatingThisYear(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_consumption_fuelcell_today", @@ -287,6 +295,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentMonth(), unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_summary_consumption_heating_currentyear", @@ -295,6 +304,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentYear(), unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_summary_consumption_heating_lastsevendays", @@ -303,6 +313,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionHeatingLastSevenDays(), unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_summary_consumption_heating_currentday", @@ -319,6 +330,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentMonth(), unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_summary_consumption_heating_currentyear", @@ -327,6 +339,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentYear(), unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_summary_consumption_heating_lastsevendays", @@ -335,6 +348,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterLastSevenDays(), unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_summary_consumption_heating_currentday", @@ -351,6 +365,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentMonth(), unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_summary_consumption_heating_currentyear", @@ -359,6 +374,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentYear(), unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_summary_consumption_heating_lastsevendays", @@ -367,6 +383,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionHeatingLastSevenDays(), unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_dhw_summary_consumption_heating_currentday", @@ -383,6 +400,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentMonth(), unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_dhw_summary_consumption_heating_currentyear", @@ -391,6 +409,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentYear(), unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_summary_dhw_consumption_heating_lastsevendays", @@ -399,6 +418,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterLastSevenDays(), unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power_production_current", @@ -423,6 +443,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerProductionThisWeek(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power_production_this_month", @@ -431,6 +452,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerProductionThisMonth(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power_production_this_year", @@ -439,6 +461,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerProductionThisYear(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="solar storage temperature", @@ -473,6 +496,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getSolarPowerProductionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="solar power production this month", @@ -482,6 +506,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getSolarPowerProductionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="solar power production this year", @@ -491,6 +516,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getSolarPowerProductionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power consumption today", @@ -509,6 +535,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getPowerConsumptionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power consumption this month", @@ -518,6 +545,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getPowerConsumptionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power consumption this year", @@ -527,6 +555,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getPowerConsumptionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="buffer top temperature", @@ -615,6 +644,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass1(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_hours_loadclass2", @@ -623,6 +653,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass2(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_hours_loadclass3", @@ -631,6 +662,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass3(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_hours_loadclass4", @@ -639,6 +671,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass4(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_hours_loadclass5", @@ -647,6 +680,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass5(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_phase", From a452ad6454fb5119a3dc68b05cddb7060ec73882 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 11:33:03 +0100 Subject: [PATCH 035/125] Update sentry-sdk to 1.40.0 (#109363) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 3c3eaeb78e3..e0a2d5f75c4 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sentry", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["sentry-sdk==1.39.2"] + "requirements": ["sentry-sdk==1.40.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4aa0c0e08d6..2b1f9f96404 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2514,7 +2514,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.6.2 # homeassistant.components.sentry -sentry-sdk==1.39.2 +sentry-sdk==1.40.0 # homeassistant.components.sfr_box sfrbox-api==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c0889cdf01..8c09602bf8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1918,7 +1918,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.6.2 # homeassistant.components.sentry -sentry-sdk==1.39.2 +sentry-sdk==1.40.0 # homeassistant.components.sfr_box sfrbox-api==0.0.8 From cd1ef93123f84f3adb54140c2c2d1c8e54b7e2b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 11:43:05 +0100 Subject: [PATCH 036/125] Remove suggested area from Verisure (#109364) --- homeassistant/components/verisure/binary_sensor.py | 1 - homeassistant/components/verisure/camera.py | 1 - homeassistant/components/verisure/lock.py | 1 - homeassistant/components/verisure/sensor.py | 2 -- homeassistant/components/verisure/switch.py | 1 - 5 files changed, 6 deletions(-) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index cadb9b6788d..19a60602540 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -58,7 +58,6 @@ class VerisureDoorWindowSensor( area = self.coordinator.data["door_window"][self.serial_number]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model="Shock Sensor Detector", identifiers={(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index a240d45cf7e..e0505328245 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -71,7 +71,6 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera) area = self.coordinator.data["cameras"][self.serial_number]["device"]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model="SmartCam", identifiers={(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 1a81b437116..8e57c9695c0 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -77,7 +77,6 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt area = self.coordinator.data["locks"][self.serial_number]["device"]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model="Lockguard Smartlock", identifiers={(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 0fb16aa87c4..51947484dca 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -68,7 +68,6 @@ class VerisureThermometer( area = self.coordinator.data["climate"][self.serial_number]["device"]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model=DEVICE_TYPE_NAME.get(device_type, device_type), identifiers={(DOMAIN, self.serial_number)}, @@ -119,7 +118,6 @@ class VerisureHygrometer( area = self.coordinator.data["climate"][self.serial_number]["device"]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model=DEVICE_TYPE_NAME.get(device_type, device_type), identifiers={(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 427ca5e6ea8..96992cadb75 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -53,7 +53,6 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch ] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model="SmartPlug", identifiers={(DOMAIN, self.serial_number)}, From a584429ce0e3782844e35d66992ac8f89c163ea0 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Fri, 2 Feb 2024 12:37:23 +0100 Subject: [PATCH 037/125] Use translation placeholders in 1-Wire (#109120) --- .../components/onewire/binary_sensor.py | 12 +- homeassistant/components/onewire/sensor.py | 9 +- homeassistant/components/onewire/strings.json | 178 ++---------------- homeassistant/components/onewire/switch.py | 24 ++- .../onewire/snapshots/test_binary_sensor.ambr | 32 ++-- .../onewire/snapshots/test_sensor.ambr | 16 +- .../onewire/snapshots/test_switch.ambr | 68 +++---- 7 files changed, 108 insertions(+), 231 deletions(-) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 2840cde704b..e7e30588f8a 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -38,7 +38,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... key=f"sensed.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"sensed_{id.lower()}", + translation_key="sensed_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ), @@ -47,7 +48,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... key=f"sensed.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"sensed_{id}", + translation_key="sensed_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_7 ), @@ -56,7 +58,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... key=f"sensed.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"sensed_{id.lower()}", + translation_key="sensed_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ), @@ -72,7 +75,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { read_mode=READ_MODE_BOOL, entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.PROBLEM, - translation_key=f"hub_short_{id}", + translation_key="hub_short_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ), diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index cc8b14b5d6e..a7d199c21a9 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -236,7 +236,8 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement="count", read_mode=READ_MODE_INT, state_class=SensorStateClass.TOTAL_INCREASING, - translation_key=f"counter_{id.lower()}", + translation_key="counter_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ), @@ -276,7 +277,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfPressure.CBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key=f"moisture_{id}", + translation_key="moisture_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ), @@ -396,7 +398,8 @@ def get_entities( description, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, - translation_key=f"wetness_{s_id}", + translation_key="wetness_id", + translation_placeholders={"id": s_id}, ) override_key = None if description.override_key: diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 753f244cfe9..8dbcbdf8978 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -21,55 +21,16 @@ }, "entity": { "binary_sensor": { - "sensed_a": { - "name": "Sensed A" + "sensed_id": { + "name": "Sensed {id}" }, - "sensed_b": { - "name": "Sensed B" - }, - "sensed_0": { - "name": "Sensed 0" - }, - "sensed_1": { - "name": "Sensed 1" - }, - "sensed_2": { - "name": "Sensed 2" - }, - "sensed_3": { - "name": "Sensed 3" - }, - "sensed_4": { - "name": "Sensed 4" - }, - "sensed_5": { - "name": "Sensed 5" - }, - "sensed_6": { - "name": "Sensed 6" - }, - "sensed_7": { - "name": "Sensed 7" - }, - "hub_short_0": { - "name": "Hub short on branch 0" - }, - "hub_short_1": { - "name": "Hub short on branch 1" - }, - "hub_short_2": { - "name": "Hub short on branch 2" - }, - "hub_short_3": { - "name": "Hub short on branch 3" + "hub_short_id": { + "name": "Hub short on branch {id}" } }, "sensor": { - "counter_a": { - "name": "Counter A" - }, - "counter_b": { - "name": "Counter B" + "counter_id": { + "name": "Counter {id}" }, "humidity_hih3600": { "name": "HIH3600 humidity" @@ -86,17 +47,8 @@ "humidity_raw": { "name": "Raw humidity" }, - "moisture_1": { - "name": "Moisture 1" - }, - "moisture_2": { - "name": "Moisture 2" - }, - "moisture_3": { - "name": "Moisture 3" - }, - "moisture_4": { - "name": "Moisture 4" + "moisture_id": { + "name": "Moisture {id}" }, "thermocouple_temperature_k": { "name": "Thermocouple K temperature" @@ -113,121 +65,31 @@ "voltage_vis_gradient": { "name": "VIS voltage gradient" }, - "wetness_0": { - "name": "Wetness 0" - }, - "wetness_1": { - "name": "Wetness 1" - }, - "wetness_2": { - "name": "Wetness 2" - }, - "wetness_3": { - "name": "Wetness 3" + "wetness_id": { + "name": "Wetness {id}" } }, "switch": { - "hub_branch_0": { - "name": "Hub branch 0" - }, - "hub_branch_1": { - "name": "Hub branch 1" - }, - "hub_branch_2": { - "name": "Hub branch 2" - }, - "hub_branch_3": { - "name": "Hub branch 3" + "hub_branch_id": { + "name": "Hub branch {id}" }, "iad": { "name": "Current A/D control" }, - "latch_0": { - "name": "Latch 0" + "latch_id": { + "name": "Latch {id}" }, - "latch_1": { - "name": "Latch 1" + "leaf_sensor_id": { + "name": "Leaf sensor {id}" }, - "latch_2": { - "name": "Latch 2" - }, - "latch_3": { - "name": "Latch 3" - }, - "latch_4": { - "name": "Latch 4" - }, - "latch_5": { - "name": "Latch 5" - }, - "latch_6": { - "name": "Latch 6" - }, - "latch_7": { - "name": "Latch 7" - }, - "latch_a": { - "name": "Latch A" - }, - "latch_b": { - "name": "Latch B" - }, - "leaf_sensor_0": { - "name": "Leaf sensor 0" - }, - "leaf_sensor_1": { - "name": "Leaf sensor 1" - }, - "leaf_sensor_2": { - "name": "Leaf sensor 2" - }, - "leaf_sensor_3": { - "name": "Leaf sensor 3" - }, - "moisture_sensor_0": { - "name": "Moisture sensor 0" - }, - "moisture_sensor_1": { - "name": "Moisture sensor 1" - }, - "moisture_sensor_2": { - "name": "Moisture sensor 2" - }, - "moisture_sensor_3": { - "name": "Moisture sensor 3" + "moisture_sensor_id": { + "name": "Moisture sensor {id}" }, "pio": { "name": "Programmed input-output" }, - "pio_0": { - "name": "Programmed input-output 0" - }, - "pio_1": { - "name": "Programmed input-output 1" - }, - "pio_2": { - "name": "Programmed input-output 2" - }, - "pio_3": { - "name": "Programmed input-output 3" - }, - "pio_4": { - "name": "Programmed input-output 4" - }, - "pio_5": { - "name": "Programmed input-output 5" - }, - "pio_6": { - "name": "Programmed input-output 6" - }, - "pio_7": { - "name": "Programmed input-output 7" - }, - "pio_a": { - "name": "Programmed input-output A" - }, - "pio_b": { - "name": "Programmed input-output B" + "pio_id": { + "name": "Programmed input-output {id}" } } }, diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index db9e8f5b0f8..00a3f8f65f4 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -42,7 +42,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"PIO.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"pio_{id.lower()}", + translation_key="pio_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ] @@ -51,7 +52,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"latch.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"latch_{id.lower()}", + translation_key="latch_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ] @@ -71,7 +73,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"PIO.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"pio_{id}", + translation_key="pio_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_7 ] @@ -80,7 +83,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"latch.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"latch_{id}", + translation_key="latch_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_7 ] @@ -90,7 +94,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"PIO.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"pio_{id.lower()}", + translation_key="pio_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ), @@ -106,7 +111,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, - translation_key=f"hub_branch_{id}", + translation_key="hub_branch_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ), @@ -117,7 +123,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, - translation_key=f"leaf_sensor_{id}", + translation_key="leaf_sensor_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ] @@ -127,7 +134,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, - translation_key=f"moisture_sensor_{id}", + translation_key="moisture_sensor_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ] diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 25d47b342c5..8ca1e476820 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -144,7 +144,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_a', + 'translation_key': 'sensed_id', 'unique_id': '/12.111111111111/sensed.A', 'unit_of_measurement': None, }), @@ -173,7 +173,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_b', + 'translation_key': 'sensed_id', 'unique_id': '/12.111111111111/sensed.B', 'unit_of_measurement': None, }), @@ -556,7 +556,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_0', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.0', 'unit_of_measurement': None, }), @@ -585,7 +585,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_1', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.1', 'unit_of_measurement': None, }), @@ -614,7 +614,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_2', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.2', 'unit_of_measurement': None, }), @@ -643,7 +643,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_3', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.3', 'unit_of_measurement': None, }), @@ -672,7 +672,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_4', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.4', 'unit_of_measurement': None, }), @@ -701,7 +701,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_5', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.5', 'unit_of_measurement': None, }), @@ -730,7 +730,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_6', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.6', 'unit_of_measurement': None, }), @@ -759,7 +759,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_7', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.7', 'unit_of_measurement': None, }), @@ -960,7 +960,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_a', + 'translation_key': 'sensed_id', 'unique_id': '/3A.111111111111/sensed.A', 'unit_of_measurement': None, }), @@ -989,7 +989,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_b', + 'translation_key': 'sensed_id', 'unique_id': '/3A.111111111111/sensed.B', 'unit_of_measurement': None, }), @@ -1308,7 +1308,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_short_0', + 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.0', 'unit_of_measurement': None, }), @@ -1337,7 +1337,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_short_1', + 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.1', 'unit_of_measurement': None, }), @@ -1366,7 +1366,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_short_2', + 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.2', 'unit_of_measurement': None, }), @@ -1395,7 +1395,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_short_3', + 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.3', 'unit_of_measurement': None, }), diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index cbcf0d6234e..936018a48c4 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -322,7 +322,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'counter_a', + 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.A', 'unit_of_measurement': 'count', }), @@ -353,7 +353,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'counter_b', + 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.B', 'unit_of_measurement': 'count', }), @@ -476,7 +476,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'counter_a', + 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.A', 'unit_of_measurement': 'count', }), @@ -507,7 +507,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'counter_b', + 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.B', 'unit_of_measurement': 'count', }), @@ -2478,7 +2478,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wetness_0', + 'translation_key': 'wetness_id', 'unique_id': '/EF.111111111112/moisture/sensor.0', 'unit_of_measurement': '%', }), @@ -2509,7 +2509,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wetness_1', + 'translation_key': 'wetness_id', 'unique_id': '/EF.111111111112/moisture/sensor.1', 'unit_of_measurement': '%', }), @@ -2540,7 +2540,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_2', + 'translation_key': 'moisture_id', 'unique_id': '/EF.111111111112/moisture/sensor.2', 'unit_of_measurement': , }), @@ -2571,7 +2571,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_3', + 'translation_key': 'moisture_id', 'unique_id': '/EF.111111111112/moisture/sensor.3', 'unit_of_measurement': , }), diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index e4d081a409b..24c985a311e 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -185,7 +185,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_a', + 'translation_key': 'pio_id', 'unique_id': '/12.111111111111/PIO.A', 'unit_of_measurement': None, }), @@ -214,7 +214,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_b', + 'translation_key': 'pio_id', 'unique_id': '/12.111111111111/PIO.B', 'unit_of_measurement': None, }), @@ -243,7 +243,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_a', + 'translation_key': 'latch_id', 'unique_id': '/12.111111111111/latch.A', 'unit_of_measurement': None, }), @@ -272,7 +272,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_b', + 'translation_key': 'latch_id', 'unique_id': '/12.111111111111/latch.B', 'unit_of_measurement': None, }), @@ -720,7 +720,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_0', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.0', 'unit_of_measurement': None, }), @@ -749,7 +749,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_1', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.1', 'unit_of_measurement': None, }), @@ -778,7 +778,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_2', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.2', 'unit_of_measurement': None, }), @@ -807,7 +807,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_3', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.3', 'unit_of_measurement': None, }), @@ -836,7 +836,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_4', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.4', 'unit_of_measurement': None, }), @@ -865,7 +865,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_5', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.5', 'unit_of_measurement': None, }), @@ -894,7 +894,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_6', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.6', 'unit_of_measurement': None, }), @@ -923,7 +923,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_7', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.7', 'unit_of_measurement': None, }), @@ -952,7 +952,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_0', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.0', 'unit_of_measurement': None, }), @@ -981,7 +981,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_1', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.1', 'unit_of_measurement': None, }), @@ -1010,7 +1010,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_2', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.2', 'unit_of_measurement': None, }), @@ -1039,7 +1039,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_3', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.3', 'unit_of_measurement': None, }), @@ -1068,7 +1068,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_4', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.4', 'unit_of_measurement': None, }), @@ -1097,7 +1097,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_5', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.5', 'unit_of_measurement': None, }), @@ -1126,7 +1126,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_6', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.6', 'unit_of_measurement': None, }), @@ -1155,7 +1155,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_7', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.7', 'unit_of_measurement': None, }), @@ -1452,7 +1452,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_a', + 'translation_key': 'pio_id', 'unique_id': '/3A.111111111111/PIO.A', 'unit_of_measurement': None, }), @@ -1481,7 +1481,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_b', + 'translation_key': 'pio_id', 'unique_id': '/3A.111111111111/PIO.B', 'unit_of_measurement': None, }), @@ -1762,7 +1762,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'leaf_sensor_0', + 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.0', 'unit_of_measurement': None, }), @@ -1791,7 +1791,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'leaf_sensor_1', + 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.1', 'unit_of_measurement': None, }), @@ -1820,7 +1820,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'leaf_sensor_2', + 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.2', 'unit_of_measurement': None, }), @@ -1849,7 +1849,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'leaf_sensor_3', + 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.3', 'unit_of_measurement': None, }), @@ -1878,7 +1878,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_sensor_0', + 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.0', 'unit_of_measurement': None, }), @@ -1907,7 +1907,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_sensor_1', + 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.1', 'unit_of_measurement': None, }), @@ -1936,7 +1936,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_sensor_2', + 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.2', 'unit_of_measurement': None, }), @@ -1965,7 +1965,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_sensor_3', + 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.3', 'unit_of_measurement': None, }), @@ -2128,7 +2128,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_branch_0', + 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.0', 'unit_of_measurement': None, }), @@ -2157,7 +2157,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_branch_1', + 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.1', 'unit_of_measurement': None, }), @@ -2186,7 +2186,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_branch_2', + 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.2', 'unit_of_measurement': None, }), @@ -2215,7 +2215,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_branch_3', + 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.3', 'unit_of_measurement': None, }), From d3dbd6fa70d1b2021a308cbaf159fdc96f742487 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 2 Feb 2024 21:38:57 +1000 Subject: [PATCH 038/125] Change device class of Auto Seat Heater sensors in Tessie (#109240) --- .../components/tessie/binary_sensor.py | 3 - homeassistant/components/tessie/icons.json | 23 +++++ homeassistant/components/tessie/sensor.py | 2 - homeassistant/components/tessie/strings.json | 2 +- .../tessie/snapshots/test_binary_sensors.ambr | 93 +++++++++---------- .../tessie/snapshots/test_sensor.ambr | 6 +- 6 files changed, 71 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/tessie/icons.json diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 594098cddfe..65bfd483f18 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -62,17 +62,14 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( ), TessieBinarySensorEntityDescription( key="climate_state_auto_seat_climate_left", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), TessieBinarySensorEntityDescription( key="climate_state_auto_seat_climate_right", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), TessieBinarySensorEntityDescription( key="climate_state_auto_steering_wheel_heat", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), TessieBinarySensorEntityDescription( diff --git a/homeassistant/components/tessie/icons.json b/homeassistant/components/tessie/icons.json new file mode 100644 index 00000000000..caf0524f2e8 --- /dev/null +++ b/homeassistant/components/tessie/icons.json @@ -0,0 +1,23 @@ +{ + "entity": { + "sensor": { + "drive_state_shift_state": { + "default": "mdi:car-shift-pattern" + }, + "drive_state_active_route_destination": { + "default": "mdi:map-marker" + } + }, + "binary_sensor": { + "climate_state_auto_seat_climate_left": { + "default": "mdi:car-seat-heater" + }, + "climate_state_auto_seat_climate_right": { + "default": "mdi:car-seat-heater" + }, + "climate_state_auto_steering_wheel_heat": { + "default": "mdi:steering" + } + } + } +} diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index ae9e06b2b35..36896863120 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -122,7 +122,6 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( ), TessieSensorEntityDescription( key="drive_state_shift_state", - icon="mdi:car-shift-pattern", options=["p", "d", "r", "n"], device_class=SensorDeviceClass.ENUM, value_fn=lambda x: x.lower() if isinstance(x, str) else x, @@ -231,7 +230,6 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( ), TessieSensorEntityDescription( key="drive_state_active_route_destination", - icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 8340557843d..381a5e3d4c0 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -258,7 +258,7 @@ "climate_state_auto_seat_climate_right": { "name": "Auto seat climate right" }, - "climate_state_auto_steering_wheel_heater": { + "climate_state_auto_steering_wheel_heat": { "name": "Auto steering wheel heater" }, "climate_state_cabin_overheat_protection": { diff --git a/tests/components/tessie/snapshots/test_binary_sensors.ambr b/tests/components/tessie/snapshots/test_binary_sensors.ambr index 2fbd6764081..aacaad1d7e4 100644 --- a/tests/components/tessie/snapshots/test_binary_sensors.ambr +++ b/tests/components/tessie/snapshots/test_binary_sensors.ambr @@ -19,7 +19,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Auto seat climate left', 'platform': 'tessie', @@ -33,7 +33,6 @@ # name: test_binary_sensors[binary_sensor.test_auto_seat_climate_left-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'Test Auto seat climate left', }), 'context': , @@ -63,7 +62,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Auto seat climate right', 'platform': 'tessie', @@ -77,7 +76,6 @@ # name: test_binary_sensors[binary_sensor.test_auto_seat_climate_right-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'Test Auto seat climate right', }), 'context': , @@ -87,6 +85,49 @@ 'state': 'on', }) # --- +# name: test_binary_sensors[binary_sensor.test_auto_steering_wheel_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_auto_steering_wheel_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto steering wheel heater', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_auto_steering_wheel_heat', + 'unique_id': 'VINVINVIN-climate_state_auto_steering_wheel_heat', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.test_auto_steering_wheel_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Auto steering wheel heater', + }), + 'context': , + 'entity_id': 'binary_sensor.test_auto_steering_wheel_heater', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_binary_sensors[binary_sensor.test_battery_heater-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -527,50 +568,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[binary_sensor.test_heat-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.test_heat', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Heat', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'climate_state_auto_steering_wheel_heat', - 'unique_id': 'VINVINVIN-climate_state_auto_steering_wheel_heat', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[binary_sensor.test_heat-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'heat', - 'friendly_name': 'Test Heat', - }), - 'context': , - 'entity_id': 'binary_sensor.test_heat', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[binary_sensor.test_preconditioning_enabled-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index 2f5e1e8ddb2..0c01fc50244 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -368,7 +368,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:map-marker', + 'original_icon': None, 'original_name': 'Destination', 'platform': 'tessie', 'previous_unique_id': None, @@ -382,7 +382,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test Destination', - 'icon': 'mdi:map-marker', }), 'context': , 'entity_id': 'sensor.test_destination', @@ -776,7 +775,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:car-shift-pattern', + 'original_icon': None, 'original_name': 'Shift state', 'platform': 'tessie', 'previous_unique_id': None, @@ -791,7 +790,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'Test Shift state', - 'icon': 'mdi:car-shift-pattern', 'options': list([ 'p', 'd', From 90ec361fc95acbd4e0e46d9627c0ae6ab678ba07 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 2 Feb 2024 12:42:12 +0100 Subject: [PATCH 039/125] Centralize validation for modbus config (#108906) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/modbus/validators.py | 261 ++++++++---------- tests/components/modbus/test_init.py | 117 ++++---- 2 files changed, 187 insertions(+), 191 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 76d8e270ffe..37eae23ba82 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -203,141 +203,6 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: return config -def scan_interval_validator(config: dict) -> dict: - """Control scan_interval.""" - for hub in config: - minimum_scan_interval = DEFAULT_SCAN_INTERVAL - for component, conf_key in PLATFORMS: - if conf_key not in hub: - continue - - for entry in hub[conf_key]: - scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - if scan_interval == 0: - continue - if scan_interval < 5: - _LOGGER.warning( - ( - "%s %s scan_interval(%d) is lower than 5 seconds, " - "which may cause Home Assistant stability issues" - ), - component, - entry.get(CONF_NAME), - scan_interval, - ) - entry[CONF_SCAN_INTERVAL] = scan_interval - minimum_scan_interval = min(scan_interval, minimum_scan_interval) - if ( - CONF_TIMEOUT in hub - and hub[CONF_TIMEOUT] > minimum_scan_interval - 1 - and minimum_scan_interval > 1 - ): - _LOGGER.warning( - "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval", - hub.get(CONF_NAME, ""), - hub[CONF_TIMEOUT], - minimum_scan_interval - 1, - ) - hub[CONF_TIMEOUT] = minimum_scan_interval - 1 - return config - - -def duplicate_entity_validator(config: dict) -> dict: - """Control scan_interval.""" - for hub_index, hub in enumerate(config): - for component, conf_key in PLATFORMS: - if conf_key not in hub: - continue - names: set[str] = set() - errors: list[int] = [] - addresses: set[str] = set() - for index, entry in enumerate(hub[conf_key]): - name = entry[CONF_NAME] - addr = str(entry[CONF_ADDRESS]) - if CONF_INPUT_TYPE in entry: - addr += "_" + str(entry[CONF_INPUT_TYPE]) - elif CONF_WRITE_TYPE in entry: - addr += "_" + str(entry[CONF_WRITE_TYPE]) - if CONF_COMMAND_ON in entry: - addr += "_" + str(entry[CONF_COMMAND_ON]) - if CONF_COMMAND_OFF in entry: - addr += "_" + str(entry[CONF_COMMAND_OFF]) - inx = entry.get(CONF_SLAVE, None) or entry.get(CONF_DEVICE_ADDRESS, 0) - addr += "_" + str(inx) - entry_addrs: set[str] = set() - entry_addrs.add(addr) - - if CONF_TARGET_TEMP in entry: - a = str(entry[CONF_TARGET_TEMP]) - a += "_" + str(inx) - entry_addrs.add(a) - if CONF_HVAC_MODE_REGISTER in entry: - a = str(entry[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]) - a += "_" + str(inx) - entry_addrs.add(a) - if CONF_FAN_MODE_REGISTER in entry: - a = str( - entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS] - if isinstance(entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS], int) - else entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS][0] - ) - a += "_" + str(inx) - entry_addrs.add(a) - - dup_addrs = entry_addrs.intersection(addresses) - - if len(dup_addrs) > 0: - for addr in dup_addrs: - err = ( - f"Modbus {component}/{name} address {addr} is duplicate, second" - " entry not loaded!" - ) - _LOGGER.warning(err) - errors.append(index) - elif name in names: - err = ( - f"Modbus {component}/{name}  is duplicate, second entry not" - " loaded!" - ) - _LOGGER.warning(err) - errors.append(index) - else: - names.add(name) - addresses.update(entry_addrs) - - for i in reversed(errors): - del config[hub_index][conf_key][i] - return config - - -def duplicate_modbus_validator(config: dict) -> dict: - """Control modbus connection for duplicates.""" - hosts: set[str] = set() - names: set[str] = set() - errors = [] - for index, hub in enumerate(config): - name = hub.get(CONF_NAME, DEFAULT_HUB) - if hub[CONF_TYPE] == SERIAL: - host = hub[CONF_PORT] - else: - host = f"{hub[CONF_HOST]}_{hub[CONF_PORT]}" - if host in hosts: - err = f"Modbus {name} contains duplicate host/port {host}, not loaded!" - _LOGGER.warning(err) - errors.append(index) - elif name in names: - err = f"Modbus {name} is duplicate, second entry not loaded!" - _LOGGER.warning(err) - errors.append(index) - else: - hosts.add(host) - names.add(name) - - for i in reversed(errors): - del config[i] - return config - - def register_int_list_validator(value: Any) -> Any: """Check if a register (CONF_ADRESS) is an int or a list having only 1 register.""" if isinstance(value, int) and value >= 0: @@ -354,7 +219,125 @@ def register_int_list_validator(value: Any) -> Any: def check_config(config: dict) -> dict: """Do final config check.""" - config2 = duplicate_modbus_validator(config) - config3 = scan_interval_validator(config2) - config4 = duplicate_entity_validator(config3) - return config4 + hosts: set[str] = set() + hub_names: set[str] = set() + hub_name_inx = 0 + minimum_scan_interval = 0 + ent_names: set[str] = set() + ent_addr: set[str] = set() + + def validate_modbus(hub: dict, hub_name_inx: int) -> bool: + """Validate modbus entries.""" + host: str = ( + hub[CONF_PORT] + if hub[CONF_TYPE] == SERIAL + else f"{hub[CONF_HOST]}_{hub[CONF_PORT]}" + ) + if CONF_NAME not in hub: + hub[CONF_NAME] = ( + DEFAULT_HUB if not hub_name_inx else f"{DEFAULT_HUB}_{hub_name_inx}" + ) + hub_name_inx += 1 + err = f"Modbus host/port {host} is missing name, added {hub[CONF_NAME]}!" + _LOGGER.warning(err) + name = hub[CONF_NAME] + if host in hosts or name in hub_names: + err = f"Modbus {name} host/port {host} is duplicate, not loaded!" + _LOGGER.warning(err) + return False + hosts.add(host) + hub_names.add(name) + return True + + def validate_entity( + hub_name: str, + entity: dict, + minimum_scan_interval: int, + ent_names: set, + ent_addr: set, + ) -> bool: + """Validate entity.""" + name = entity[CONF_NAME] + addr = str(entity[CONF_ADDRESS]) + scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + if scan_interval < 5: + _LOGGER.warning( + ( + "%s %s scan_interval(%d) is lower than 5 seconds, " + "which may cause Home Assistant stability issues" + ), + hub_name, + name, + scan_interval, + ) + entity[CONF_SCAN_INTERVAL] = scan_interval + minimum_scan_interval = min(scan_interval, minimum_scan_interval) + for conf_type in ( + CONF_INPUT_TYPE, + CONF_WRITE_TYPE, + CONF_COMMAND_ON, + CONF_COMMAND_OFF, + ): + if conf_type in entity: + addr += f"_{entity[conf_type]}" + inx = entity.get(CONF_SLAVE, None) or entity.get(CONF_DEVICE_ADDRESS, 0) + addr += f"_{inx}" + loc_addr: set[str] = {addr} + + if CONF_TARGET_TEMP in entity: + loc_addr.add(f"{entity[CONF_TARGET_TEMP]}_{inx}") + if CONF_HVAC_MODE_REGISTER in entity: + loc_addr.add(f"{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}") + if CONF_FAN_MODE_REGISTER in entity: + loc_addr.add(f"{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}") + + dup_addrs = ent_addr.intersection(loc_addr) + if len(dup_addrs) > 0: + for addr in dup_addrs: + err = ( + f"Modbus {hub_name}/{name} address {addr} is duplicate, second" + " entry not loaded!" + ) + _LOGGER.warning(err) + return False + if name in ent_names: + err = f"Modbus {hub_name}/{name} is duplicate, second entry not loaded!" + _LOGGER.warning(err) + return False + ent_names.add(name) + ent_addr.update(loc_addr) + return True + + hub_inx = 0 + while hub_inx < len(config): + hub = config[hub_inx] + if not validate_modbus(hub, hub_name_inx): + del config[hub_inx] + continue + for _component, conf_key in PLATFORMS: + if conf_key not in hub: + continue + entity_inx = 0 + entities = hub[conf_key] + minimum_scan_interval = 9999 + while entity_inx < len(entities): + if not validate_entity( + hub[CONF_NAME], + entities[entity_inx], + minimum_scan_interval, + ent_names, + ent_addr, + ): + del entities[entity_inx] + else: + entity_inx += 1 + + if hub[CONF_TIMEOUT] >= minimum_scan_interval: + hub[CONF_TIMEOUT] = minimum_scan_interval - 1 + _LOGGER.warning( + "Modbus %s timeout is adjusted(%d) due to scan_interval", + hub[CONF_NAME], + hub[CONF_TIMEOUT], + ) + hub_inx += 1 + return config diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 3c932a24afb..c5b12a112fd 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -79,9 +79,8 @@ from homeassistant.components.modbus.const import ( DataType, ) from homeassistant.components.modbus.validators import ( - duplicate_entity_validator, + check_config, duplicate_fan_mode_validator, - duplicate_modbus_validator, nan_validator, register_int_list_validator, struct_validator, @@ -340,55 +339,46 @@ async def test_exception_struct_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, }, { CONF_NAME: TEST_MODBUS_NAME, CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST + " 2", CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + }, + { + CONF_NAME: TEST_MODBUS_NAME + "2", + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, }, ], [ { - CONF_NAME: TEST_MODBUS_NAME, CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, }, { CONF_NAME: TEST_MODBUS_NAME + " 2", CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, }, ], ], ) -async def test_duplicate_modbus_validator(do_config) -> None: +async def test_check_config(do_config) -> None: """Test duplicate modbus validator.""" - duplicate_modbus_validator(do_config) + check_config(do_config) assert len(do_config) == 1 -@pytest.mark.parametrize( - "do_config", - [ - { - CONF_ADDRESS: 11, - CONF_FAN_MODE_VALUES: { - CONF_FAN_MODE_ON: 7, - CONF_FAN_MODE_OFF: 9, - CONF_FAN_MODE_HIGH: 9, - }, - } - ], -) -async def test_duplicate_fan_mode_validator(do_config) -> None: - """Test duplicate modbus validator.""" - duplicate_fan_mode_validator(do_config) - assert len(do_config[CONF_FAN_MODE_VALUES]) == 2 - - @pytest.mark.parametrize( "do_config", [ @@ -398,6 +388,7 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_SENSORS: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -418,6 +409,7 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_SENSORS: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -432,35 +424,12 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: ], } ], - [ - { - CONF_NAME: TEST_MODBUS_NAME, - CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST, - CONF_PORT: TEST_PORT_TCP, - CONF_CLIMATES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_ADDRESS: 117, - CONF_SLAVE: 0, - }, - { - CONF_NAME: TEST_ENTITY_NAME + " 2", - CONF_ADDRESS: 117, - CONF_SLAVE: 0, - }, - ], - } - ], ], ) -async def test_duplicate_entity_validator(do_config) -> None: +async def test_check_config_sensor(do_config) -> None: """Test duplicate entity validator.""" - duplicate_entity_validator(do_config) - if CONF_SENSORS in do_config[0]: - assert len(do_config[0][CONF_SENSORS]) == 1 - elif CONF_CLIMATES in do_config[0]: - assert len(do_config[0][CONF_CLIMATES]) == 1 + check_config(do_config) + assert len(do_config[0][CONF_SENSORS]) == 1 @pytest.mark.parametrize( @@ -472,6 +441,28 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 117, + CONF_SLAVE: 0, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 119, + CONF_SLAVE: 0, + }, + ], + } + ], + [ + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_CLIMATES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -492,6 +483,7 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_CLIMATES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -526,6 +518,7 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_CLIMATES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -561,6 +554,7 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_CLIMATES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -592,12 +586,31 @@ async def test_duplicate_entity_validator(do_config) -> None: ], ], ) -async def test_duplicate_entity_validator_with_climate(do_config) -> None: +async def test_check_config_climate(do_config) -> None: """Test duplicate entity validator.""" - duplicate_entity_validator(do_config) + check_config(do_config) assert len(do_config[0][CONF_CLIMATES]) == 1 +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 11, + CONF_FAN_MODE_VALUES: { + CONF_FAN_MODE_ON: 7, + CONF_FAN_MODE_OFF: 9, + CONF_FAN_MODE_HIGH: 9, + }, + } + ], +) +async def test_duplicate_fan_mode_validator(do_config) -> None: + """Test duplicate modbus validator.""" + duplicate_fan_mode_validator(do_config) + assert len(do_config[CONF_FAN_MODE_VALUES]) == 2 + + @pytest.mark.parametrize( "do_config", [ From e328d3ec5e01048b55e7602b33c28c573bdca5f0 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 2 Feb 2024 22:21:13 +1000 Subject: [PATCH 040/125] Add Charging sensor to Tessie (#108205) --- .../components/tessie/binary_sensor.py | 1 + homeassistant/components/tessie/const.py | 10 +++ homeassistant/components/tessie/sensor.py | 9 ++- homeassistant/components/tessie/strings.json | 11 ++++ .../tessie/snapshots/test_sensor.ambr | 62 +++++++++++++++++++ .../components/tessie/test_binary_sensors.py | 2 + 6 files changed, 94 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 65bfd483f18..ff0cf661475 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -41,6 +41,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( key="charge_state_charging_state", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, is_on=lambda x: x == "Charging", + entity_registry_enabled_default=False, ), TessieBinarySensorEntityDescription( key="charge_state_preconditioning_enabled", diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index 591d4652274..8ec063bf47c 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -68,3 +68,13 @@ class TessieChargeCableLockStates(StrEnum): ENGAGED = "Engaged" DISENGAGED = "Disengaged" + + +TessieChargeStates = { + "Starting": "starting", + "Charging": "charging", + "Stopped": "stopped", + "Complete": "complete", + "Disconnected": "disconnected", + "NoPower": "no_power", +} diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index 36896863120..7c126754fb5 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -32,7 +32,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from homeassistant.util.variance import ignore_variance -from .const import DOMAIN +from .const import DOMAIN, TessieChargeStates from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity @@ -54,6 +54,13 @@ class TessieSensorEntityDescription(SensorEntityDescription): DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="charge_state_charging_state", + icon="mdi:ev-station", + options=list(TessieChargeStates.values()), + device_class=SensorDeviceClass.ENUM, + value_fn=lambda value: TessieChargeStates[cast(str, value)], + ), TessieSensorEntityDescription( key="charge_state_usable_battery_level", state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 381a5e3d4c0..01e6a654163 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -67,6 +67,17 @@ } }, "sensor": { + "charge_state_charging_state": { + "name": "Charging", + "state": { + "starting": "Starting", + "charging": "Charging", + "disconnected": "Disconnected", + "stopped": "Stopped", + "complete": "Complete", + "no_power": "No power" + } + }, "charge_state_usable_battery_level": { "name": "Battery level" }, diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index 0c01fc50244..b9a423bfa9a 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -347,6 +347,68 @@ 'state': '224', }) # --- +# name: test_sensors[sensor.test_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ev-station', + 'original_name': 'Charging', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charging_state', + 'unique_id': 'VINVINVIN-charge_state_charging_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Charging', + 'icon': 'mdi:ev-station', + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_charging', + 'last_changed': , + 'last_updated': , + 'state': 'charging', + }) +# --- # name: test_sensors[sensor.test_destination-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tessie/test_binary_sensors.py b/tests/components/tessie/test_binary_sensors.py index ca53a60d493..b6dccd9d3b1 100644 --- a/tests/components/tessie/test_binary_sensors.py +++ b/tests/components/tessie/test_binary_sensors.py @@ -1,4 +1,5 @@ """Test the Tessie binary sensor platform.""" +import pytest from syrupy import SnapshotAssertion from homeassistant.const import Platform @@ -8,6 +9,7 @@ from homeassistant.helpers import entity_registry as er from .common import assert_entities, setup_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry ) -> None: From 3bab1d7cd5a6b38ee673ceb9e9dce140b69d005a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 2 Feb 2024 13:42:07 +0100 Subject: [PATCH 041/125] Specify end_time when importing Elvia data to deal with drift (#109361) --- homeassistant/components/elvia/importer.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/elvia/importer.py b/homeassistant/components/elvia/importer.py index 3fc79240254..69e3d64d09d 100644 --- a/homeassistant/components/elvia/importer.py +++ b/homeassistant/components/elvia/importer.py @@ -38,11 +38,18 @@ class ElviaImporter: self.client = Elvia(meter_value_token=api_token).meter_value() self.metering_point_id = metering_point_id - async def _fetch_hourly_data(self, since: datetime) -> list[MeterValueTimeSeries]: + async def _fetch_hourly_data( + self, + since: datetime, + until: datetime, + ) -> list[MeterValueTimeSeries]: """Fetch hourly data.""" - LOGGER.debug("Fetching hourly data since %s", since) + start_time = since.isoformat() + end_time = until.isoformat() + LOGGER.debug("Fetching hourly data %s - %s", start_time, end_time) all_data = await self.client.get_meter_values( - start_time=since.isoformat(), + start_time=start_time, + end_time=end_time, metering_point_ids=[self.metering_point_id], ) return all_data["meteringpoints"][0]["metervalue"]["timeSeries"] @@ -62,8 +69,10 @@ class ElviaImporter: if not last_stats: # First time we insert 1 years of data (if available) + until = dt_util.utcnow() hourly_data = await self._fetch_hourly_data( - since=dt_util.now() - timedelta(days=365) + since=until - timedelta(days=365), + until=until, ) if hourly_data is None or len(hourly_data) == 0: return @@ -71,7 +80,8 @@ class ElviaImporter: _sum = 0.0 else: hourly_data = await self._fetch_hourly_data( - since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"]) + since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"]), + until=dt_util.utcnow(), ) if ( From a6c697c80f8276692a081865b5bf834aa2d82a45 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 Feb 2024 14:03:21 +0100 Subject: [PATCH 042/125] Add entity name translations to Tibber (#108797) --- homeassistant/components/tibber/sensor.py | 61 +++++++------- homeassistant/components/tibber/strings.json | 85 ++++++++++++++++++++ 2 files changed, 116 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 467cd2bfd77..52e18c9c6a2 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -64,129 +64,129 @@ PARALLEL_UPDATES = 0 RT_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="averagePower", - name="average power", + translation_key="average_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="power", - name="power", + translation_key="power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="powerProduction", - name="power production", + translation_key="power_production", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="minPower", - name="min power", + translation_key="min_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="maxPower", - name="max power", + translation_key="max_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="accumulatedConsumption", - name="accumulated consumption", + translation_key="accumulated_consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedConsumptionLastHour", - name="accumulated consumption current hour", + translation_key="accumulated_consumption_last_hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="estimatedHourConsumption", - name="Estimated consumption current hour", + translation_key="estimated_hour_consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), SensorEntityDescription( key="accumulatedProduction", - name="accumulated production", + translation_key="accumulated_production", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedProductionLastHour", - name="accumulated production current hour", + translation_key="accumulated_production_last_hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="lastMeterConsumption", - name="last meter consumption", + translation_key="last_meter_consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="lastMeterProduction", - name="last meter production", + translation_key="last_meter_production", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="voltagePhase1", - name="voltage phase1", + translation_key="voltage_phase1", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltagePhase2", - name="voltage phase2", + translation_key="voltage_phase2", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltagePhase3", - name="voltage phase3", + translation_key="voltage_phase3", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL1", - name="current L1", + translation_key="current_l1", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL2", - name="current L2", + translation_key="current_l2", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL3", - name="current L3", + translation_key="current_l3", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="signalStrength", - name="signal strength", + translation_key="signal_strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=SensorStateClass.MEASUREMENT, @@ -194,19 +194,19 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="accumulatedReward", - name="accumulated reward", + translation_key="accumulated_reward", device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedCost", - name="accumulated cost", + translation_key="accumulated_cost", device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="powerFactor", - name="power factor", + translation_key="power_factor", device_class=SensorDeviceClass.POWER_FACTOR, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -216,23 +216,23 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="month_cost", - name="Monthly cost", + translation_key="month_cost", device_class=SensorDeviceClass.MONETARY, ), SensorEntityDescription( key="peak_hour", - name="Monthly peak hour consumption", + translation_key="peak_hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), SensorEntityDescription( key="peak_hour_time", - name="Time of max hour consumption", + translation_key="peak_hour_time", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key="month_cons", - name="Monthly net consumption", + translation_key="month_cons", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -305,6 +305,8 @@ async def async_setup_entry( class TibberSensor(SensorEntity): """Representation of a generic Tibber sensor.""" + _attr_has_entity_name = True + def __init__( self, *args: Any, tibber_home: tibber.TibberHome, **kwargs: Any ) -> None: @@ -335,6 +337,9 @@ class TibberSensor(SensorEntity): class TibberSensorElPrice(TibberSensor): """Representation of a Tibber sensor for el price.""" + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_translation_key = "electricity_price" + def __init__(self, tibber_home: tibber.TibberHome) -> None: """Initialize the sensor.""" super().__init__(tibber_home=tibber_home) @@ -355,8 +360,6 @@ class TibberSensorElPrice(TibberSensor): "off_peak_2": None, } self._attr_icon = ICON - self._attr_name = f"Electricity price {self._home_name}" - self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_unique_id = self._tibber_home.home_id self._model = "Price Sensor" @@ -424,7 +427,6 @@ class TibberDataSensor(TibberSensor, CoordinatorEntity["TibberDataCoordinator"]) self._attr_unique_id = ( f"{self._tibber_home.home_id}_{self.entity_description.key}" ) - self._attr_name = f"{entity_description.name} {self._home_name}" if entity_description.key == "month_cost": self._attr_native_unit_of_measurement = self._tibber_home.currency @@ -452,7 +454,6 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"]) self._model = "Tibber Pulse" self._device_name = f"{self._model} {self._home_name}" - self._attr_name = f"{description.name} {self._home_name}" self._attr_native_value = initial_state self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}" diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json index c7cef9f4657..af14c96674d 100644 --- a/homeassistant/components/tibber/strings.json +++ b/homeassistant/components/tibber/strings.json @@ -1,4 +1,89 @@ { + "entity": { + "sensor": { + "electricity_price": { + "name": "Electricity price" + }, + "month_cost": { + "name": "Monthly cost" + }, + "peak_hour": { + "name": "Monthly peak hour consumption" + }, + "peak_hour_time": { + "name": "Time of max hour consumption" + }, + "month_cons": { + "name": "Monthly net consumption" + }, + "average_power": { + "name": "Average power" + }, + "power": { + "name": "Power" + }, + "power_production": { + "name": "Power production" + }, + "min_power": { + "name": "Min power" + }, + "max_power": { + "name": "Max power" + }, + "accumulated_consumption": { + "name": "Accumulated consumption" + }, + "accumulated_consumption_last_hour": { + "name": "Accumulated consumption current hour" + }, + "estimated_hour_consumption": { + "name": "Estimated consumption current hour" + }, + "accumulated_production": { + "name": "Accumulated production" + }, + "accumulated_production_last_hour": { + "name": "Accumulated production current hour" + }, + "last_meter_consumption": { + "name": "Last meter consumption" + }, + "last_meter_production": { + "name": "Last meter production" + }, + "voltage_phase1": { + "name": "Voltage phase1" + }, + "voltage_phase2": { + "name": "Voltage phase2" + }, + "voltage_phase3": { + "name": "Voltage phase3" + }, + "current_l1": { + "name": "Current L1" + }, + "current_l2": { + "name": "Current L2" + }, + "current_l3": { + "name": "Current L3" + }, + "signal_strength": { + "name": "Signal strength" + }, + "accumulated_reward": { + "name": "Accumulated reward" + }, + "accumulated_cost": { + "name": "Accumulated cost" + }, + "power_factor": { + "name": "Power factor" + } + } + }, "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" From 343086a6c835fa19340c4ae7cb774323e4a520d3 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Feb 2024 14:12:26 +0100 Subject: [PATCH 043/125] Improve Ecovacs naming (#109372) --- homeassistant/components/ecovacs/strings.json | 16 +- .../ecovacs/snapshots/test_button.ambr | 98 +++++----- .../ecovacs/snapshots/test_select.ambr | 12 +- .../ecovacs/snapshots/test_sensor.ambr | 180 +++++++++--------- .../ecovacs/snapshots/test_switch.ambr | 12 +- tests/components/ecovacs/test_button.py | 13 +- tests/components/ecovacs/test_select.py | 6 +- tests/components/ecovacs/test_sensor.py | 18 +- tests/components/ecovacs/test_switch.py | 8 +- 9 files changed, 182 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index f56b65a4e46..7a456483877 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -47,13 +47,13 @@ "name": "Relocate" }, "reset_lifespan_brush": { - "name": "Reset brush lifespan" + "name": "Reset main brush lifespan" }, "reset_lifespan_filter": { "name": "Reset filter lifespan" }, "reset_lifespan_side_brush": { - "name": "Reset side brush lifespan" + "name": "Reset side brushes lifespan" } }, "image": { @@ -79,13 +79,13 @@ } }, "lifespan_brush": { - "name": "Brush lifespan" + "name": "Main brush lifespan" }, "lifespan_filter": { "name": "Filter lifespan" }, "lifespan_side_brush": { - "name": "Side brush lifespan" + "name": "Side brushes lifespan" }, "network_ip": { "name": "IP address" @@ -100,7 +100,7 @@ "name": "Area cleaned" }, "stats_time": { - "name": "Time cleaned" + "name": "Cleaning duration" }, "total_stats_area": { "name": "Total area cleaned" @@ -109,12 +109,12 @@ "name": "Total cleanings" }, "total_stats_time": { - "name": "Total time cleaned" + "name": "Total cleaning duration" } }, "select": { "water_amount": { - "name": "Water amount", + "name": "Water flow level", "state": { "high": "High", "low": "Low", @@ -137,7 +137,7 @@ "name": "Advanced mode" }, "carpet_auto_fan_boost": { - "name": "Carpet auto fan speed boost" + "name": "Carpet auto-boost suction" }, "clean_preference": { "name": "Clean preference" diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr index ca61d16602a..45b7ef1cc51 100644 --- a/tests/components/ecovacs/snapshots/test_button.ambr +++ b/tests/components/ecovacs/snapshots/test_button.ambr @@ -42,49 +42,6 @@ 'state': '2024-01-01T00:00:00+00:00', }) # --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_brush_lifespan:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.ozmo_950_reset_brush_lifespan', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reset brush lifespan', - 'platform': 'ecovacs', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'reset_lifespan_brush', - 'unique_id': 'E1234567890000000001_reset_lifespan_brush', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_brush_lifespan:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Reset brush lifespan', - }), - 'context': , - 'entity_id': 'button.ozmo_950_reset_brush_lifespan', - 'last_changed': , - 'last_updated': , - 'state': '2024-01-01T00:00:00+00:00', - }) -# --- # name: test_buttons[yna5x1][button.ozmo_950_reset_filter_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -128,7 +85,7 @@ 'state': '2024-01-01T00:00:00+00:00', }) # --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brush_lifespan:entity-registry] +# name: test_buttons[yna5x1][button.ozmo_950_reset_main_brush_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -140,7 +97,7 @@ 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.ozmo_950_reset_side_brush_lifespan', + 'entity_id': 'button.ozmo_950_reset_main_brush_lifespan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -150,7 +107,50 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Reset side brush lifespan', + 'original_name': 'Reset main brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_brush', + 'unique_id': 'E1234567890000000001_reset_lifespan_brush', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[yna5x1][button.ozmo_950_reset_main_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ozmo 950 Reset main brush lifespan', + }), + 'context': , + 'entity_id': 'button.ozmo_950_reset_main_brush_lifespan', + 'last_changed': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brushes_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset side brushes lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, @@ -159,13 +159,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brush_lifespan:state] +# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brushes_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Reset side brush lifespan', + 'friendly_name': 'Ozmo 950 Reset side brushes lifespan', }), 'context': , - 'entity_id': 'button.ozmo_950_reset_side_brush_lifespan', + 'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan', 'last_changed': , 'last_updated': , 'state': '2024-01-01T00:00:00+00:00', diff --git a/tests/components/ecovacs/snapshots/test_select.ambr b/tests/components/ecovacs/snapshots/test_select.ambr index abf37a17256..4b01d448fd8 100644 --- a/tests/components/ecovacs/snapshots/test_select.ambr +++ b/tests/components/ecovacs/snapshots/test_select.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_amount:entity-registry] +# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_flow_level:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -18,7 +18,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.ozmo_950_water_amount', + 'entity_id': 'select.ozmo_950_water_flow_level', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -28,7 +28,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Water amount', + 'original_name': 'Water flow level', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, @@ -37,10 +37,10 @@ 'unit_of_measurement': None, }) # --- -# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_amount:state] +# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_flow_level:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Water amount', + 'friendly_name': 'Ozmo 950 Water flow level', 'options': list([ 'low', 'medium', @@ -49,7 +49,7 @@ ]), }), 'context': , - 'entity_id': 'select.ozmo_950_water_amount', + 'entity_id': 'select.ozmo_950_water_flow_level', 'last_changed': , 'last_updated': , 'state': 'ultrahigh', diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 3a59b3ba418..f07722afb53 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -88,7 +88,7 @@ 'state': '100', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_brush_lifespan:entity-registry] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -99,37 +99,41 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.ozmo_950_brush_lifespan', + 'entity_category': None, + 'entity_id': 'sensor.ozmo_950_cleaning_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Brush lifespan', + 'original_name': 'Cleaning duration', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'lifespan_brush', - 'unique_id': 'E1234567890000000001_lifespan_brush', - 'unit_of_measurement': '%', + 'translation_key': 'stats_time', + 'unique_id': 'E1234567890000000001_stats_time', + 'unit_of_measurement': , }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_brush_lifespan:state] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Brush lifespan', - 'unit_of_measurement': '%', + 'device_class': 'duration', + 'friendly_name': 'Ozmo 950 Cleaning duration', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.ozmo_950_brush_lifespan', + 'entity_id': 'sensor.ozmo_950_cleaning_duration', 'last_changed': , 'last_updated': , - 'state': '80', + 'state': '5.0', }) # --- # name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_error:entity-registry] @@ -263,7 +267,7 @@ 'state': '192.168.0.10', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brush_lifespan:entity-registry] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -275,7 +279,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.ozmo_950_side_brush_lifespan', + 'entity_id': 'sensor.ozmo_950_main_brush_lifespan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -285,29 +289,29 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Side brush lifespan', + 'original_name': 'Main brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'lifespan_side_brush', - 'unique_id': 'E1234567890000000001_lifespan_side_brush', + 'translation_key': 'lifespan_brush', + 'unique_id': 'E1234567890000000001_lifespan_brush', 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brush_lifespan:state] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Side brush lifespan', + 'friendly_name': 'Ozmo 950 Main brush lifespan', 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.ozmo_950_side_brush_lifespan', + 'entity_id': 'sensor.ozmo_950_main_brush_lifespan', 'last_changed': , 'last_updated': , - 'state': '40', + 'state': '80', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_time_cleaned:entity-registry] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -318,41 +322,37 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ozmo_950_time_cleaned', + 'entity_category': , + 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Time cleaned', + 'original_name': 'Side brushes lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'stats_time', - 'unique_id': 'E1234567890000000001_stats_time', - 'unit_of_measurement': , + 'translation_key': 'lifespan_side_brush', + 'unique_id': 'E1234567890000000001_lifespan_side_brush', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_time_cleaned:state] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'Ozmo 950 Time cleaned', - 'unit_of_measurement': , + 'friendly_name': 'Ozmo 950 Side brushes lifespan', + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.ozmo_950_time_cleaned', + 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan', 'last_changed': , 'last_updated': , - 'state': '5.0', + 'state': '40', }) # --- # name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_area_cleaned:entity-registry] @@ -402,6 +402,57 @@ 'state': '60', }) # --- +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ozmo_950_total_cleaning_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total cleaning duration', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_time', + 'unique_id': 'E1234567890000000001_total_stats_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Ozmo 950 Total cleaning duration', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ozmo_950_total_cleaning_duration', + 'last_changed': , + 'last_updated': , + 'state': '40.000', + }) +# --- # name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleanings:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -448,57 +499,6 @@ 'state': '123', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_time_cleaned:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ozmo_950_total_time_cleaned', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total time cleaned', - 'platform': 'ecovacs', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_stats_time', - 'unique_id': 'E1234567890000000001_total_stats_time', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_time_cleaned:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'Ozmo 950 Total time cleaned', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ozmo_950_total_time_cleaned', - 'last_changed': , - 'last_updated': , - 'state': '40.000', - }) -# --- # name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_rssi:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ecovacs/snapshots/test_switch.ambr b/tests/components/ecovacs/snapshots/test_switch.ambr index 75441c4f918..c645502a831 100644 --- a/tests/components/ecovacs/snapshots/test_switch.ambr +++ b/tests/components/ecovacs/snapshots/test_switch.ambr @@ -42,7 +42,7 @@ 'state': 'on', }) # --- -# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_fan_speed_boost:entity-registry] +# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_boost_suction:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -54,7 +54,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.ozmo_950_carpet_auto_fan_speed_boost', + 'entity_id': 'switch.ozmo_950_carpet_auto_boost_suction', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -64,7 +64,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Carpet auto fan speed boost', + 'original_name': 'Carpet auto-boost suction', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, @@ -73,13 +73,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_fan_speed_boost:state] +# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_boost_suction:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Carpet auto fan speed boost', + 'friendly_name': 'Ozmo 950 Carpet auto-boost suction', }), 'context': , - 'entity_id': 'switch.ozmo_950_carpet_auto_fan_speed_boost', + 'entity_id': 'switch.ozmo_950_carpet_auto_boost_suction', 'last_changed': , 'last_updated': , 'state': 'on', diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index f804e813256..24c926b1f77 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -33,13 +33,16 @@ def platforms() -> Platform | list[Platform]: "yna5x1", [ ("button.ozmo_950_relocate", SetRelocationState()), - ("button.ozmo_950_reset_brush_lifespan", ResetLifeSpan(LifeSpan.BRUSH)), + ( + "button.ozmo_950_reset_main_brush_lifespan", + ResetLifeSpan(LifeSpan.BRUSH), + ), ( "button.ozmo_950_reset_filter_lifespan", ResetLifeSpan(LifeSpan.FILTER), ), ( - "button.ozmo_950_reset_side_brush_lifespan", + "button.ozmo_950_reset_side_brushes_lifespan", ResetLifeSpan(LifeSpan.SIDE_BRUSH), ), ], @@ -56,7 +59,7 @@ async def test_buttons( entities: list[tuple[str, Command]], ) -> None: """Test that sensor entity snapshots match.""" - assert sorted(hass.states.async_entity_ids()) == [e[0] for e in entities] + assert hass.states.async_entity_ids() == [e[0] for e in entities] device = controller.devices[0] for entity_id, command in entities: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" @@ -89,9 +92,9 @@ async def test_buttons( ( "yna5x1", [ - "button.ozmo_950_reset_brush_lifespan", + "button.ozmo_950_reset_main_brush_lifespan", "button.ozmo_950_reset_filter_lifespan", - "button.ozmo_950_reset_side_brush_lifespan", + "button.ozmo_950_reset_side_brushes_lifespan", ], ), ], diff --git a/tests/components/ecovacs/test_select.py b/tests/components/ecovacs/test_select.py index cfe34c5a7a6..0d1a5d19116 100644 --- a/tests/components/ecovacs/test_select.py +++ b/tests/components/ecovacs/test_select.py @@ -44,7 +44,7 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): ( "yna5x1", [ - "select.ozmo_950_water_amount", + "select.ozmo_950_water_flow_level", ], ), ], @@ -58,7 +58,7 @@ async def test_selects( entity_ids: list[str], ) -> None: """Test that select entity snapshots match.""" - assert entity_ids == sorted(hass.states.async_entity_ids()) + assert entity_ids == hass.states.async_entity_ids() for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN @@ -83,7 +83,7 @@ async def test_selects( [ ( "yna5x1", - "select.ozmo_950_water_amount", + "select.ozmo_950_water_flow_level", "ultrahigh", "low", SetWaterInfo(WaterAmount.LOW), diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 18d65349fa2..78755668f0f 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -54,18 +54,18 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): "yna5x1", [ "sensor.ozmo_950_area_cleaned", - "sensor.ozmo_950_battery", - "sensor.ozmo_950_brush_lifespan", - "sensor.ozmo_950_error", - "sensor.ozmo_950_filter_lifespan", - "sensor.ozmo_950_ip_address", - "sensor.ozmo_950_side_brush_lifespan", - "sensor.ozmo_950_time_cleaned", + "sensor.ozmo_950_cleaning_duration", "sensor.ozmo_950_total_area_cleaned", + "sensor.ozmo_950_total_cleaning_duration", "sensor.ozmo_950_total_cleanings", - "sensor.ozmo_950_total_time_cleaned", + "sensor.ozmo_950_battery", + "sensor.ozmo_950_ip_address", "sensor.ozmo_950_wi_fi_rssi", "sensor.ozmo_950_wi_fi_ssid", + "sensor.ozmo_950_main_brush_lifespan", + "sensor.ozmo_950_filter_lifespan", + "sensor.ozmo_950_side_brushes_lifespan", + "sensor.ozmo_950_error", ], ), ], @@ -79,7 +79,7 @@ async def test_sensors( entity_ids: list[str], ) -> None: """Test that sensor entity snapshots match.""" - assert entity_ids == sorted(hass.states.async_entity_ids()) + assert entity_ids == hass.states.async_entity_ids() for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN diff --git a/tests/components/ecovacs/test_switch.py b/tests/components/ecovacs/test_switch.py index 43c5d25e18f..35d2f487b95 100644 --- a/tests/components/ecovacs/test_switch.py +++ b/tests/components/ecovacs/test_switch.py @@ -69,7 +69,7 @@ class SwitchTestCase: SetContinuousCleaning, ), SwitchTestCase( - "switch.ozmo_950_carpet_auto_fan_speed_boost", + "switch.ozmo_950_carpet_auto_boost_suction", CarpetAutoFanBoostEvent(True), SetCarpetAutoFanBoost, ), @@ -90,9 +90,7 @@ async def test_switch_entities( device = controller.devices[0] event_bus = device.events - assert sorted(hass.states.async_entity_ids()) == sorted( - test.entity_id for test in tests - ) + assert hass.states.async_entity_ids() == [test.entity_id for test in tests] for test_case in tests: entity_id = test_case.entity_id assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" @@ -139,7 +137,7 @@ async def test_switch_entities( [ "switch.ozmo_950_advanced_mode", "switch.ozmo_950_continuous_cleaning", - "switch.ozmo_950_carpet_auto_fan_speed_boost", + "switch.ozmo_950_carpet_auto_boost_suction", ], ), ], From 0c3541c194b09d6deea02691756d673916c25139 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Fri, 2 Feb 2024 14:44:50 +0100 Subject: [PATCH 044/125] Add entity description to GPSD (#109320) --- homeassistant/components/gpsd/sensor.py | 46 +++++++++++++++++++------ 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index d5d25397f2a..135d9c6c28f 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -1,6 +1,8 @@ -"""Support for GPSD.""" +"""Sensor platform for GPSD integration.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import logging from typing import Any @@ -15,6 +17,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -24,6 +27,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, + EntityCategory, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv @@ -43,6 +47,28 @@ ATTR_SPEED = "speed" DEFAULT_NAME = "GPS" +_MODE_VALUES = {2: "2d_fix", 3: "3d_fix"} + + +@dataclass(frozen=True, kw_only=True) +class GpsdSensorDescription(SensorEntityDescription): + """Class describing GPSD sensor entities.""" + + value_fn: Callable[[AGPS3mechanism], str | None] + + +SENSOR_TYPES: tuple[GpsdSensorDescription, ...] = ( + GpsdSensorDescription( + key="mode", + translation_key="mode", + name=None, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=list(_MODE_VALUES.values()), + value_fn=lambda agps_thread: _MODE_VALUES.get(agps_thread.data_stream.mode), + ), +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -64,7 +90,9 @@ async def async_setup_entry( config_entry.data[CONF_HOST], config_entry.data[CONF_PORT], config_entry.entry_id, + description, ) + for description in SENSOR_TYPES ] ) @@ -101,23 +129,23 @@ class GpsdSensor(SensorEntity): """Representation of a GPS receiver available via GPSD.""" _attr_has_entity_name = True - _attr_name = None - _attr_translation_key = "mode" - _attr_device_class = SensorDeviceClass.ENUM - _attr_options = ["2d_fix", "3d_fix"] + + entity_description: GpsdSensorDescription def __init__( self, host: str, port: int, unique_id: str, + description: GpsdSensorDescription, ) -> None: """Initialize the GPSD sensor.""" + self.entity_description = description self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, entry_type=DeviceEntryType.SERVICE, ) - self._attr_unique_id = f"{unique_id}-mode" + self._attr_unique_id = f"{unique_id}-{self.entity_description.key}" self.agps_thread = AGPS3mechanism() self.agps_thread.stream_data(host=host, port=port) @@ -126,11 +154,7 @@ class GpsdSensor(SensorEntity): @property def native_value(self) -> str | None: """Return the state of GPSD.""" - if self.agps_thread.data_stream.mode == 3: - return "3d_fix" - if self.agps_thread.data_stream.mode == 2: - return "2d_fix" - return None + return self.entity_description.value_fn(self.agps_thread) @property def extra_state_attributes(self) -> dict[str, Any]: From 9d22f07fc69e30673483f86750063d8b69202a89 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Feb 2024 15:46:59 +0100 Subject: [PATCH 045/125] Use send_json_auto_id in conversation tests (#109354) --- tests/components/conversation/test_init.py | 10 +++------- tests/components/conversation/test_trigger.py | 3 +-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 58e94d27aac..61712761250 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -733,7 +733,7 @@ async def test_ws_api( assert await async_setup_component(hass, "conversation", {}) client = await hass_ws_client(hass) - await client.send_json({"id": 5, "type": "conversation/process", **payload}) + await client.send_json_auto_id({"type": "conversation/process", **payload}) msg = await client.receive_json() @@ -757,18 +757,14 @@ async def test_ws_prepare( client = await hass_ws_client(hass) - msg = { - "id": 5, - "type": "conversation/prepare", - } + msg = {"type": "conversation/prepare"} if agent_id is not None: msg["agent_id"] = agent_id - await client.send_json(msg) + await client.send_json_auto_id(msg) msg = await client.receive_json() assert msg["success"] - assert msg["id"] == 5 # Intents should now be load assert agent._lang_intents.get(hass.config.language) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 26626a04079..74df1b7f8a6 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -105,9 +105,8 @@ async def test_subscribe_trigger_does_not_interfere_with_responses( ) -> None: """Test that subscribing to a trigger from the websocket API does not interfere with responses.""" websocket_client = await hass_ws_client() - await websocket_client.send_json( + await websocket_client.send_json_auto_id( { - "id": 5, "type": "subscribe_trigger", "trigger": {"platform": "conversation", "command": ["test sentence"]}, } From 6b7a9843141cb9ee780a0b7113f7cae5fd2b69b8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Feb 2024 17:30:07 +0100 Subject: [PATCH 046/125] Update frontend to 20240202.0 (#109388) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2b005c7e1ad..039328b9cac 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240131.0"] + "requirements": ["home-assistant-frontend==20240202.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fbb072db9c0..950cae8b322 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.4.0 hass-nabucasa==0.76.0 hassil==1.6.0 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240131.0 +home-assistant-frontend==20240202.0 home-assistant-intents==2024.2.1 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2b1f9f96404..9b386af7103 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ hole==0.8.0 holidays==0.41 # homeassistant.components.frontend -home-assistant-frontend==20240131.0 +home-assistant-frontend==20240202.0 # homeassistant.components.conversation home-assistant-intents==2024.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c09602bf8c..80077ef43d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -855,7 +855,7 @@ hole==0.8.0 holidays==0.41 # homeassistant.components.frontend -home-assistant-frontend==20240131.0 +home-assistant-frontend==20240202.0 # homeassistant.components.conversation home-assistant-intents==2024.2.1 From 7608f0c9ee0f9aa6aa383d0400368e940ba3503e Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 2 Feb 2024 11:31:16 -0500 Subject: [PATCH 047/125] Add independent session in honeywell (#108435) --- .../components/honeywell/__init__.py | 15 ++++++++++----- tests/components/honeywell/conftest.py | 19 +++++++++++++++++++ tests/components/honeywell/test_init.py | 16 ++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c79d99276b1..baabf4ca4d8 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -8,7 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import ( + async_create_clientsession, + async_get_clientsession, +) from .const import ( _LOGGER, @@ -48,9 +51,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b username = config_entry.data[CONF_USERNAME] password = config_entry.data[CONF_PASSWORD] - client = aiosomecomfort.AIOSomeComfort( - username, password, session=async_get_clientsession(hass) - ) + if len(hass.config_entries.async_entries(DOMAIN)) > 1: + session = async_create_clientsession(hass) + else: + session = async_get_clientsession(hass) + + client = aiosomecomfort.AIOSomeComfort(username, password, session=session) try: await client.login() await client.discover() @@ -76,7 +82,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if len(devices) == 0: _LOGGER.debug("No devices found") return False - data = HoneywellData(config_entry.entry_id, client, devices) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = data await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index 876050586d2..5c5b6c0a44a 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -39,6 +39,15 @@ def config_data(): } +@pytest.fixture +def another_config_data(): + """Provide configuration data for tests.""" + return { + CONF_USERNAME: "user2", + CONF_PASSWORD: "fake2", + } + + @pytest.fixture def config_options(): """Provide configuratio options for test.""" @@ -55,6 +64,16 @@ def config_entry(config_data, config_options): ) +@pytest.fixture +def config_entry2(another_config_data, config_options): + """Create a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=another_config_data, + options=config_options, + ) + + @pytest.fixture def device(): """Mock a somecomfort.Device.""" diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index ccfc2c5d264..98578217af6 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -33,6 +33,22 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) - ) # 1 climate entity; 2 sensor entities +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_setup_multiple_entry( + hass: HomeAssistant, config_entry: MockConfigEntry, config_entry2: MockConfigEntry +) -> None: + """Initialize the config entry.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + config_entry2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry2.entry_id) + await hass.async_block_till_done() + assert config_entry2.state is ConfigEntryState.LOADED + + async def test_setup_multiple_thermostats( hass: HomeAssistant, config_entry: MockConfigEntry, From 2c3a952ef8831e1e6ecbc6fd055b5781e29ebb92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 20:34:00 +0100 Subject: [PATCH 048/125] Update elgato to 5.1.2 (#109391) --- homeassistant/components/elgato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index 0671a7adb1d..c68902560b9 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["elgato==5.1.1"], + "requirements": ["elgato==5.1.2"], "zeroconf": ["_elg._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9b386af7103..db70d4836bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -758,7 +758,7 @@ ecoaliface==0.4.0 electrickiwi-api==0.8.5 # homeassistant.components.elgato -elgato==5.1.1 +elgato==5.1.2 # homeassistant.components.eliqonline eliqonline==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80077ef43d5..4f1f5b76775 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -618,7 +618,7 @@ easyenergy==2.1.0 electrickiwi-api==0.8.5 # homeassistant.components.elgato -elgato==5.1.1 +elgato==5.1.2 # homeassistant.components.elkm1 elkm1-lib==2.2.6 From e61864c0b577e33a0b3d48f40b4a8832b6ebd4ac Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 2 Feb 2024 21:24:24 +0100 Subject: [PATCH 049/125] Bump python-kasa to 0.6.2.1 (#109397) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 15748e83737..a479314d649 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -205,5 +205,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.6.2"] + "requirements": ["python-kasa[speedups]==0.6.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index db70d4836bc..dfa5279a62a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2232,7 +2232,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.6.2 +python-kasa[speedups]==0.6.2.1 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f1f5b76775..aabc7fe1eb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.6.2 +python-kasa[speedups]==0.6.2.1 # homeassistant.components.matter python-matter-server==5.4.0 From 430b9cef439cafe37a83341adcc66282a9c5e7dc Mon Sep 17 00:00:00 2001 From: wilburCforce <109390391+wilburCforce@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:42:53 -0600 Subject: [PATCH 050/125] Fix device type in Lutron (#109401) remove testing code --- homeassistant/components/lutron/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index d728cfac890..0bd00177cc1 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -42,7 +42,7 @@ async def async_setup_entry( lights = [] for area_name, device in entry_data.lights: - if device.type == "CEILING_FAN_TYPE2": + if device.type == "CEILING_FAN_TYPE": # If this is a fan, check to see if this entity already exists. # If not, do not create a new one. entity_id = ent_reg.async_get_entity_id( From ae5d4e183a546aa3d3d6bf02fbd7b15e6ccf87a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 14:52:09 -0600 Subject: [PATCH 051/125] Remove remaning ESPHome files from coveragerc (#109400) --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index bcd4e349668..fa0bf2fbd4c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -359,7 +359,6 @@ omit = homeassistant/components/escea/__init__.py homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py - homeassistant/components/esphome/manager.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* homeassistant/components/eufylife_ble/__init__.py From 09ba46ddb9e577c35eab6634d90ca8d377577cb4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Feb 2024 22:10:30 +0100 Subject: [PATCH 052/125] Mask sensitive data in google_assistant logs (#109366) * Mask sensitive data in google_assistant logs * Move common code to homeassistant/util/redact.py * Move to helpers * Add tests * Tweak * Redact additional logs * Fix stale docstring * Don't reveal the length of masked data * Update test --- .../google_assistant/data_redaction.py | 36 +++++++ .../components/google_assistant/helpers.py | 25 +++-- .../components/google_assistant/smart_home.py | 13 ++- homeassistant/helpers/redact.py | 75 +++++++++++++++ .../components/google_assistant/test_http.py | 2 +- tests/helpers/test_redact.py | 94 +++++++++++++++++++ 6 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/google_assistant/data_redaction.py create mode 100644 homeassistant/helpers/redact.py create mode 100644 tests/helpers/test_redact.py diff --git a/homeassistant/components/google_assistant/data_redaction.py b/homeassistant/components/google_assistant/data_redaction.py new file mode 100644 index 00000000000..ae6fe5f7098 --- /dev/null +++ b/homeassistant/components/google_assistant/data_redaction.py @@ -0,0 +1,36 @@ +"""Helpers to redact Google Assistant data when logging.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from homeassistant.core import callback +from homeassistant.helpers.redact import async_redact_data, partial_redact + +REQUEST_MSG_TO_REDACT: dict[str, Callable[[str], str]] = { + "agentUserId": partial_redact, + "uuid": partial_redact, + "webhookId": partial_redact, +} + +RESPONSE_MSG_TO_REDACT = REQUEST_MSG_TO_REDACT | {id: partial_redact} + +SYNC_MSG_TO_REDACT = REQUEST_MSG_TO_REDACT + + +@callback +def async_redact_request_msg(msg: dict[str, Any]) -> dict[str, Any]: + """Mask sensitive data in message.""" + return async_redact_data(msg, REQUEST_MSG_TO_REDACT) + + +@callback +def async_redact_response_msg(msg: dict[str, Any]) -> dict[str, Any]: + """Mask sensitive data in message.""" + return async_redact_data(msg, RESPONSE_MSG_TO_REDACT) + + +@callback +def async_redact_sync_msg(msg: dict[str, Any]) -> dict[str, Any]: + """Mask sensitive data in message.""" + return async_redact_data(msg, SYNC_MSG_TO_REDACT) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index f3d0d24f7c8..d75ebb49509 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -32,6 +32,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import get_url +from homeassistant.helpers.redact import partial_redact from homeassistant.helpers.storage import Store from homeassistant.util.dt import utcnow @@ -48,6 +49,7 @@ from .const import ( STORE_AGENT_USER_IDS, STORE_GOOGLE_LOCAL_WEBHOOK_ID, ) +from .data_redaction import async_redact_request_msg, async_redact_response_msg from .error import SmartHomeError SYNC_DELAY = 15 @@ -332,8 +334,8 @@ class AbstractConfig(ABC): _LOGGER.debug( "Register webhook handler %s for agent user id %s", - webhook_id, - user_agent_id, + partial_redact(webhook_id), + partial_redact(user_agent_id), ) try: webhook.async_register( @@ -348,8 +350,8 @@ class AbstractConfig(ABC): except ValueError: _LOGGER.warning( "Webhook handler %s for agent user id %s is already defined!", - webhook_id, - user_agent_id, + partial_redact(webhook_id), + partial_redact(user_agent_id), ) setup_successful = False break @@ -374,8 +376,8 @@ class AbstractConfig(ABC): webhook_id = self.get_local_webhook_id(agent_user_id) _LOGGER.debug( "Unregister webhook handler %s for agent user id %s", - webhook_id, - agent_user_id, + partial_redact(webhook_id), + partial_redact(agent_user_id), ) webhook.async_unregister(self.hass, webhook_id) @@ -410,7 +412,7 @@ class AbstractConfig(ABC): "Received local message from %s (JS %s):\n%s\n", request.remote, request.headers.get("HA-Cloud-Version", "unknown"), - pprint.pformat(payload), + pprint.pformat(async_redact_request_msg(payload)), ) if (agent_user_id := self.get_local_agent_user_id(webhook_id)) is None: @@ -421,8 +423,8 @@ class AbstractConfig(ABC): "Cannot process request for webhook %s as no linked agent user is" " found:\n%s\n" ), - webhook_id, - pprint.pformat(payload), + partial_redact(webhook_id), + pprint.pformat(async_redact_request_msg(payload)), ) webhook.async_unregister(self.hass, webhook_id) return None @@ -441,7 +443,10 @@ class AbstractConfig(ABC): ) if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("Responding to local message:\n%s\n", pprint.pformat(result)) + _LOGGER.debug( + "Responding to local message:\n%s\n", + pprint.pformat(async_redact_response_msg(result)), + ) return json_response(result) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index b8c57812540..7d8cc752342 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -18,6 +18,11 @@ from .const import ( EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED, ) +from .data_redaction import ( + async_redact_request_msg, + async_redact_response_msg, + async_redact_sync_msg, +) from .error import SmartHomeError from .helpers import GoogleEntity, RequestData, async_get_entities @@ -42,7 +47,11 @@ async def async_handle_message(hass, config, user_id, message, source): response = await _process(hass, data, message) if response and "errorCode" in response["payload"]: - _LOGGER.error("Error handling message %s: %s", message, response["payload"]) + _LOGGER.error( + "Error handling message %s: %s", + async_redact_request_msg(message), + async_redact_response_msg(response["payload"]), + ) return response @@ -118,7 +127,7 @@ async def async_devices_sync( devices = await async_devices_sync_response(hass, data.config, agent_user_id) response = create_sync_response(agent_user_id, devices) - _LOGGER.debug("Syncing entities response: %s", response) + _LOGGER.debug("Syncing entities response: %s", async_redact_sync_msg(response)) return response diff --git a/homeassistant/helpers/redact.py b/homeassistant/helpers/redact.py new file mode 100644 index 00000000000..f8df73b9180 --- /dev/null +++ b/homeassistant/helpers/redact.py @@ -0,0 +1,75 @@ +"""Helpers to redact sensitive data.""" +from __future__ import annotations + +from collections.abc import Callable, Iterable, Mapping +from typing import Any, TypeVar, cast, overload + +from homeassistant.core import callback + +REDACTED = "**REDACTED**" + +_T = TypeVar("_T") +_ValueT = TypeVar("_ValueT") + + +def partial_redact( + x: str | Any, unmasked_prefix: int = 4, unmasked_suffix: int = 4 +) -> str: + """Mask part of a string with *.""" + if not isinstance(x, str): + return REDACTED + + unmasked = unmasked_prefix + unmasked_suffix + if len(x) < unmasked * 2: + return REDACTED + + if not unmasked_prefix and not unmasked_suffix: + return REDACTED + + suffix = x[-unmasked_suffix:] if unmasked_suffix else "" + return f"{x[:unmasked_prefix]}***{suffix}" + + +@overload +def async_redact_data( # type: ignore[overload-overlap] + data: Mapping, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] +) -> dict: + ... + + +@overload +def async_redact_data( + data: _T, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] +) -> _T: + ... + + +@callback +def async_redact_data( + data: _T, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] +) -> _T: + """Redact sensitive data in a dict.""" + if not isinstance(data, (Mapping, list)): + return data + + if isinstance(data, list): + return cast(_T, [async_redact_data(val, to_redact) for val in data]) + + redacted = {**data} + + for key, value in redacted.items(): + if value is None: + continue + if isinstance(value, str) and not value: + continue + if key in to_redact: + if isinstance(to_redact, Mapping): + redacted[key] = to_redact[key](value) + else: + redacted[key] = REDACTED + elif isinstance(value, Mapping): + redacted[key] = async_redact_data(value, to_redact) + elif isinstance(value, list): + redacted[key] = [async_redact_data(item, to_redact) for item in value] + + return cast(_T, redacted) diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index aa7f8472cab..c6589555c3e 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -466,6 +466,6 @@ async def test_async_enable_local_sdk( ) assert resp.status == HTTPStatus.OK assert ( - "Cannot process request for webhook mock_webhook_id as no linked agent user is found:" + "Cannot process request for webhook **REDACTED** as no linked agent user is found:" in caplog.text ) diff --git a/tests/helpers/test_redact.py b/tests/helpers/test_redact.py new file mode 100644 index 00000000000..73461012907 --- /dev/null +++ b/tests/helpers/test_redact.py @@ -0,0 +1,94 @@ +"""Test the data redation helper.""" +from homeassistant.helpers.redact import REDACTED, async_redact_data, partial_redact + + +def test_redact() -> None: + """Test the async_redact_data helper.""" + data = { + "key1": "value1", + "key2": ["value2_a", "value2_b"], + "key3": [["value_3a", "value_3b"], ["value_3c", "value_3d"]], + "key4": { + "key4_1": "value4_1", + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + "key5": None, + "key6": "", + "key7": False, + } + + to_redact = { + "key1", + "key3", + "key4_1", + "key5", + "key6", + "key7", + } + + assert async_redact_data(data, to_redact) == { + "key1": REDACTED, + "key2": ["value2_a", "value2_b"], + "key3": REDACTED, + "key4": { + "key4_1": REDACTED, + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + "key5": None, + "key6": "", + "key7": REDACTED, + } + + +def test_redact_custom_redact_function() -> None: + """Test the async_redact_data helper.""" + data = { + "key1": "val1val1val1val1", + "key2": ["value2_a", "value2_b"], + "key3": [ + ["val_3avalue_3avalue_3a", "value_3bvalue_3bvalue_3b"], + ["value_3cvalue_3cvalue_3c", "value_3dvalue_3dvalue_3d"], + ], + "key4": { + "key4_1": "val4_1val4_1val4_1val4_1", + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + "key5": None, + "key6": "", + "key7": False, + } + + to_redact = { + "key1": partial_redact, + "key3": partial_redact, # Value is a list, will default to REDACTED + "key4_1": partial_redact, + "key5": partial_redact, + "key6": partial_redact, + "key7": partial_redact, # Value is False, will default to REDACTED + } + + assert async_redact_data(data, to_redact) == { + "key1": "val1***val1", + "key2": ["value2_a", "value2_b"], + "key3": REDACTED, + "key4": { + "key4_1": "val4***l4_1", + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + "key5": None, + "key6": "", + "key7": REDACTED, + } + + +def test_partial_redact() -> None: + """Test the partial_redact helper.""" + assert partial_redact(None, 0, 0) == REDACTED + assert partial_redact("short_string") == REDACTED + assert partial_redact("long_enough_string") == "long***ring" + assert partial_redact("long_enough_string", 2, 2) == "lo***ng" + assert partial_redact("long_enough_string", 0, 0) == REDACTED From 480932712425415d28878a0e5dfc4ddc1b8d37c4 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 Feb 2024 22:42:10 +0100 Subject: [PATCH 053/125] Bump aiotankerkoenig to 0.3.0 (#109404) --- homeassistant/components/tankerkoenig/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tankerkoenig/manifest.json b/homeassistant/components/tankerkoenig/manifest.json index bf8896196ef..adea5b96490 100644 --- a/homeassistant/components/tankerkoenig/manifest.json +++ b/homeassistant/components/tankerkoenig/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tankerkoenig", "iot_class": "cloud_polling", "loggers": ["aiotankerkoenig"], - "requirements": ["aiotankerkoenig==0.2.0"] + "requirements": ["aiotankerkoenig==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index dfa5279a62a..13688ee9059 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -377,7 +377,7 @@ aioswitcher==3.4.1 aiosyncthing==0.5.1 # homeassistant.components.tankerkoenig -aiotankerkoenig==0.2.0 +aiotankerkoenig==0.3.0 # homeassistant.components.tractive aiotractive==0.5.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aabc7fe1eb0..8f608ee1c37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,7 @@ aioswitcher==3.4.1 aiosyncthing==0.5.1 # homeassistant.components.tankerkoenig -aiotankerkoenig==0.2.0 +aiotankerkoenig==0.3.0 # homeassistant.components.tractive aiotractive==0.5.6 From e567236cac161f126cdb5f9bd95bc2d31711463e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 2 Feb 2024 23:03:55 +0100 Subject: [PATCH 054/125] Add diagnostics to proximity (#109393) --- .../components/proximity/diagnostics.py | 49 +++++++++++ .../proximity/snapshots/test_diagnostics.ambr | 86 +++++++++++++++++++ .../components/proximity/test_diagnostics.py | 62 +++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 homeassistant/components/proximity/diagnostics.py create mode 100644 tests/components/proximity/snapshots/test_diagnostics.ambr create mode 100644 tests/components/proximity/test_diagnostics.py diff --git a/homeassistant/components/proximity/diagnostics.py b/homeassistant/components/proximity/diagnostics.py new file mode 100644 index 00000000000..ba5e1f53722 --- /dev/null +++ b/homeassistant/components/proximity/diagnostics.py @@ -0,0 +1,49 @@ +"""Diagnostics support for Proximity.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.device_tracker import ATTR_GPS, ATTR_IP, ATTR_MAC +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.components.person import ATTR_USER_ID +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import ProximityDataUpdateCoordinator + +TO_REDACT = { + ATTR_GPS, + ATTR_IP, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_MAC, + ATTR_USER_ID, + "context", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: ProximityDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + diag_data = { + "entry": entry.as_dict(), + } + + tracked_states: dict[str, dict] = {} + for tracked_entity_id in coordinator.tracked_entities: + if (state := hass.states.get(tracked_entity_id)) is None: + continue + tracked_states[tracked_entity_id] = state.as_dict() + + diag_data["data"] = { + "proximity": coordinator.data.proximity, + "entities": coordinator.data.entities, + "entity_mapping": coordinator.entity_mapping, + "tracked_states": async_redact_data(tracked_states, TO_REDACT), + } + return diag_data diff --git a/tests/components/proximity/snapshots/test_diagnostics.ambr b/tests/components/proximity/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..f8f7d9b014e --- /dev/null +++ b/tests/components/proximity/snapshots/test_diagnostics.ambr @@ -0,0 +1,86 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'entities': dict({ + 'device_tracker.test1': dict({ + 'dir_of_travel': None, + 'dist_to_zone': 2218752, + 'is_in_ignored_zone': False, + 'name': 'test1', + }), + 'device_tracker.test2': dict({ + 'dir_of_travel': None, + 'dist_to_zone': 4077309, + 'is_in_ignored_zone': False, + 'name': 'test2', + }), + }), + 'entity_mapping': dict({ + 'device_tracker.test1': list([ + 'sensor.home_test1_distance', + 'sensor.home_test1_direction_of_travel', + ]), + 'device_tracker.test2': list([ + 'sensor.home_test2_distance', + 'sensor.home_test2_direction_of_travel', + ]), + 'device_tracker.test3': list([ + 'sensor.home_test3_distance', + 'sensor.home_test3_direction_of_travel', + ]), + }), + 'proximity': dict({ + 'dir_of_travel': 'unknown', + 'dist_to_zone': 2219, + 'nearest': 'test1', + }), + 'tracked_states': dict({ + 'device_tracker.test1': dict({ + 'attributes': dict({ + 'friendly_name': 'test1', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'context': '**REDACTED**', + 'entity_id': 'device_tracker.test1', + 'state': 'not_home', + }), + 'device_tracker.test2': dict({ + 'attributes': dict({ + 'friendly_name': 'test2', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'context': '**REDACTED**', + 'entity_id': 'device_tracker.test2', + 'state': 'not_home', + }), + }), + }), + 'entry': dict({ + 'data': dict({ + 'ignored_zones': list([ + ]), + 'tolerance': 1, + 'tracked_entities': list([ + 'device_tracker.test1', + 'device_tracker.test2', + 'device_tracker.test3', + ]), + 'zone': 'zone.home', + }), + 'disabled_by': None, + 'domain': 'proximity', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'home', + 'unique_id': 'proximity_home', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/proximity/test_diagnostics.py b/tests/components/proximity/test_diagnostics.py new file mode 100644 index 00000000000..35ecd152a06 --- /dev/null +++ b/tests/components/proximity/test_diagnostics.py @@ -0,0 +1,62 @@ +"""Tests for proximity diagnostics platform.""" +from __future__ import annotations + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.components.proximity.const import ( + CONF_IGNORED_ZONES, + CONF_TOLERANCE, + CONF_TRACKED_ENTITIES, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ZONE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + hass.states.async_set( + "device_tracker.test1", + "not_home", + {"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1}, + ) + hass.states.async_set( + "device_tracker.test2", + "not_home", + {"friendly_name": "test2", "latitude": 150.1, "longitude": 20.1}, + ) + + mock_entry = MockConfigEntry( + domain=DOMAIN, + title="home", + data={ + CONF_ZONE: "zone.home", + CONF_TRACKED_ENTITIES: [ + "device_tracker.test1", + "device_tracker.test2", + "device_tracker.test3", + ], + CONF_IGNORED_ZONES: [], + CONF_TOLERANCE: 1, + }, + unique_id=f"{DOMAIN}_home", + ) + + mock_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state == ConfigEntryState.LOADED + + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_entry + ) == snapshot(exclude=props("entry_id", "last_changed", "last_updated")) From 99fcff47f9f52162f7a691b41b212fa420ddec78 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 Feb 2024 23:04:41 +0100 Subject: [PATCH 055/125] Bump aioelectricitymaps to 0.3.0 (#109399) * Bump aioelectricitymaps to 0.3.0 * Fix tests --- .../components/co2signal/config_flow.py | 9 ++++++--- .../components/co2signal/coordinator.py | 11 +++++++---- .../components/co2signal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/co2signal/test_config_flow.py | 10 +++++----- tests/components/co2signal/test_sensor.py | 18 ++++++++++++------ 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index 2b2aca0b229..a678868ee18 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -4,8 +4,11 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from aioelectricitymaps import ElectricityMaps -from aioelectricitymaps.exceptions import ElectricityMapsError, InvalidToken +from aioelectricitymaps import ( + ElectricityMaps, + ElectricityMapsError, + ElectricityMapsInvalidTokenError, +) import voluptuous as vol from homeassistant import config_entries @@ -146,7 +149,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await fetch_latest_carbon_intensity(self.hass, em, data) - except InvalidToken: + except ElectricityMapsInvalidTokenError: errors["base"] = "invalid_auth" except ElectricityMapsError: errors["base"] = "unknown" diff --git a/homeassistant/components/co2signal/coordinator.py b/homeassistant/components/co2signal/coordinator.py index 115c976b465..b06bee38bc4 100644 --- a/homeassistant/components/co2signal/coordinator.py +++ b/homeassistant/components/co2signal/coordinator.py @@ -4,9 +4,12 @@ from __future__ import annotations from datetime import timedelta import logging -from aioelectricitymaps import ElectricityMaps -from aioelectricitymaps.exceptions import ElectricityMapsError, InvalidToken -from aioelectricitymaps.models import CarbonIntensityResponse +from aioelectricitymaps import ( + CarbonIntensityResponse, + ElectricityMaps, + ElectricityMapsError, + ElectricityMapsInvalidTokenError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -43,7 +46,7 @@ class CO2SignalCoordinator(DataUpdateCoordinator[CarbonIntensityResponse]): return await fetch_latest_carbon_intensity( self.hass, self.client, self.config_entry.data ) - except InvalidToken as err: + except ElectricityMapsInvalidTokenError as err: raise ConfigEntryAuthFailed from err except ElectricityMapsError as err: raise UpdateFailed(str(err)) from err diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 87f2b5c2db0..a4cbed00684 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aioelectricitymaps"], - "requirements": ["aioelectricitymaps==0.2.0"] + "requirements": ["aioelectricitymaps==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 13688ee9059..b72327edafb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -233,7 +233,7 @@ aioeagle==1.1.0 aioecowitt==2023.5.0 # homeassistant.components.co2signal -aioelectricitymaps==0.2.0 +aioelectricitymaps==0.3.0 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f608ee1c37..7acb46665bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioeagle==1.1.0 aioecowitt==2023.5.0 # homeassistant.components.co2signal -aioelectricitymaps==0.2.0 +aioelectricitymaps==0.3.0 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 5b1ade1ee49..29ce783f33a 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -1,10 +1,10 @@ """Test the CO2 Signal config flow.""" from unittest.mock import AsyncMock, patch -from aioelectricitymaps.exceptions import ( - ElectricityMapsDecodeError, +from aioelectricitymaps import ( + ElectricityMapsConnectionError, ElectricityMapsError, - InvalidToken, + ElectricityMapsInvalidTokenError, ) import pytest @@ -134,11 +134,11 @@ async def test_form_country(hass: HomeAssistant) -> None: ("side_effect", "err_code"), [ ( - InvalidToken, + ElectricityMapsInvalidTokenError, "invalid_auth", ), (ElectricityMapsError("Something else"), "unknown"), - (ElectricityMapsDecodeError("Boom"), "unknown"), + (ElectricityMapsConnectionError("Boom"), "unknown"), ], ids=[ "invalid auth", diff --git a/tests/components/co2signal/test_sensor.py b/tests/components/co2signal/test_sensor.py index b79c8e04c23..4d663e1026b 100644 --- a/tests/components/co2signal/test_sensor.py +++ b/tests/components/co2signal/test_sensor.py @@ -2,10 +2,11 @@ from datetime import timedelta from unittest.mock import AsyncMock -from aioelectricitymaps.exceptions import ( - ElectricityMapsDecodeError, +from aioelectricitymaps import ( + ElectricityMapsConnectionError, + ElectricityMapsConnectionTimeoutError, ElectricityMapsError, - InvalidToken, + ElectricityMapsInvalidTokenError, ) from freezegun.api import FrozenDateTimeFactory import pytest @@ -42,7 +43,8 @@ async def test_sensor( @pytest.mark.parametrize( "error", [ - ElectricityMapsDecodeError, + ElectricityMapsConnectionTimeoutError, + ElectricityMapsConnectionError, ElectricityMapsError, Exception, ], @@ -93,8 +95,12 @@ async def test_sensor_reauth_triggered( assert (state := hass.states.get("sensor.electricity_maps_co2_intensity")) assert state.state == "45.9862319009581" - electricity_maps.latest_carbon_intensity_by_coordinates.side_effect = InvalidToken - electricity_maps.latest_carbon_intensity_by_country_code.side_effect = InvalidToken + electricity_maps.latest_carbon_intensity_by_coordinates.side_effect = ( + ElectricityMapsInvalidTokenError + ) + electricity_maps.latest_carbon_intensity_by_country_code.side_effect = ( + ElectricityMapsInvalidTokenError + ) freezer.tick(timedelta(minutes=20)) async_fire_time_changed(hass) From e0fc328e279d229653755ba37096dd84b8d7613a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:43:32 +0100 Subject: [PATCH 056/125] Add new climate feature flags to flexit_bacnet (#109431) --- homeassistant/components/flexit_bacnet/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flexit_bacnet/climate.py b/homeassistant/components/flexit_bacnet/climate.py index 7740bed73e1..0d8a381a014 100644 --- a/homeassistant/components/flexit_bacnet/climate.py +++ b/homeassistant/components/flexit_bacnet/climate.py @@ -62,13 +62,17 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity): ] _attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: FlexitCoordinator) -> None: """Initialize the Flexit unit.""" From 97e6391b9a97fd43b65949c11185ab2b98652c91 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:46:14 +0100 Subject: [PATCH 057/125] Add migrated climate feature flags to shelly (#109425) --- homeassistant/components/shelly/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 9c43c0b57b8..59343ca6d2f 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -167,6 +167,7 @@ class BlockSleepingClimate( ) _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -448,6 +449,7 @@ class RpcClimate(ShellyRpcEntity, ClimateEntity): ) _attr_target_temperature_step = RPC_THERMOSTAT_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: """Initialize.""" From 3c15a2216d910d22570b1578c312cabe50d3254b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:46:27 +0100 Subject: [PATCH 058/125] Add migrated climate feature flags to homekit_controller (#109433) --- homeassistant/components/homekit_controller/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 8cc4ec569dd..0ca85da3fa2 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -139,6 +139,7 @@ class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity): """The base HomeKit Controller climate entity.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @callback def _async_reconfigure(self) -> None: From 595dd651bb59ec78d96842c746e6340518ef41fd Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 2 Feb 2024 19:13:17 -0600 Subject: [PATCH 059/125] Bump intents to 2024.2.2 (#109412) Bump intents to 2024.2.2 --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ea0a11ae657..1e46170024c 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.0", "home-assistant-intents==2024.2.1"] + "requirements": ["hassil==1.6.0", "home-assistant-intents==2024.2.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 950cae8b322..96efd08bc1f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ hass-nabucasa==0.76.0 hassil==1.6.0 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240202.0 -home-assistant-intents==2024.2.1 +home-assistant-intents==2024.2.2 httpx==0.26.0 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index b72327edafb..154cecbff64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1062,7 +1062,7 @@ holidays==0.41 home-assistant-frontend==20240202.0 # homeassistant.components.conversation -home-assistant-intents==2024.2.1 +home-assistant-intents==2024.2.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7acb46665bb..49c46f237ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -858,7 +858,7 @@ holidays==0.41 home-assistant-frontend==20240202.0 # homeassistant.components.conversation -home-assistant-intents==2024.2.1 +home-assistant-intents==2024.2.2 # homeassistant.components.home_connect homeconnect==0.7.2 From 16b20403e6bc09fbebc6aa2c93b40eff23c0fb0e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:06:55 +0100 Subject: [PATCH 060/125] Add migrated climate feature flags to zha (#109443) --- homeassistant/components/zha/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 40da264d695..cbc759e7008 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -141,6 +141,7 @@ class Thermostat(ZhaEntity, ClimateEntity): _attr_precision = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key: str = "thermostat" + _enable_turn_on_off_backwards_compatibility = False def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): """Initialize ZHA Thermostat instance.""" From 61cf7862a0bedc8339828d6d116e9d16a24a1f46 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:26:04 +0100 Subject: [PATCH 061/125] Adds new climate feature flags in baf (#109476) --- homeassistant/components/baf/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py index 531659e901f..907e8ff2356 100644 --- a/homeassistant/components/baf/climate.py +++ b/homeassistant/components/baf/climate.py @@ -33,10 +33,15 @@ async def async_setup_entry( class BAFAutoComfort(BAFEntity, ClimateEntity): """BAF climate auto comfort.""" - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY] _attr_translation_key = "auto_comfort" + _enable_turn_on_off_backwards_compatibility = False @callback def _async_update_attrs(self) -> None: From ae210886c107d36e4e75560febae35d316f1d3a5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:26:17 +0100 Subject: [PATCH 062/125] Add migrated climate feature flags to nexia (#109472) --- homeassistant/components/nexia/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 32ac8b5320a..63caeb445b7 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -153,6 +153,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Provides Nexia Climate support.""" _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone From 3347a3f8a678fda1ee0c7b9ff332c82873a41682 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 2 Feb 2024 20:26:44 -0600 Subject: [PATCH 063/125] More thorough checks in ESPHome voice assistant UDP server (#109394) * More thorough checks in UDP server * Simplify and change to stop_requested * Check transport --- homeassistant/components/esphome/manager.py | 1 - .../components/esphome/voice_assistant.py | 31 ++-- .../esphome/test_voice_assistant.py | 152 ++++++++++++------ 3 files changed, 122 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index f197574c30a..59f37d3a078 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -352,7 +352,6 @@ class ESPHomeManager: if self.voice_assistant_udp_server is not None: _LOGGER.warning("Voice assistant UDP server was not stopped") self.voice_assistant_udp_server.stop() - self.voice_assistant_udp_server.close() self.voice_assistant_udp_server = None hass = self.hass diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index de6b521d980..7c5c74d58ee 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -1,4 +1,5 @@ """ESPHome voice assistant support.""" + from __future__ import annotations import asyncio @@ -67,7 +68,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): """Receive UDP packets and forward them to the voice assistant.""" started = False - stopped = False + stop_requested = False transport: asyncio.DatagramTransport | None = None remote_addr: tuple[str, int] | None = None @@ -92,6 +93,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): self._tts_done = asyncio.Event() self._tts_task: asyncio.Task | None = None + @property + def is_running(self) -> bool: + """True if the the UDP server is started and hasn't been asked to stop.""" + return self.started and (not self.stop_requested) + async def start_server(self) -> int: """Start accepting connections.""" @@ -99,7 +105,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): """Accept connection.""" if self.started: raise RuntimeError("Can only start once") - if self.stopped: + if self.stop_requested: raise RuntimeError("No longer accepting connections") self.started = True @@ -124,7 +130,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): @callback def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: """Handle incoming UDP packet.""" - if not self.started or self.stopped: + if not self.is_running: return if self.remote_addr is None: self.remote_addr = addr @@ -142,19 +148,19 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): def stop(self) -> None: """Stop the receiver.""" self.queue.put_nowait(b"") - self.started = False - self.stopped = True + self.close() def close(self) -> None: """Close the receiver.""" self.started = False - self.stopped = True + self.stop_requested = True + if self.transport is not None: self.transport.close() async def _iterate_packets(self) -> AsyncIterable[bytes]: """Iterate over incoming packets.""" - if not self.started or self.stopped: + if not self.is_running: raise RuntimeError("Not running") while data := await self.queue.get(): @@ -303,8 +309,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): async def _send_tts(self, media_id: str) -> None: """Send TTS audio to device via UDP.""" + # Always send stream start/end events + self.handle_event(VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {}) + try: - if self.transport is None: + if (not self.is_running) or (self.transport is None): return extension, data = await tts.async_get_media_source_audio( @@ -337,15 +346,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): _LOGGER.debug("Sending %d bytes of audio", audio_bytes_size) - self.handle_event( - VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {} - ) - bytes_per_sample = stt.AudioBitRates.BITRATE_16 // 8 sample_offset = 0 samples_left = audio_bytes_size // bytes_per_sample - while samples_left > 0: + while (samples_left > 0) and self.is_running: bytes_offset = sample_offset * bytes_per_sample chunk: bytes = audio_bytes[bytes_offset : bytes_offset + 1024] samples_in_chunk = len(chunk) // bytes_per_sample diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 38a33bfdec2..f6665c4ad91 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -70,6 +70,19 @@ def voice_assistant_udp_server_v2( return voice_assistant_udp_server(entry=mock_voice_assistant_v2_entry) +@pytest.fixture +def test_wav() -> bytes: + """Return one second of empty WAV audio.""" + with io.BytesIO() as wav_io: + with wave.open(wav_io, "wb") as wav_file: + wav_file.setframerate(16000) + wav_file.setsampwidth(2) + wav_file.setnchannels(1) + wav_file.writeframes(bytes(_ONE_SECOND)) + + return wav_io.getvalue() + + async def test_pipeline_events( hass: HomeAssistant, voice_assistant_udp_server_v1: VoiceAssistantUDPServer, @@ -241,11 +254,13 @@ async def test_udp_server_multiple( ): await voice_assistant_udp_server_v1.start_server() - with patch( - "homeassistant.components.esphome.voice_assistant.UDP_PORT", - new=unused_udp_port_factory(), - ), pytest.raises(RuntimeError): - pass + with ( + patch( + "homeassistant.components.esphome.voice_assistant.UDP_PORT", + new=unused_udp_port_factory(), + ), + pytest.raises(RuntimeError), + ): await voice_assistant_udp_server_v1.start_server() @@ -257,10 +272,13 @@ async def test_udp_server_after_stopped( ) -> None: """Test that the UDP server raises an error if started after stopped.""" voice_assistant_udp_server_v1.close() - with patch( - "homeassistant.components.esphome.voice_assistant.UDP_PORT", - new=unused_udp_port_factory(), - ), pytest.raises(RuntimeError): + with ( + patch( + "homeassistant.components.esphome.voice_assistant.UDP_PORT", + new=unused_udp_port_factory(), + ), + pytest.raises(RuntimeError), + ): await voice_assistant_udp_server_v1.start_server() @@ -362,35 +380,33 @@ async def test_send_tts_not_called_when_empty( async def test_send_tts( hass: HomeAssistant, voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + test_wav, ) -> None: """Test the UDP server calls sendto to transmit audio data to device.""" - with io.BytesIO() as wav_io: - with wave.open(wav_io, "wb") as wav_file: - wav_file.setframerate(16000) - wav_file.setsampwidth(2) - wav_file.setnchannels(1) - wav_file.writeframes(bytes(_ONE_SECOND)) - - wav_bytes = wav_io.getvalue() - with patch( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", wav_bytes), + return_value=("wav", test_wav), ): + voice_assistant_udp_server_v2.started = True voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) - - voice_assistant_udp_server_v2._event_callback( - PipelineEvent( - type=PipelineEventType.TTS_END, - data={ - "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL} - }, + with patch.object( + voice_assistant_udp_server_v2.transport, "is_closing", return_value=False + ): + voice_assistant_udp_server_v2._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": { + "media_id": _TEST_MEDIA_ID, + "url": _TEST_OUTPUT_URL, + } + }, + ) ) - ) - await voice_assistant_udp_server_v2._tts_done.wait() + await voice_assistant_udp_server_v2._tts_done.wait() - voice_assistant_udp_server_v2.transport.sendto.assert_called() + voice_assistant_udp_server_v2.transport.sendto.assert_called() async def test_send_tts_wrong_sample_rate( @@ -400,17 +416,20 @@ async def test_send_tts_wrong_sample_rate( """Test the UDP server calls sendto to transmit audio data to device.""" with io.BytesIO() as wav_io: with wave.open(wav_io, "wb") as wav_file: - wav_file.setframerate(22050) # should be 16000 + wav_file.setframerate(22050) wav_file.setsampwidth(2) wav_file.setnchannels(1) wav_file.writeframes(bytes(_ONE_SECOND)) wav_bytes = wav_io.getvalue() - - with patch( - "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", wav_bytes), - ), pytest.raises(ValueError): + with ( + patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("wav", wav_bytes), + ), + pytest.raises(ValueError), + ): + voice_assistant_udp_server_v2.started = True voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) voice_assistant_udp_server_v2._event_callback( @@ -431,10 +450,14 @@ async def test_send_tts_wrong_format( voice_assistant_udp_server_v2: VoiceAssistantUDPServer, ) -> None: """Test that only WAV audio will be streamed.""" - with patch( - "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("raw", bytes(1024)), - ), pytest.raises(ValueError): + with ( + patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("raw", bytes(1024)), + ), + pytest.raises(ValueError), + ): + voice_assistant_udp_server_v2.started = True voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) voice_assistant_udp_server_v2._event_callback( @@ -450,6 +473,33 @@ async def test_send_tts_wrong_format( await voice_assistant_udp_server_v2._tts_task # raises ValueError +async def test_send_tts_not_started( + hass: HomeAssistant, + voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + test_wav, +) -> None: + """Test the UDP server does not call sendto when not started.""" + with patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("wav", test_wav), + ): + voice_assistant_udp_server_v2.started = False + voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + + voice_assistant_udp_server_v2._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL} + }, + ) + ) + + await voice_assistant_udp_server_v2._tts_done.wait() + + voice_assistant_udp_server_v2.transport.sendto.assert_not_called() + + async def test_wake_word( hass: HomeAssistant, voice_assistant_udp_server_v2: VoiceAssistantUDPServer, @@ -459,11 +509,12 @@ async def test_wake_word( async def async_pipeline_from_audio_stream(*args, start_stage, **kwargs): assert start_stage == PipelineStage.WAKE_WORD - with patch( - "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch( - "asyncio.Event.wait" # TTS wait event + with ( + patch( + "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch("asyncio.Event.wait"), # TTS wait event ): voice_assistant_udp_server_v2.transport = Mock() @@ -515,10 +566,15 @@ async def test_wake_word_abort_exception( async def async_pipeline_from_audio_stream(*args, **kwargs): raise WakeWordDetectionAborted - with patch( - "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch.object(voice_assistant_udp_server_v2, "handle_event") as mock_handle_event: + with ( + patch( + "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch.object( + voice_assistant_udp_server_v2, "handle_event" + ) as mock_handle_event, + ): voice_assistant_udp_server_v2.transport = Mock() await voice_assistant_udp_server_v2.run_pipeline( From 0ef95c2d4bf0bf299c9fbe3593f1a1ca4a017bc5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:30:53 +0100 Subject: [PATCH 064/125] Add migrated ClimateEntityFeatures to advantage_air (#109420) * Add migrated ClimateEntityFeatures to advantage_air * AdvantageAirZone --- homeassistant/components/advantage_air/climate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index d4f3c05902c..870a001a10f 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -83,6 +83,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): _attr_max_temp = 32 _attr_min_temp = 16 _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, instance: AdvantageAirData, ac_key: str) -> None: """Initialize an AdvantageAir AC unit.""" @@ -202,11 +203,16 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): """AdvantageAir MyTemp Zone control.""" _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT_COOL] - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = PRECISION_WHOLE _attr_max_temp = 32 _attr_min_temp = 16 + _enable_turn_on_off_backwards_compatibility = False def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None: """Initialize an AdvantageAir Zone control.""" From 268c84a37957913049093d497ce50f4567cffe02 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:41:25 +0100 Subject: [PATCH 065/125] Add Mill migrated ClimateEntityFeatures (#109415) --- homeassistant/components/mill/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index d0b15f5d8ff..2e7b22da833 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -99,6 +99,7 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity): ) _attr_target_temperature_step = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: MillDataUpdateCoordinator, heater: mill.Heater From fbb7c9003ff5c8f2f285e534c8232f7db7b3e147 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:47:09 +0100 Subject: [PATCH 066/125] Add new ClimateEntityFeature for Tado (#109416) --- homeassistant/components/tado/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1193638c10e..dd0d6a22a08 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -131,7 +131,10 @@ def create_climate_entity(tado, name: str, zone_id: int, device_info: dict): zone_type = capabilities["type"] support_flags = ( - ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) supported_hvac_modes = [ TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_OFF], @@ -221,6 +224,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): _attr_name = None _attr_translation_key = DOMAIN _available = False + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 1fba47fc8e1367e8d60c93dc2be6fc496acae6ff Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:50:24 +0100 Subject: [PATCH 067/125] Add new climate feature flags to radiotherm (#109466) --- homeassistant/components/radiotherm/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index f5ea14e8f4e..4ab57fd6821 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -106,6 +106,7 @@ class RadioThermostat(RadioThermostatEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT _attr_precision = PRECISION_HALVES _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: """Initialize the thermostat.""" @@ -113,7 +114,10 @@ class RadioThermostat(RadioThermostatEntity, ClimateEntity): self._attr_unique_id = self.init_data.mac self._attr_fan_modes = CT30_FAN_OPERATION_LIST self._attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) if not isinstance(self.device, radiotherm.thermostat.CT80): return From 79bcf60c737e2ca54f2f9091c3f498e55aac86df Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:56:57 +0100 Subject: [PATCH 068/125] Adds migrated ClimateEntityFeature to Netatmo (#109418) --- homeassistant/components/netatmo/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 721e453e834..db12efb2f01 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -190,6 +190,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _attr_supported_features = SUPPORT_FLAGS _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, netatmo_device: NetatmoRoom) -> None: """Initialize the sensor.""" From 5c3707ec9ce9d5dda2abb9cdb0151ba8cdc90a12 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 04:02:19 +0100 Subject: [PATCH 069/125] Add new climate feature flags to airzone (#109423) --- homeassistant/components/airzone/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index f5a0e1b109e..2b4cae18086 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -117,6 +117,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): _attr_name = None _speeds: dict[int, str] = {} _speeds_reverse: dict[str, int] = {} + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -129,7 +130,11 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): super().__init__(coordinator, entry, system_zone_id, zone_data) self._attr_unique_id = f"{self._attr_unique_id}_{system_zone_id}" - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) self._attr_target_temperature_step = API_TEMPERATURE_STEP self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[ self.get_airzone_value(AZD_TEMP_UNIT) From ab5163fb5e3dc1b741ec355e94fd7bb1224496a7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:32:47 -0500 Subject: [PATCH 070/125] Add migrated climate entity features to flexit (#109430) --- homeassistant/components/flexit/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index b833617f2ca..85d5e9f4eac 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -69,6 +69,7 @@ class Flexit(ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, hub: ModbusHub, modbus_slave: int | None, name: str | None From 974cee2349d5a90af990446e9c2f9ea80f33687f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:03 -0500 Subject: [PATCH 071/125] Add migrated feature flags to vera (#109438) --- homeassistant/components/vera/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 85c1851b20e..93d0fbf2aee 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -53,6 +53,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False def __init__( self, vera_device: veraApi.VeraThermostat, controller_data: ControllerData From fe25975cab332552e8496c2d13c6c304eb7a062e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:42 -0500 Subject: [PATCH 072/125] Adds migrated climate entity feature for velbus (#109435) --- homeassistant/components/velbus/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index ecdddd19289..9afbfc683a8 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -41,6 +41,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_preset_modes = list(PRESET_MODES) + _enable_turn_on_off_backwards_compatibility = False @property def target_temperature(self) -> float | None: From 63f78dece0078ba843340179c2061e3a2870e638 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:55 -0500 Subject: [PATCH 073/125] Add new climate feature flags to venstar (#109436) --- homeassistant/components/venstar/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 6359cc19e57..a9ee56c4dbb 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -108,6 +108,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF, HVACMode.AUTO] _attr_precision = PRECISION_HALVES _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -130,6 +131,8 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) if self._client.mode == self._client.MODE_AUTO: From bb6051f9c432412b95319de1bae67639662a2e39 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:35:16 -0500 Subject: [PATCH 074/125] Add new climate feature flags to zhong_hong (#109444) --- homeassistant/components/zhong_hong/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 1364dbe107a..fbada765cde 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -128,9 +128,13 @@ class ZhongHongClimate(ClimateEntity): ] _attr_should_poll = False _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, hub, addr_out, addr_in): """Set up the ZhongHong climate devices.""" From 1d3c5d92ea7267a3ebf1aeeeba944bd85264a9c9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:36:19 -0500 Subject: [PATCH 075/125] Add new climate feature flags to whirlpool (#109440) --- homeassistant/components/whirlpool/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 2d38d713859..48b9b99c1e2 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -103,10 +103,13 @@ class AirConEntity(ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_swing_modes = SUPPORTED_SWING_MODES _attr_target_temperature_step = SUPPORTED_TARGET_TEMPERATURE_STEP _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 5462badebf2ba29ff49ff7ebedbdf40ca25ce6be Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:36:43 -0500 Subject: [PATCH 076/125] Add migrated climate feature flags to zwave_me (#109445) --- homeassistant/components/zwave_me/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_me/climate.py b/homeassistant/components/zwave_me/climate.py index 7d654311213..35e0d745619 100644 --- a/homeassistant/components/zwave_me/climate.py +++ b/homeassistant/components/zwave_me/climate.py @@ -56,6 +56,7 @@ class ZWaveMeClimate(ZWaveMeEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _enable_turn_on_off_backwards_compatibility = False def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" From 96a3aac78e9dab112039fc1e93f553e55f877d9a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:37:14 -0500 Subject: [PATCH 077/125] Add new climate feature flags to yolink (#109442) --- homeassistant/components/yolink/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/yolink/climate.py b/homeassistant/components/yolink/climate.py index 6e4495ee0b9..a1e2fdd90a2 100644 --- a/homeassistant/components/yolink/climate.py +++ b/homeassistant/components/yolink/climate.py @@ -62,6 +62,7 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity): """YoLink Climate Entity.""" _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -86,6 +87,8 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity): ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) @callback From 04ce480d652b473b6a3b75a71687ecbb7cd5cc87 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:37:49 -0500 Subject: [PATCH 078/125] Add migrated climate feature flags to teslemetry (#109446) --- homeassistant/components/teslemetry/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index b626d3ef759..748acbb8552 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -45,6 +45,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): | ClimateEntityFeature.PRESET_MODE ) _attr_preset_modes = ["off", "keep", "dog", "camp"] + _enable_turn_on_off_backwards_compatibility = False @property def hvac_mode(self) -> HVACMode | None: From f40e2ecb95a6a987040e94cf34fe26b6854d446c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:38:40 -0500 Subject: [PATCH 079/125] Add migrated climate feature flags for tessie (#109447) --- homeassistant/components/tessie/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tessie/climate.py b/homeassistant/components/tessie/climate.py index d143771ee2c..8eb69d619ff 100644 --- a/homeassistant/components/tessie/climate.py +++ b/homeassistant/components/tessie/climate.py @@ -56,6 +56,7 @@ class TessieClimateEntity(TessieEntity, ClimateEntity): TessieClimateKeeper.DOG, TessieClimateKeeper.CAMP, ] + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From e308dcf398ec9493fbe2564cfbe0036219a9e480 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:39:18 -0500 Subject: [PATCH 080/125] Add new climate feature flags to tfiac (#109448) --- homeassistant/components/tfiac/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 2e764b5c637..7e5999b7f02 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -83,8 +83,11 @@ class TfiacClimate(ClimateEntity): ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + _enable_turn_on_off_backwards_compatibility = False def __init__(self, hass, client): """Init class.""" From 26caa85179ef2a27ba562b1e27df10a647867c0b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:39:37 -0500 Subject: [PATCH 081/125] Add migrated climate feature flags to tolo (#109449) --- homeassistant/components/tolo/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 05afce41ff3..033a4c5b51c 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -58,6 +58,7 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity): ) _attr_target_temperature_step = 1 _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry From 3c8bfce3a4fa2238d362617f5816b2a4cf52c6ce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:40:17 -0500 Subject: [PATCH 082/125] Add migrated climate feature flags to toon (#109450) --- homeassistant/components/toon/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index cc51bb03fec..16fbdbdd356 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -51,6 +51,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From a6b912b282e91495101e06146dbaebf3d0bf3e45 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:40:50 -0500 Subject: [PATCH 083/125] Add migrated climate feature flags to xs1 (#109441) --- homeassistant/components/xs1/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 4c4f6682ffa..949d2330347 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -54,6 +54,7 @@ class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device, sensor): """Initialize the actuator.""" From a69fe882ff202a616361789d3c8900c53682259a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:41:08 -0500 Subject: [PATCH 084/125] Add migrated climate feature flags to touchline (#109451) --- homeassistant/components/touchline/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index ed3d4500db1..5004646a667 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -69,6 +69,7 @@ class Touchline(ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, touchline_thermostat): """Initialize the Touchline device.""" From b7f2ae4e3ae10ae9e11a55ec46cb7ff60dac2337 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:41:28 -0500 Subject: [PATCH 085/125] Add migrated climate feature flags to schluter (#109452) --- homeassistant/components/schluter/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index c8c0d76690d..5d747c8f345 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -81,6 +81,7 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, serial_number, api, session_id): """Initialize the thermostat.""" From 88aec4af72a7c58e9d72f96f70c972dfaec8bc7e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:42:37 -0500 Subject: [PATCH 086/125] Add new climate feature flags to screenlogic (#109454) --- homeassistant/components/screenlogic/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index 7cdfbba10c0..6d95f06a49c 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -81,8 +81,12 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity): entity_description: ScreenLogicClimateDescription _attr_hvac_modes = SUPPORTED_MODES _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, entity_description) -> None: """Initialize a ScreenLogic climate entity.""" From de308fbd55b6bb714e6a505e477febe645506842 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:44:04 -0500 Subject: [PATCH 087/125] Add migrated climate feature flags to senz (#109455) --- homeassistant/components/senz/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py index a94941ac642..c921e1ac1da 100644 --- a/homeassistant/components/senz/climate.py +++ b/homeassistant/components/senz/climate.py @@ -45,6 +45,7 @@ class SENZClimate(CoordinatorEntity, ClimateEntity): _attr_min_temp = 5 _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 9325243ef28d6752aa7868ccd4c97a81d65f375b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:45:06 -0500 Subject: [PATCH 088/125] Add new climate feature flags to stiebel_eltron (#109457) --- homeassistant/components/stiebel_eltron/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index 88cce6c52d7..cedd1b3dd90 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -73,9 +73,13 @@ class StiebelEltron(ClimateEntity): _attr_hvac_modes = SUPPORT_HVAC _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, name, ste_data): """Initialize the unit.""" From 7b132dc1897530690c37f668bec344f8c11a9574 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:51:38 -0500 Subject: [PATCH 089/125] Add new climate feature flags to oem (#109461) --- homeassistant/components/oem/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 1b600b25d94..86c770ec82d 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -66,8 +66,13 @@ class ThermostatDevice(ClimateEntity): """Interface class for the oemthermostat module.""" _attr_hvac_modes = SUPPORT_HVAC - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, thermostat, name): """Initialize the device.""" From 9360165ba70243c1238da8044787aef63a1b2d0f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:54:01 -0500 Subject: [PATCH 090/125] Add migrated climate feature flags to opentherm_gw (#109462) --- homeassistant/components/opentherm_gw/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index bcad621eb82..0b9cd1862be 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -84,6 +84,7 @@ class OpenThermClimate(ClimateEntity): _away_state_a = False _away_state_b = False _current_operation: HVACAction | None = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, gw_dev, options): """Initialize the device.""" From c0fd709b3e07c5f4414d6fd5d6b25c0a1cd38cce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 00:42:10 -0500 Subject: [PATCH 091/125] Add migrated climate feature flags to smarttub (#109427) --- homeassistant/components/smarttub/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 9f1802e7327..4921fca022d 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -67,6 +67,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_preset_modes = list(PRESET_MODES.values()) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, spa): """Initialize the entity.""" From 3039616133dba5217daf5b54d04de3a64c7912e3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:14 -0500 Subject: [PATCH 092/125] Adds migrated climate feature flags to nobo_hub (#109473) --- homeassistant/components/nobo_hub/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index 7041d097f3e..ca8ee08885d 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -81,6 +81,7 @@ class NoboZone(ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 1 # Need to poll to get preset change when in HVACMode.AUTO, so can't set _attr_should_poll = False + _enable_turn_on_off_backwards_compatibility = False def __init__(self, zone_id, hub: nobo, override_type) -> None: """Initialize the climate device.""" From 69f5b5e78e5b685a63bc285b331f0d685094fa05 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:34 -0500 Subject: [PATCH 093/125] Adds migrated climate feature flags in nuheat (#109474) --- homeassistant/components/nuheat/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 13a46c0b32f..b2ebbfa8485 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -78,6 +78,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): _attr_has_entity_name = True _attr_name = None _attr_preset_modes = PRESET_MODES + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, thermostat, temperature_unit): """Initialize the thermostat.""" From b990e9663648c2f11c22958a93cd2d89e209449a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:44 -0500 Subject: [PATCH 094/125] Adds new climate feature flags to ambiclimate (#109475) --- homeassistant/components/ambiclimate/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index fc192d8658f..58b2334260e 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -153,10 +153,15 @@ class AmbiclimateEntity(ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 1 - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, heater: AmbiclimateDevice, store: Store[dict[str, Any]]) -> None: """Initialize the thermostat.""" From 3e903495fa5bb0a5e11ae164bfbea16a0534a912 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:59 -0500 Subject: [PATCH 095/125] Adds migrated climate feature flags in balboa (#109477) --- homeassistant/components/balboa/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index 0ca8b1a3acc..b9cce73de75 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -63,6 +63,7 @@ class BalboaClimateEntity(BalboaEntity, ClimateEntity): ) _attr_translation_key = DOMAIN _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, client: SpaClient) -> None: """Initialize the climate entity.""" From d4c0a9a847c71886f62b8fe64ab493399faceb83 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:04:11 -0500 Subject: [PATCH 096/125] Adds new climate feature flags to broadlink (#109479) --- homeassistant/components/broadlink/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/climate.py b/homeassistant/components/broadlink/climate.py index 6937d6bb0da..dd37d270f9e 100644 --- a/homeassistant/components/broadlink/climate.py +++ b/homeassistant/components/broadlink/climate.py @@ -35,9 +35,14 @@ class BroadlinkThermostat(ClimateEntity, BroadlinkEntity): _attr_has_entity_name = True _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF, HVACMode.AUTO] - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: BroadlinkDevice) -> None: """Initialize the climate entity.""" From 366da3e01f421874f7118447096982708ac668a0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:04:26 -0500 Subject: [PATCH 097/125] Adds new climate feature flags to bsblan (#109480) --- homeassistant/components/bsblan/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 609d5ab6e83..511701cb538 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -73,12 +73,16 @@ class BSBLANClimate( _attr_name = None # Determine preset modes _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_preset_modes = PRESET_MODES # Determine hvac modes _attr_hvac_modes = HVAC_MODES + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From eb7d12597648ceb6862030c2e006532494a6c694 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:16 -0500 Subject: [PATCH 098/125] Add migrated climate feature flags to moehlenhoff (#109470) --- homeassistant/components/moehlenhoff_alpha2/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index 23a39084f9f..063628d6d32 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -46,6 +46,7 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL] _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_preset_modes = [PRESET_AUTO, PRESET_DAY, PRESET_NIGHT] + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: Alpha2BaseCoordinator, heat_area_id: str) -> None: """Initialize Alpha2 ClimateEntity.""" From 82a2980cbdb682111acee500a275f9af9042d745 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:33 -0500 Subject: [PATCH 099/125] Adds new climate feature flags to melissa (#109469) --- homeassistant/components/melissa/climate.py | 6 +++++- tests/components/melissa/test_climate.py | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 9facb18ed05..f94c3af6d9a 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -57,9 +57,13 @@ class MelissaClimate(ClimateEntity): _attr_hvac_modes = OP_MODES _attr_supported_features = ( - ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, api, serial_number, init_data): """Initialize the climate device.""" diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 4568eaf2e77..dc2ca4391f1 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -223,7 +223,10 @@ async def test_supported_features(hass: HomeAssistant) -> None: device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) assert thermostat.supported_features == features From 7666c432e48a3a023d75ef3176bdb9111b07f28e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:46 -0500 Subject: [PATCH 100/125] Adds new climate feature flags to maxcube (#109467) --- homeassistant/components/maxcube/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 2ef451b04a7..f3d302fc209 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -68,8 +68,12 @@ class MaxCubeClimate(ClimateEntity): _attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.HEAT] _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, handler, device): """Initialize MAX! Cube ClimateEntity.""" From 8f637d3ca7bd95e4fdeb5321acb52af96ff7c0b3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:06:21 -0500 Subject: [PATCH 101/125] Adds migrated climate feature flags for proliphix (#109465) --- homeassistant/components/proliphix/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 5f841441d59..797fd751197 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -60,6 +60,7 @@ class ProliphixThermostat(ClimateEntity): _attr_precision = PRECISION_TENTHS _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + _enable_turn_on_off_backwards_compatibility = False def __init__(self, pdp): """Initialize the thermostat.""" From 0d881dfc12059d8d47a85f37e3482298e5299ff6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:06:53 -0500 Subject: [PATCH 102/125] Adds new climate feature flags for airzone_cloud (#109424) --- homeassistant/components/airzone_cloud/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index e076edc1f5b..73333d346c5 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -144,8 +144,13 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): """Define an Airzone Cloud climate.""" _attr_name = None - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @callback def _handle_coordinator_update(self) -> None: From 0884215130472fd6138938c692c16b35682defae Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 3 Feb 2024 06:39:35 +0000 Subject: [PATCH 103/125] Bump aiohomekit to 3.1.4 (#109414) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 799058b0e20..1617b907a26 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.1.3"], + "requirements": ["aiohomekit==3.1.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 154cecbff64..7c4e1045123 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.3 +aiohomekit==3.1.4 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49c46f237ab..14cd5e5f8a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.3 +aiohomekit==3.1.4 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 From 53db392150682f44b5171bddf6dbd1ea7dbd52a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Feb 2024 00:47:07 -0600 Subject: [PATCH 104/125] Convert auth token removal websocket api to normal functions (#109432) There was nothing being awaited here anymore --- homeassistant/components/auth/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index f97647fff0e..dd07e137e5e 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -578,6 +578,7 @@ def websocket_refresh_tokens( connection.send_result(msg["id"], tokens) +@callback @websocket_api.websocket_command( { vol.Required("type"): "auth/delete_refresh_token", @@ -585,8 +586,7 @@ def websocket_refresh_tokens( } ) @websocket_api.ws_require_user() -@websocket_api.async_response -async def websocket_delete_refresh_token( +def websocket_delete_refresh_token( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Handle a delete refresh token request.""" @@ -601,6 +601,7 @@ async def websocket_delete_refresh_token( connection.send_result(msg["id"], {}) +@callback @websocket_api.websocket_command( { vol.Required("type"): "auth/delete_all_refresh_tokens", @@ -609,8 +610,7 @@ async def websocket_delete_refresh_token( } ) @websocket_api.ws_require_user() -@websocket_api.async_response -async def websocket_delete_all_refresh_tokens( +def websocket_delete_all_refresh_tokens( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Handle delete all refresh tokens request.""" From 5f1d20c5e23d44b0a64550ab4ab95ab8dcc93236 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 3 Feb 2024 07:50:33 +0100 Subject: [PATCH 105/125] Add new OUIs for tplink (#109437) --- homeassistant/components/tplink/manifest.json | 66 ++++++++++++++- homeassistant/generated/dhcp.py | 82 ++++++++++++++++++- 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index a479314d649..a91e7e5a46f 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -156,6 +156,10 @@ "hostname": "k[lps]*", "macaddress": "54AF97*" }, + { + "hostname": "l[59]*", + "macaddress": "54AF97*" + }, { "hostname": "k[lps]*", "macaddress": "AC15A2*" @@ -177,21 +181,41 @@ "macaddress": "5CE931*" }, { - "hostname": "l5*", + "hostname": "l[59]*", "macaddress": "3C52A1*" }, { "hostname": "l5*", "macaddress": "5C628B*" }, + { + "hostname": "tp*", + "macaddress": "5C628B*" + }, { "hostname": "p1*", "macaddress": "482254*" }, + { + "hostname": "s5*", + "macaddress": "482254*" + }, { "hostname": "p1*", "macaddress": "30DE4B*" }, + { + "hostname": "p1*", + "macaddress": "3C52A1*" + }, + { + "hostname": "tp*", + "macaddress": "3C52A1*" + }, + { + "hostname": "s5*", + "macaddress": "3C52A1*" + }, { "hostname": "l9*", "macaddress": "A842A1*" @@ -199,6 +223,46 @@ { "hostname": "l9*", "macaddress": "3460F9*" + }, + { + "hostname": "hs*", + "macaddress": "704F57*" + }, + { + "hostname": "k[lps]*", + "macaddress": "74DA88*" + }, + { + "hostname": "p3*", + "macaddress": "788CB5*" + }, + { + "hostname": "p1*", + "macaddress": "CC32E5*" + }, + { + "hostname": "k[lps]*", + "macaddress": "CC32E5*" + }, + { + "hostname": "hs*", + "macaddress": "CC32E5*" + }, + { + "hostname": "k[lps]*", + "macaddress": "D80D17*" + }, + { + "hostname": "k[lps]*", + "macaddress": "D84732*" + }, + { + "hostname": "p1*", + "macaddress": "F0A731*" + }, + { + "hostname": "l9*", + "macaddress": "F0A731*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index a087c8ac483..a6722282e35 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -788,6 +788,11 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "k[lps]*", "macaddress": "54AF97*", }, + { + "domain": "tplink", + "hostname": "l[59]*", + "macaddress": "54AF97*", + }, { "domain": "tplink", "hostname": "k[lps]*", @@ -815,7 +820,7 @@ DHCP: list[dict[str, str | bool]] = [ }, { "domain": "tplink", - "hostname": "l5*", + "hostname": "l[59]*", "macaddress": "3C52A1*", }, { @@ -823,16 +828,41 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "l5*", "macaddress": "5C628B*", }, + { + "domain": "tplink", + "hostname": "tp*", + "macaddress": "5C628B*", + }, { "domain": "tplink", "hostname": "p1*", "macaddress": "482254*", }, + { + "domain": "tplink", + "hostname": "s5*", + "macaddress": "482254*", + }, { "domain": "tplink", "hostname": "p1*", "macaddress": "30DE4B*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "3C52A1*", + }, + { + "domain": "tplink", + "hostname": "tp*", + "macaddress": "3C52A1*", + }, + { + "domain": "tplink", + "hostname": "s5*", + "macaddress": "3C52A1*", + }, { "domain": "tplink", "hostname": "l9*", @@ -843,6 +873,56 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "l9*", "macaddress": "3460F9*", }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "704F57*", + }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "74DA88*", + }, + { + "domain": "tplink", + "hostname": "p3*", + "macaddress": "788CB5*", + }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "CC32E5*", + }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "CC32E5*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "CC32E5*", + }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "D80D17*", + }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "D84732*", + }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "F0A731*", + }, + { + "domain": "tplink", + "hostname": "l9*", + "macaddress": "F0A731*", + }, { "domain": "tuya", "macaddress": "105A17*", From a1cbc62ddc44fdc61d9a7be37df61faa0a4c2e0a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 02:03:38 -0500 Subject: [PATCH 106/125] Add new climate feature flags to mysensors (#109471) Adds new climate feature flags to mysensors --- homeassistant/components/mysensors/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index d532135304a..0058fca021e 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -70,11 +70,12 @@ class MySensorsHVAC(mysensors.device.MySensorsChildEntity, ClimateEntity): """Representation of a MySensors HVAC.""" _attr_hvac_modes = OPERATION_LIST + _enable_turn_on_off_backwards_compatibility = False @property def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" - features = ClimateEntityFeature(0) + features = ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON set_req = self.gateway.const.SetReq if set_req.V_HVAC_SPEED in self._values: features = features | ClimateEntityFeature.FAN_MODE From 6f9876d5e0e2b5136d2ce4f1e9d91b65fed8fbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 3 Feb 2024 08:16:28 +0100 Subject: [PATCH 107/125] Extend the history of Elvia history to 3 years (#109490) Extend the history of Elvia data to 3 years --- homeassistant/components/elvia/config_flow.py | 4 ++- homeassistant/components/elvia/importer.py | 36 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/elvia/config_flow.py b/homeassistant/components/elvia/config_flow.py index e65c93b09a6..fb50842e39b 100644 --- a/homeassistant/components/elvia/config_flow.py +++ b/homeassistant/components/elvia/config_flow.py @@ -35,8 +35,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._api_token = api_token = user_input[CONF_API_TOKEN] client = Elvia(meter_value_token=api_token).meter_value() try: + end_time = dt_util.utcnow() results = await client.get_meter_values( - start_time=(dt_util.now() - timedelta(hours=1)).isoformat() + start_time=(end_time - timedelta(hours=1)).isoformat(), + end_time=end_time.isoformat(), ) except ElviaError.AuthError as exception: diff --git a/homeassistant/components/elvia/importer.py b/homeassistant/components/elvia/importer.py index 69e3d64d09d..097db51cab8 100644 --- a/homeassistant/components/elvia/importer.py +++ b/homeassistant/components/elvia/importer.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime, timedelta from typing import TYPE_CHECKING, cast -from elvia import Elvia +from elvia import Elvia, error as ElviaError from homeassistant.components.recorder.models import StatisticData, StatisticMetaData from homeassistant.components.recorder.statistics import ( @@ -68,21 +68,37 @@ class ElviaImporter: ) if not last_stats: - # First time we insert 1 years of data (if available) + # First time we insert 3 years of data (if available) + hourly_data: list[MeterValueTimeSeries] = [] until = dt_util.utcnow() - hourly_data = await self._fetch_hourly_data( - since=until - timedelta(days=365), - until=until, - ) + for year in (3, 2, 1): + try: + year_hours = await self._fetch_hourly_data( + since=until - timedelta(days=365 * year), + until=until - timedelta(days=365 * (year - 1)), + ) + except ElviaError.ElviaException: + # This will raise if the contract have no data for the + # year, we can safely ignore this + continue + hourly_data.extend(year_hours) + if hourly_data is None or len(hourly_data) == 0: + LOGGER.error("No data available for the metering point") return last_stats_time = None _sum = 0.0 else: - hourly_data = await self._fetch_hourly_data( - since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"]), - until=dt_util.utcnow(), - ) + try: + hourly_data = await self._fetch_hourly_data( + since=dt_util.utc_from_timestamp( + last_stats[statistic_id][0]["end"] + ), + until=dt_util.utcnow(), + ) + except ElviaError.ElviaException as err: + LOGGER.error("Error fetching data: %s", err) + return if ( hourly_data is None From fe4dd2cb93d91a6d29e55524e3488f7c2e3d7f96 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 3 Feb 2024 09:00:00 +0100 Subject: [PATCH 108/125] Improve color mode handling in light groups (#109390) * Improve color mode handling in light groups * Update config flow test --- homeassistant/components/group/light.py | 42 ++++++++++++++-------- tests/components/group/test_config_flow.py | 2 +- tests/components/group/test_light.py | 40 ++++++++------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 5a113491891..c8689cdaa1c 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -30,6 +30,7 @@ from homeassistant.components.light import ( ColorMode, LightEntity, LightEntityFeature, + filter_supported_color_modes, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -162,6 +163,9 @@ class LightGroup(GroupEntity, LightEntity): if mode: self.mode = all + self._attr_color_mode = ColorMode.UNKNOWN + self._attr_supported_color_modes = {ColorMode.ONOFF} + async def async_turn_on(self, **kwargs: Any) -> None: """Forward the turn_on command to all lights in the light group.""" data = { @@ -261,26 +265,36 @@ class LightGroup(GroupEntity, LightEntity): effects_count = Counter(itertools.chain(all_effects)) self._attr_effect = effects_count.most_common(1)[0][0] - self._attr_color_mode = None - all_color_modes = list(find_state_attributes(on_states, ATTR_COLOR_MODE)) - if all_color_modes: - # Report the most common color mode, select brightness and onoff last - color_mode_count = Counter(itertools.chain(all_color_modes)) - if ColorMode.ONOFF in color_mode_count: - color_mode_count[ColorMode.ONOFF] = -1 - if ColorMode.BRIGHTNESS in color_mode_count: - color_mode_count[ColorMode.BRIGHTNESS] = 0 - self._attr_color_mode = color_mode_count.most_common(1)[0][0] - - self._attr_supported_color_modes = None + supported_color_modes = {ColorMode.ONOFF} all_supported_color_modes = list( find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) ) if all_supported_color_modes: # Merge all color modes. - self._attr_supported_color_modes = cast( - set[str], set().union(*all_supported_color_modes) + supported_color_modes = filter_supported_color_modes( + cast(set[ColorMode], set().union(*all_supported_color_modes)) ) + self._attr_supported_color_modes = supported_color_modes + + self._attr_color_mode = ColorMode.UNKNOWN + all_color_modes = list(find_state_attributes(on_states, ATTR_COLOR_MODE)) + if all_color_modes: + # Report the most common color mode, select brightness and onoff last + color_mode_count = Counter(itertools.chain(all_color_modes)) + if ColorMode.ONOFF in color_mode_count: + if ColorMode.ONOFF in supported_color_modes: + color_mode_count[ColorMode.ONOFF] = -1 + else: + color_mode_count.pop(ColorMode.ONOFF) + if ColorMode.BRIGHTNESS in color_mode_count: + if ColorMode.BRIGHTNESS in supported_color_modes: + color_mode_count[ColorMode.BRIGHTNESS] = 0 + else: + color_mode_count.pop(ColorMode.BRIGHTNESS) + if color_mode_count: + self._attr_color_mode = color_mode_count.most_common(1)[0][0] + else: + self._attr_color_mode = next(iter(supported_color_modes)) self._attr_supported_features = LightEntityFeature(0) for support in find_state_attributes(states, ATTR_SUPPORTED_FEATURES): diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 7b83ed9eb0d..9db70ca80d1 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -479,7 +479,7 @@ LIGHT_ATTRS = [ "supported_color_modes": ["onoff"], "supported_features": 0, }, - {"color_mode": "onoff"}, + {"color_mode": "unknown"}, ] LOCK_ATTRS = [{"supported_features": 1}, {}] MEDIA_PLAYER_ATTRS = [{"supported_features": 0}, {}] diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 59f0a5b7d55..63f21456066 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -28,9 +28,6 @@ from homeassistant.components.light import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, ColorMode, ) from homeassistant.const import ( @@ -278,10 +275,8 @@ async def test_brightness( entity0.brightness = 255 entity1 = platform.ENTITIES[1] - entity1.supported_features = SUPPORT_BRIGHTNESS - # Set color modes to none to trigger backwards compatibility in LightEntity - entity1.supported_color_modes = None - entity1.color_mode = None + entity1.supported_color_modes = {ColorMode.BRIGHTNESS} + entity1.color_mode = ColorMode.BRIGHTNESS assert await async_setup_component( hass, @@ -352,10 +347,8 @@ async def test_color_hs(hass: HomeAssistant, enable_custom_integrations: None) - entity0.hs_color = (0, 100) entity1 = platform.ENTITIES[1] - entity1.supported_features = SUPPORT_COLOR - # Set color modes to none to trigger backwards compatibility in LightEntity - entity1.supported_color_modes = None - entity1.color_mode = None + entity1.supported_color_modes = {ColorMode.HS} + entity1.color_mode = ColorMode.HS assert await async_setup_component( hass, @@ -703,10 +696,8 @@ async def test_color_temp( entity0.color_temp_kelvin = 2 entity1 = platform.ENTITIES[1] - entity1.supported_features = SUPPORT_COLOR_TEMP - # Set color modes to none to trigger backwards compatibility in LightEntity - entity1.supported_color_modes = None - entity1.color_mode = None + entity1.supported_color_modes = {ColorMode.COLOR_TEMP} + entity1.color_mode = ColorMode.COLOR_TEMP assert await async_setup_component( hass, @@ -846,10 +837,8 @@ async def test_min_max_mireds( entity0._attr_max_color_temp_kelvin = 5 entity1 = platform.ENTITIES[1] - entity1.supported_features = SUPPORT_COLOR_TEMP - # Set color modes to none to trigger backwards compatibility in LightEntity - entity1.supported_color_modes = None - entity1.color_mode = None + entity1.supported_color_modes = {ColorMode.COLOR_TEMP} + entity1.color_mode = ColorMode.COLOR_TEMP entity1._attr_min_color_temp_kelvin = 1 entity1._attr_max_color_temp_kelvin = 1234567890 @@ -1021,15 +1010,15 @@ async def test_supported_color_modes( entity0 = platform.ENTITIES[0] entity0.supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} + entity0.color_mode = ColorMode.UNKNOWN entity1 = platform.ENTITIES[1] entity1.supported_color_modes = {ColorMode.RGBW, ColorMode.RGBWW} + entity1.color_mode = ColorMode.UNKNOWN entity2 = platform.ENTITIES[2] - entity2.supported_features = SUPPORT_BRIGHTNESS - # Set color modes to none to trigger backwards compatibility in LightEntity - entity2.supported_color_modes = None - entity2.color_mode = None + entity2.supported_color_modes = {ColorMode.BRIGHTNESS} + entity2.color_mode = ColorMode.UNKNOWN assert await async_setup_component( hass, @@ -1051,7 +1040,6 @@ async def test_supported_color_modes( state = hass.states.get("light.light_group") assert set(state.attributes[ATTR_SUPPORTED_COLOR_MODES]) == { - "brightness", "color_temp", "hs", "rgbw", @@ -1198,6 +1186,7 @@ async def test_color_mode2( await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP] assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP await hass.services.async_call( @@ -1208,7 +1197,8 @@ async def test_color_mode2( ) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP] + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP async def test_supported_features(hass: HomeAssistant) -> None: From d44b00f8513b24f844095748e073bdd7fecb067d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 3 Feb 2024 09:14:52 +0100 Subject: [PATCH 109/125] Change IoT class for Traccar Client (#109493) --- homeassistant/components/traccar/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 978a0b2f507..c3b9e540ab6 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/traccar", - "iot_class": "local_polling", + "iot_class": "cloud_push", "loggers": ["pytraccar"], "requirements": ["pytraccar==2.0.0", "stringcase==1.2.0"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9ae87cbd706..c49882f4394 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6173,7 +6173,7 @@ "traccar": { "integration_type": "hub", "config_flow": true, - "iot_class": "local_polling", + "iot_class": "cloud_push", "name": "Traccar Client" }, "traccar_server": { From 68797feac5442ca6b9f4730b771d9da31a3ceaa9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 3 Feb 2024 02:20:10 -0600 Subject: [PATCH 110/125] Do not suggest area for portable Sonos speakers (#109350) * Do not suggest area for portable speakers * Update tests * Improve readability, update tests --- homeassistant/components/sonos/entity.py | 6 +++- tests/components/sonos/test_media_player.py | 31 +++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 90cadcdad37..05b69c54c50 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -76,6 +76,10 @@ class SonosEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return information about the device.""" + suggested_area: str | None = None + if not self.speaker.battery_info: + # Only set suggested area for non-portable devices + suggested_area = self.speaker.zone_name return DeviceInfo( identifiers={(DOMAIN, self.soco.uid)}, name=self.speaker.zone_name, @@ -86,7 +90,7 @@ class SonosEntity(Entity): (dr.CONNECTION_UPNP, f"uuid:{self.speaker.uid}"), }, manufacturer="Sonos", - suggested_area=self.speaker.zone_name, + suggested_area=suggested_area, configuration_url=f"http://{self.soco.ip_address}:1400/support/review", ) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index fa37b2210e7..ddf550dc376 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,26 +1,45 @@ """Tests for the Sonos Media Player platform.""" from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + CONNECTION_UPNP, + DeviceRegistry, +) async def test_device_registry( - hass: HomeAssistant, async_autosetup_sonos, soco + hass: HomeAssistant, device_registry: DeviceRegistry, async_autosetup_sonos, soco ) -> None: """Test sonos device registered in the device registry.""" - device_registry = dr.async_get(hass) reg_device = device_registry.async_get_device( identifiers={("sonos", "RINCON_test")} ) + assert reg_device is not None assert reg_device.model == "Model Name" assert reg_device.sw_version == "13.1" assert reg_device.connections == { - (dr.CONNECTION_NETWORK_MAC, "00:11:22:33:44:55"), - (dr.CONNECTION_UPNP, "uuid:RINCON_test"), + (CONNECTION_NETWORK_MAC, "00:11:22:33:44:55"), + (CONNECTION_UPNP, "uuid:RINCON_test"), } assert reg_device.manufacturer == "Sonos" - assert reg_device.suggested_area == "Zone A" assert reg_device.name == "Zone A" + # Default device provides battery info, area should not be suggested + assert reg_device.suggested_area is None + + +async def test_device_registry_not_portable( + hass: HomeAssistant, device_registry: DeviceRegistry, async_setup_sonos, soco +) -> None: + """Test non-portable sonos device registered in the device registry to ensure area suggested.""" + soco.get_battery_info.return_value = {} + await async_setup_sonos() + + reg_device = device_registry.async_get_device( + identifiers={("sonos", "RINCON_test")} + ) + assert reg_device is not None + assert reg_device.suggested_area == "Zone A" async def test_entity_basic( From 28337fb941fd4d83d3cb35afa24174224224bc1f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:07:56 -0500 Subject: [PATCH 111/125] Add new climate feature flags to demo (#109481) --- homeassistant/components/demo/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index b857f98e2da..745a2473939 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -97,6 +97,7 @@ class DemoClimate(ClimateEntity): _attr_name = None _attr_should_poll = False _attr_translation_key = "ubercool" + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -137,6 +138,9 @@ class DemoClimate(ClimateEntity): self._attr_supported_features |= ( ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) self._target_temperature = target_temperature self._target_humidity = target_humidity self._unit_of_measurement = unit_of_measurement From f15aa037ef71dacb3c0b1827d572fdebdb7f33cc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:17:20 -0500 Subject: [PATCH 112/125] Add new climate feature flags to deconz (#109482) --- homeassistant/components/deconz/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index eb1d0d6b672..35a0e810c9e 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -100,6 +100,7 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): TYPE = DOMAIN _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: Thermostat, gateway: DeconzGateway) -> None: """Set up thermostat device.""" @@ -119,7 +120,11 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): HVAC_MODE_TO_DECONZ[item]: item for item in self._attr_hvac_modes } - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) if device.fan_mode: self._attr_supported_features |= ClimateEntityFeature.FAN_MODE From e0f0159304a5b7d08fb08dd27ea657f11a1a5399 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:18:00 -0500 Subject: [PATCH 113/125] Add new climate feature flags to blebox (#109478) --- homeassistant/components/blebox/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index e4ac8985ebd..1350f1f29a2 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -53,8 +53,13 @@ async def async_setup_entry( class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEntity): """Representation of a BleBox climate feature (saunaBox).""" - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @property def hvac_modes(self): From 98892f5b41684a663baa294ffeb95a8aa7c5621e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:18:40 -0500 Subject: [PATCH 114/125] Add new feature flags to melcloud (#109468) --- homeassistant/components/melcloud/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 9d2a4f08257..ed37ff76b76 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -114,6 +114,7 @@ class MelCloudClimate(ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: MelCloudDevice) -> None: """Initialize the climate.""" @@ -137,6 +138,8 @@ class AtaDeviceClimate(MelCloudClimate): ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) def __init__(self, device: MelCloudDevice, ata_device: AtaDevice) -> None: From c233a12946ef2479f1d99d7ca4ac8b0db87030fe Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 3 Feb 2024 11:51:23 +0100 Subject: [PATCH 115/125] Fix Tankerkoenig diagnostics file to use right format (#109494) Fix tankerkoenig diagnostics file --- homeassistant/components/tankerkoenig/diagnostics.py | 6 +++++- .../components/tankerkoenig/snapshots/test_diagnostics.ambr | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tankerkoenig/diagnostics.py b/homeassistant/components/tankerkoenig/diagnostics.py index 811ec07ef19..d5fd7c8cada 100644 --- a/homeassistant/components/tankerkoenig/diagnostics.py +++ b/homeassistant/components/tankerkoenig/diagnostics.py @@ -1,6 +1,7 @@ """Diagnostics support for Tankerkoenig.""" from __future__ import annotations +from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data @@ -27,6 +28,9 @@ async def async_get_config_entry_diagnostics( diag_data = { "entry": async_redact_data(entry.as_dict(), TO_REDACT), - "data": coordinator.data, + "data": { + station_id: asdict(price_info) + for station_id, price_info in coordinator.data.items() + }, } return diag_data diff --git a/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr b/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr index a27a210c46e..f52cb3a88a5 100644 --- a/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr +++ b/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr @@ -3,8 +3,10 @@ dict({ 'data': dict({ '3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8': dict({ - '__type': "", - 'repr': "PriceInfo(status=, e5=1.719, e10=1.659, diesel=1.659)", + 'diesel': 1.659, + 'e10': 1.659, + 'e5': 1.719, + 'status': 'open', }), }), 'entry': dict({ From 5b9a3d5bd5cd00344dca28a96b558781edebe552 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:08:58 -0500 Subject: [PATCH 116/125] Add migrated ClimateEntityFeature to MQTT (#109419) --- homeassistant/components/mqtt/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 3df9db0d5d0..94311eeda61 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -610,6 +610,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED _attr_target_temperature_low: float | None = None _attr_target_temperature_high: float | None = None + _enable_turn_on_off_backwards_compatibility = False @staticmethod def config_schema() -> vol.Schema: From 8bec20ffa702304de4aca8a95f5b5a3c00dc35e4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:11:19 -0500 Subject: [PATCH 117/125] Add ClimateEntityFeatures to Nest (#109417) --- homeassistant/components/nest/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 03fb79eb78e..2d0186b2bfd 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -100,6 +100,7 @@ class ThermostatEntity(ClimateEntity): _attr_has_entity_name = True _attr_should_poll = False _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" @@ -246,7 +247,7 @@ class ThermostatEntity(ClimateEntity): def _get_supported_features(self) -> ClimateEntityFeature: """Compute the bitmap of supported features from the current state.""" - features = ClimateEntityFeature(0) + features = ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON if HVACMode.HEAT_COOL in self.hvac_modes: features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE if HVACMode.HEAT in self.hvac_modes or HVACMode.COOL in self.hvac_modes: From 96feec9cbf9b09a32cd8c0851ea1d76b5c613ce7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:11:39 -0500 Subject: [PATCH 118/125] Add migrated climate feature flags to smartthings (#109426) --- homeassistant/components/smartthings/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 656a198f42b..4c2afa45b7f 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -162,6 +162,8 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): """Define a SmartThings climate entities.""" + _enable_turn_on_off_backwards_compatibility = False + def __init__(self, device): """Init the class.""" super().__init__(device) @@ -343,6 +345,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): """Define a SmartThings Air Conditioner.""" _hvac_modes: list[HVACMode] + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device) -> None: """Init the class.""" From 6c8636ae7bf29ba1916ed1479c26012ec42b6cc9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:13:20 -0500 Subject: [PATCH 119/125] Add new climate feature flags to evohome (#109429) --- homeassistant/components/evohome/climate.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index a1c46f3d331..8b74d31cc0d 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -156,6 +156,7 @@ class EvoClimateEntity(EvoDevice, ClimateEntity): """Base for an evohome Climate device.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @property def hvac_modes(self) -> list[HVACMode]: @@ -190,7 +191,10 @@ class EvoZone(EvoChild, EvoClimateEntity): ] self._attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: @@ -372,6 +376,9 @@ class EvoController(EvoClimateEntity): ] if self._attr_preset_modes: self._attr_supported_features = ClimateEntityFeature.PRESET_MODE + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller. From 29556465de5094ef90e21de8d89409801c62ef78 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:13:52 -0500 Subject: [PATCH 120/125] Add migrated climate feature flags to vicare (#109439) --- homeassistant/components/vicare/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 7c47629530a..ba2665ac083 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -157,6 +157,7 @@ class ViCareClimate(ViCareEntity, ClimateEntity): _attr_preset_modes = list(HA_TO_VICARE_PRESET_HEATING) _current_action: bool | None = None _current_mode: str | None = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From c6ea57458c1f7898e99a6e914eb531c906225ebd Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Sat, 3 Feb 2024 05:14:33 -0600 Subject: [PATCH 121/125] Pass slots to error messages instead of IDs [rework] (#109410) Co-authored-by: tetele --- .../components/conversation/__init__.py | 2 +- .../components/conversation/default_agent.py | 28 ++++--- .../components/conversation/manifest.json | 2 +- homeassistant/helpers/intent.py | 23 +++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/snapshots/test_init.ambr | 12 +-- .../conversation/test_default_agent.py | 84 ++++++++++++------- 9 files changed, 95 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 7ca7fec115f..09b0e8e2310 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -349,7 +349,7 @@ async def websocket_hass_agent_debug( }, # Slot values that would be received by the intent "slots": { # direct access to values - entity_key: entity.value + entity_key: entity.text or entity.value for entity_key, entity in result.entities.items() }, # Extra slot details, such as the originally matched text diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index a2cb3b68041..fb33d87e107 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1,4 +1,5 @@ """Standard conversation implementation for Home Assistant.""" + from __future__ import annotations import asyncio @@ -264,9 +265,11 @@ class DefaultAgent(AbstractConversationAgent): _LOGGER.debug( "Recognized intent '%s' for template '%s' but had unmatched: %s", result.intent.name, - result.intent_sentence.text - if result.intent_sentence is not None - else "", + ( + result.intent_sentence.text + if result.intent_sentence is not None + else "" + ), result.unmatched_entities_list, ) error_response_type, error_response_args = _get_unmatched_response(result) @@ -285,7 +288,8 @@ class DefaultAgent(AbstractConversationAgent): # Slot values to pass to the intent slots = { - entity.name: {"value": entity.value} for entity in result.entities_list + entity.name: {"value": entity.value, "text": entity.text or entity.value} + for entity in result.entities_list } try: @@ -474,9 +478,11 @@ class DefaultAgent(AbstractConversationAgent): for entity_name, entity_value in recognize_result.entities.items() }, # First matched or unmatched state - "state": template.TemplateState(self.hass, state1) - if state1 is not None - else None, + "state": ( + template.TemplateState(self.hass, state1) + if state1 is not None + else None + ), "query": { # Entity states that matched the query (e.g, "on") "matched": [ @@ -734,7 +740,7 @@ class DefaultAgent(AbstractConversationAgent): if not entity: # Default name - entity_names.append((state.name, state.name, context)) + entity_names.append((state.name, state.entity_id, context)) continue if entity.aliases: @@ -742,10 +748,10 @@ class DefaultAgent(AbstractConversationAgent): if not alias.strip(): continue - entity_names.append((alias, alias, context)) + entity_names.append((alias, state.entity_id, context)) # Default name - entity_names.append((state.name, state.name, context)) + entity_names.append((state.name, state.entity_id, context)) # Expose all areas areas = ar.async_get(self.hass) @@ -785,7 +791,7 @@ class DefaultAgent(AbstractConversationAgent): if device_area is None: return None - return {"area": device_area.id} + return {"area": {"value": device_area.id, "text": device_area.name}} def _get_error_text( self, diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 1e46170024c..e4317052b04 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.0", "home-assistant-intents==2024.2.2"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.2.2"] } diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 26468f1fdb7..fe399659a56 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -1,4 +1,5 @@ """Module to coordinate user intentions.""" + from __future__ import annotations import asyncio @@ -401,17 +402,21 @@ class ServiceIntentHandler(IntentHandler): hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - name: str | None = slots.get("name", {}).get("value") - if name == "all": + name_slot = slots.get("name", {}) + entity_id: str | None = name_slot.get("value") + entity_name: str | None = name_slot.get("text") + if entity_id == "all": # Don't match on name if targeting all entities - name = None + entity_id = None # Look up area first to fail early - area_name = slots.get("area", {}).get("value") + area_slot = slots.get("area", {}) + area_id = area_slot.get("value") + area_name = area_slot.get("text") area: area_registry.AreaEntry | None = None - if area_name is not None: + if area_id is not None: areas = area_registry.async_get(hass) - area = areas.async_get_area(area_name) or areas.async_get_area_by_name( + area = areas.async_get_area(area_id) or areas.async_get_area_by_name( area_name ) if area is None: @@ -431,7 +436,7 @@ class ServiceIntentHandler(IntentHandler): states = list( async_match_states( hass, - name=name, + name=entity_id, area=area, domains=domains, device_classes=device_classes, @@ -442,8 +447,8 @@ class ServiceIntentHandler(IntentHandler): if not states: # No states matched constraints raise NoStatesMatchedError( - name=name, - area=area_name, + name=entity_name or entity_id, + area=area_name or area_id, domains=domains, device_classes=device_classes, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 96efd08bc1f..7746745da6b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ ha-av==10.1.1 ha-ffmpeg==3.1.0 habluetooth==2.4.0 hass-nabucasa==0.76.0 -hassil==1.6.0 +hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240202.0 home-assistant-intents==2024.2.2 diff --git a/requirements_all.txt b/requirements_all.txt index 7c4e1045123..6b87dd3d5b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1025,7 +1025,7 @@ hass-nabucasa==0.76.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==1.6.0 +hassil==1.6.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14cd5e5f8a1..f3674dd283c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -830,7 +830,7 @@ habluetooth==2.4.0 hass-nabucasa==0.76.0 # homeassistant.components.conversation -hassil==1.6.0 +hassil==1.6.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 468f3215cb7..034bfafc1f5 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -1397,7 +1397,7 @@ 'name': dict({ 'name': 'name', 'text': 'my cool light', - 'value': 'my cool light', + 'value': 'light.kitchen', }), }), 'intent': dict({ @@ -1422,7 +1422,7 @@ 'name': dict({ 'name': 'name', 'text': 'my cool light', - 'value': 'my cool light', + 'value': 'light.kitchen', }), }), 'intent': dict({ @@ -1498,7 +1498,7 @@ 'sentence_template': '[tell me] how many {on_off_domains:domain} (is|are) {on_off_states:state} [in ]', 'slots': dict({ 'area': 'kitchen', - 'domain': 'light', + 'domain': 'lights', 'state': 'on', }), 'source': 'builtin', @@ -1572,7 +1572,7 @@ 'name': dict({ 'name': 'name', 'text': 'test light', - 'value': 'test light', + 'value': 'light.demo_1234', }), }), 'intent': dict({ @@ -1581,7 +1581,7 @@ 'match': True, 'sentence_template': '[] brightness [to] ', 'slots': dict({ - 'brightness': 100, + 'brightness': '100%', 'name': 'test light', }), 'source': 'builtin', @@ -1604,7 +1604,7 @@ 'name': dict({ 'name': 'name', 'text': 'test light', - 'value': 'test light', + 'value': 'light.demo_1234', }), }), 'intent': dict({ diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index d7182aa3c2f..0cf343a3e20 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -1,4 +1,5 @@ """Test for the default agent.""" + from collections import defaultdict from unittest.mock import AsyncMock, patch @@ -85,8 +86,10 @@ async def test_exposed_areas( entity_registry: er.EntityRegistry, ) -> None: """Test that all areas are exposed.""" - area_kitchen = area_registry.async_get_or_create("kitchen") - area_bedroom = area_registry.async_get_or_create("bedroom") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") entry = MockConfigEntry() entry.add_to_hass(hass) @@ -122,6 +125,9 @@ async def test_exposed_areas( # All is well for the exposed kitchen light assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["area"]["value"] == area_kitchen.id + assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name # Bedroom has no exposed entities result = await conversation.async_converse( @@ -195,7 +201,8 @@ async def test_unexposed_entities_skipped( entity_registry: er.EntityRegistry, ) -> None: """Test that unexposed entities are skipped in exposed areas.""" - area_kitchen = area_registry.async_get_or_create("kitchen") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") # Both lights are in the kitchen exposed_light = entity_registry.async_get_or_create("light", "demo", "1234") @@ -224,6 +231,9 @@ async def test_unexposed_entities_skipped( assert len(calls) == 1 assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["area"]["value"] == area_kitchen.id + assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name # Only one light should be returned hass.states.async_set(exposed_light.entity_id, "on") @@ -314,8 +324,10 @@ async def test_device_area_context( turn_on_calls = async_mock_service(hass, "light", "turn_on") turn_off_calls = async_mock_service(hass, "light", "turn_off") - area_kitchen = area_registry.async_get_or_create("Kitchen") - area_bedroom = area_registry.async_get_or_create("Bedroom") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") # Create 2 lights in each area area_lights = defaultdict(list) @@ -363,13 +375,14 @@ async def test_device_area_context( assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.intent is not None assert result.response.intent.slots["area"]["value"] == area_kitchen.id + assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name # Verify only kitchen lights were targeted assert {s.entity_id for s in result.response.matched_states} == { - e.entity_id for e in area_lights["kitchen"] + e.entity_id for e in area_lights[area_kitchen.id] } assert {c.data["entity_id"][0] for c in turn_on_calls} == { - e.entity_id for e in area_lights["kitchen"] + e.entity_id for e in area_lights[area_kitchen.id] } turn_on_calls.clear() @@ -386,13 +399,14 @@ async def test_device_area_context( assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.intent is not None assert result.response.intent.slots["area"]["value"] == area_bedroom.id + assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name # Verify only bedroom lights were targeted assert {s.entity_id for s in result.response.matched_states} == { - e.entity_id for e in area_lights["bedroom"] + e.entity_id for e in area_lights[area_bedroom.id] } assert {c.data["entity_id"][0] for c in turn_on_calls} == { - e.entity_id for e in area_lights["bedroom"] + e.entity_id for e in area_lights[area_bedroom.id] } turn_on_calls.clear() @@ -409,13 +423,14 @@ async def test_device_area_context( assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.intent is not None assert result.response.intent.slots["area"]["value"] == area_bedroom.id + assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name # Verify only bedroom lights were targeted assert {s.entity_id for s in result.response.matched_states} == { - e.entity_id for e in area_lights["bedroom"] + e.entity_id for e in area_lights[area_bedroom.id] } assert {c.data["entity_id"][0] for c in turn_off_calls} == { - e.entity_id for e in area_lights["bedroom"] + e.entity_id for e in area_lights[area_bedroom.id] } turn_off_calls.clear() @@ -463,7 +478,8 @@ async def test_error_no_device_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: """Test error message when area is missing a device/entity.""" - area_registry.async_get_or_create("kitchen") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") result = await conversation.async_converse( hass, "turn on missing entity in the kitchen", None, Context(), None ) @@ -482,7 +498,7 @@ async def test_error_no_domain( """Test error message when no devices/entities exist for a domain.""" # We don't have a sentence for turning on all fans - fan_domain = MatchEntity(name="domain", value="fan", text="") + fan_domain = MatchEntity(name="domain", value="fan", text="fans") recognize_result = RecognizeResult( intent=Intent("HassTurnOn"), intent_data=IntentData([]), @@ -513,7 +529,8 @@ async def test_error_no_domain_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: """Test error message when no devices/entities for a domain exist in an area.""" - area_registry.async_get_or_create("kitchen") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") result = await conversation.async_converse( hass, "turn on the lights in the kitchen", None, Context(), None ) @@ -526,13 +543,11 @@ async def test_error_no_domain_in_area( ) -async def test_error_no_device_class( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry -) -> None: +async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None: """Test error message when no entities of a device class exist.""" # We don't have a sentence for opening all windows - window_class = MatchEntity(name="device_class", value="window", text="") + window_class = MatchEntity(name="device_class", value="window", text="windows") recognize_result = RecognizeResult( intent=Intent("HassTurnOn"), intent_data=IntentData([]), @@ -563,7 +578,8 @@ async def test_error_no_device_class_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: """Test error message when no entities of a device class exist in an area.""" - area_registry.async_get_or_create("bedroom") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") result = await conversation.async_converse( hass, "open bedroom windows", None, Context(), None ) @@ -600,7 +616,8 @@ async def test_no_states_matched_default_error( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: """Test default response when no states match and slots are missing.""" - area_registry.async_get_or_create("kitchen") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") with patch( "homeassistant.components.conversation.default_agent.intent.async_handle", @@ -629,9 +646,9 @@ async def test_empty_aliases( entity_registry: er.EntityRegistry, ) -> None: """Test that empty aliases are not added to slot lists.""" - area_kitchen = area_registry.async_get_or_create("kitchen") - assert area_kitchen.id is not None - area_registry.async_update(area_kitchen.id, aliases={" "}) + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + area_kitchen = area_registry.async_update(area_kitchen.id, aliases={" "}) entry = MockConfigEntry() entry.add_to_hass(hass) @@ -643,11 +660,16 @@ async def test_empty_aliases( device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id) kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") - entity_registry.async_update_entity( - kitchen_light.entity_id, device_id=kitchen_device.id, aliases={" "} + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, + device_id=kitchen_device.id, + name="kitchen light", + aliases={" "}, ) hass.states.async_set( - kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + kitchen_light.entity_id, + "on", + attributes={ATTR_FRIENDLY_NAME: kitchen_light.name}, ) with patch( @@ -665,16 +687,16 @@ async def test_empty_aliases( assert slot_lists.keys() == {"area", "name"} areas = slot_lists["area"] assert len(areas.values) == 1 - assert areas.values[0].value_out == "kitchen" + assert areas.values[0].value_out == area_kitchen.id + assert areas.values[0].text_in.text == area_kitchen.normalized_name names = slot_lists["name"] assert len(names.values) == 1 - assert names.values[0].value_out == "kitchen light" + assert names.values[0].value_out == kitchen_light.entity_id + assert names.values[0].text_in.text == kitchen_light.name -async def test_all_domains_loaded( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry -) -> None: +async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: """Test that sentences for all domains are always loaded.""" # light domain is not loaded From 33f3fb32d8c9cc248d0359e2a5f814687eae8ab6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:25:52 -0500 Subject: [PATCH 122/125] Add migrated climate feature flags to overkiz (#109463) --- .../overkiz/climate_entities/atlantic_electrical_heater.py | 1 + ...tic_electrical_heater_with_adjustable_temperature_setpoint.py | 1 + .../overkiz/climate_entities/atlantic_electrical_towel_dryer.py | 1 + .../climate_entities/atlantic_heat_recovery_ventilation.py | 1 + .../overkiz/climate_entities/atlantic_pass_apc_heating_zone.py | 1 + .../overkiz/climate_entities/atlantic_pass_apc_zone_control.py | 1 + .../climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py | 1 + .../climate_entities/somfy_heating_temperature_interface.py | 1 + .../components/overkiz/climate_entities/somfy_thermostat.py | 1 + .../climate_entities/valve_heating_temperature_interface.py | 1 + 10 files changed, 10 insertions(+) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py index 867e977276d..2678986574d 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py @@ -53,6 +53,7 @@ class AtlanticElectricalHeater(OverkizEntity, ClimateEntity): ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py index 14237b4601b..36e958fb49c 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py @@ -75,6 +75,7 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint( | ClimateEntityFeature.TURN_ON ) _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py index b053611de9b..fefaa75a114 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py @@ -45,6 +45,7 @@ class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity): _attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ] _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py index 115a30a7c36..5876f7df4a7 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py @@ -54,6 +54,7 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity): | ClimateEntityFeature.TURN_ON ) _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py index 90bc3e40404..25dab7c1d7e 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py @@ -83,6 +83,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py index 1ef0f9bf400..fe9f20b05fc 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py @@ -30,6 +30,7 @@ class AtlanticPassAPCZoneControl(OverkizEntity, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py index 162b9b4fce6..9b956acd014 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py @@ -90,6 +90,7 @@ class HitachiAirToAirHeatPumpHLRRWIFI(OverkizEntity, ClimateEntity): _attr_target_temperature_step = 1.0 _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py index cc470dee032..f98865456e1 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py @@ -81,6 +81,7 @@ class SomfyHeatingTemperatureInterface(OverkizEntity, ClimateEntity): # Both min and max temp values have been retrieved from the Somfy Application. _attr_min_temp = 15.0 _attr_max_temp = 26.0 + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py index 9a81b6d5bd3..2b6840b463d 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py @@ -64,6 +64,7 @@ class SomfyThermostat(OverkizEntity, ClimateEntity): _attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ] _attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False # Both min and max temp values have been retrieved from the Somfy Application. _attr_min_temp = 15.0 diff --git a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py index b58c29a6121..79c360a5f93 100644 --- a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py +++ b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py @@ -58,6 +58,7 @@ class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity): ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator From 3f7d2da35c1877075078f9322f6ad7d09c8a8658 Mon Sep 17 00:00:00 2001 From: Jurriaan Pruis Date: Sat, 3 Feb 2024 12:31:48 +0100 Subject: [PATCH 123/125] Bump matrix-nio to 0.24.0 (#109403) Update matrix-nio to 0.24.0 --- homeassistant/components/matrix/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index a0eb7f3cb5b..0838bcc3764 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.22.1", "Pillow==10.2.0"] + "requirements": ["matrix-nio==0.24.0", "Pillow==10.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6b87dd3d5b0..a501710a518 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1256,7 +1256,7 @@ lxml==5.1.0 mac-vendor-lookup==0.1.12 # homeassistant.components.matrix -matrix-nio==0.22.1 +matrix-nio==0.24.0 # homeassistant.components.maxcube maxcube-api==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3674dd283c..b64fcb944eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ lxml==5.1.0 mac-vendor-lookup==0.1.12 # homeassistant.components.matrix -matrix-nio==0.22.1 +matrix-nio==0.24.0 # homeassistant.components.maxcube maxcube-api==0.4.3 From bb8d7424236b670b2cabdd5132967e4bd7ebb338 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Sat, 3 Feb 2024 12:53:22 +0100 Subject: [PATCH 124/125] Ignore gateway devices in ViCare integration (#106477) * filter unsupported devices * Update __init__.py * use debug * remove dead code --- homeassistant/components/vicare/__init__.py | 23 +++++++++--- .../components/vicare/config_flow.py | 2 +- homeassistant/components/vicare/sensor.py | 35 ------------------- 3 files changed, 19 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 603a42bae41..a2b2f3ac769 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -10,6 +10,7 @@ from typing import Any from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device +from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig from PyViCare.PyViCareUtils import ( PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError, @@ -85,15 +86,16 @@ def setup_vicare_api(hass: HomeAssistant, entry: ConfigEntry) -> None: """Set up PyVicare API.""" vicare_api = vicare_login(hass, entry.data) - for device in vicare_api.devices: - _LOGGER.info( + device_config_list = get_supported_devices(vicare_api.devices) + + for device in device_config_list: + _LOGGER.debug( "Found device: %s (online: %s)", device.getModel(), str(device.isOnline()) ) # Currently we only support a single device - device_list = vicare_api.devices - device = device_list[0] - hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG_LIST] = device_list + device = device_config_list[0] + hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG_LIST] = device_config_list hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG] = device hass.data[DOMAIN][entry.entry_id][VICARE_API] = getattr( device, @@ -113,3 +115,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return unload_ok + + +def get_supported_devices( + devices: list[PyViCareDeviceConfig], +) -> list[PyViCareDeviceConfig]: + """Remove unsupported devices from the list.""" + return [ + device_config + for device_config in devices + if device_config.getModel() not in ["Heatbox1", "Heatbox2_SRC"] + ] diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py index 87bfcf7b146..32ae4af0fe7 100644 --- a/homeassistant/components/vicare/config_flow.py +++ b/homeassistant/components/vicare/config_flow.py @@ -118,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Invoke when a Viessmann MAC address is discovered on the network.""" formatted_mac = format_mac(discovery_info.macaddress) - _LOGGER.info("Found device with mac %s", formatted_mac) + _LOGGER.debug("Found device with mac %s", formatted_mac) await self.async_set_unique_id(formatted_mac) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 6c794b548ad..a8a21c7e787 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -692,41 +692,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ) -def _build_entity( - vicare_api, - device_config: PyViCareDeviceConfig, - entity_description: ViCareSensorEntityDescription, -): - """Create a ViCare sensor entity.""" - if is_supported(entity_description.key, entity_description, vicare_api): - return ViCareSensor( - vicare_api, - device_config, - entity_description, - ) - return None - - -async def _entities_from_descriptions( - hass: HomeAssistant, - entities: list[ViCareSensor], - sensor_descriptions: tuple[ViCareSensorEntityDescription, ...], - iterables, - config_entry: ConfigEntry, -) -> None: - """Create entities from descriptions and list of burners/circuits.""" - for description in sensor_descriptions: - for current in iterables: - entity = await hass.async_add_executor_job( - _build_entity, - current, - hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], - description, - ) - if entity: - entities.append(entity) - - def _build_entities( device: PyViCareDevice, device_config: PyViCareDeviceConfig, From 897ea272d6565cf17b8a6515a4f0511b39a0fc91 Mon Sep 17 00:00:00 2001 From: Cody C <50791984+codyc1515@users.noreply.github.com> Date: Sun, 4 Feb 2024 02:12:55 +1300 Subject: [PATCH 125/125] Update Twinkly DHCP discovery addresses (#109495) --- homeassistant/components/twinkly/manifest.json | 3 +++ homeassistant/generated/dhcp.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index c6ab0bab893..6ec89261b3d 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -6,6 +6,9 @@ "dhcp": [ { "hostname": "twinkly_*" + }, + { + "hostname": "twinkly-*" } ], "documentation": "https://www.home-assistant.io/integrations/twinkly", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index a6722282e35..4f9f822e85e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -971,6 +971,10 @@ DHCP: list[dict[str, str | bool]] = [ "domain": "twinkly", "hostname": "twinkly_*", }, + { + "domain": "twinkly", + "hostname": "twinkly-*", + }, { "domain": "unifiprotect", "macaddress": "B4FBE4*",