From fac8349c37a44f9888c00d3adb8c6d3c1712456a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 12:18:33 +0200 Subject: [PATCH] Add learning offset select to Airgradient (#120532) --- .../components/airgradient/select.py | 68 +++- .../components/airgradient/strings.json | 34 +- .../airgradient/snapshots/test_select.ambr | 366 ++++++++++++++++++ .../airgradient/snapshots/test_sensor.ambr | 48 +-- tests/components/airgradient/test_select.py | 2 +- 5 files changed, 489 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 1cb902a2d3c..c37df0483d1 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -79,6 +79,65 @@ LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( ), ) +LEARNING_TIME_OFFSET_OPTIONS = { + 12: "12", + 60: "60", + 120: "120", + 360: "360", + 720: "720", +} +LEARNING_TIME_OFFSET_OPTIONS_INVERSE = { + v: k for k, v in LEARNING_TIME_OFFSET_OPTIONS.items() +} +ABC_DAYS = { + 8: "8", + 30: "30", + 90: "90", + 180: "180", + 0: "off", +} +ABC_DAYS_INVERSE = {v: k for k, v in ABC_DAYS.items()} + +CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( + AirGradientSelectEntityDescription( + key="nox_index_learning_time_offset", + translation_key="nox_index_learning_time_offset", + options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( + config.nox_learning_offset + ), + set_value_fn=lambda client, value: client.set_nox_learning_offset( + LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + ), + ), + AirGradientSelectEntityDescription( + key="voc_index_learning_time_offset", + translation_key="voc_index_learning_time_offset", + options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( + config.nox_learning_offset + ), + set_value_fn=lambda client, value: client.set_tvoc_learning_offset( + LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + ), + ), + AirGradientSelectEntityDescription( + key="co2_automatic_baseline_calibration", + translation_key="co2_automatic_baseline_calibration", + options=list(ABC_DAYS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: ABC_DAYS.get( + config.co2_automatic_baseline_calibration_days + ), + set_value_fn=lambda client, + value: client.set_co2_automatic_baseline_calibration( + ABC_DAYS_INVERSE.get(value, 0) + ), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -104,7 +163,10 @@ async def async_setup_entry( coordinator.data.configuration_control is ConfigurationControl.LOCAL and not added_entities ): - entities: list[AirGradientSelect] = [] + entities: list[AirGradientSelect] = [ + AirGradientSelect(coordinator, description) + for description in CONTROL_ENTITIES + ] if "I" in model: entities.extend( AirGradientSelect(coordinator, description) @@ -123,7 +185,9 @@ async def async_setup_entry( and added_entities ): entity_registry = er.async_get(hass) - for entity_description in DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES: + for entity_description in ( + DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES + CONTROL_ENTITIES + ): unique_id = f"{coordinator.serial_number}-{entity_description.key}" if entity_id := entity_registry.async_get_entity_id( SELECT_DOMAIN, DOMAIN, unique_id diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 4e8973bdde2..eb529a99ae3 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -69,6 +69,36 @@ "co2": "Carbon dioxide", "pm": "Particulate matter" } + }, + "nox_index_learning_time_offset": { + "name": "NOx index learning offset", + "state": { + "12": "12 hours", + "60": "60 hours", + "120": "120 hours", + "360": "360 hours", + "720": "720 hours" + } + }, + "voc_index_learning_time_offset": { + "name": "VOC index learning offset", + "state": { + "12": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::12%]", + "60": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::60%]", + "120": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::120%]", + "360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]", + "720": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::720%]" + } + }, + "co2_automatic_baseline_calibration": { + "name": "CO2 automatic baseline calibration", + "state": { + "8": "8 days", + "30": "30 days", + "90": "90 days", + "180": "180 days", + "0": "[%key:common::state::off%]" + } } }, "sensor": { @@ -98,10 +128,10 @@ "name": "Carbon dioxide automatic baseline calibration" }, "nox_learning_offset": { - "name": "NOx learning offset" + "name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]" }, "tvoc_learning_offset": { - "name": "VOC learning offset" + "name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]" }, "led_bar_mode": { "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index 409eae52225..19cdc2134fc 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,4 +1,65 @@ # serializer version: 1 +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CO2 automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- # name: test_all_entities[indoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -221,6 +282,189 @@ 'state': 'co2', }) # --- +# name: test_all_entities[indoor][select.airgradient_nox_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_index_learning_time_offset', + 'unique_id': '84fce612f5b8-nox_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_nox_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient NOx index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[indoor][select.airgradient_voc_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voc_index_learning_time_offset', + 'unique_id': '84fce612f5b8-voc_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_voc_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient VOC index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CO2 automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- # name: test_all_entities[outdoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -276,3 +520,125 @@ 'state': 'local', }) # --- +# name: test_all_entities[outdoor][select.airgradient_nox_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_index_learning_time_offset', + 'unique_id': '84fce612f5b8-nox_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_nox_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient NOx index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_voc_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voc_index_learning_time_offset', + 'unique_id': '84fce612f5b8-voc_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_voc_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient VOC index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index c3d14c7d8fc..ff83fdcc111 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -462,7 +462,7 @@ 'state': '1', }) # --- -# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-entry] +# name: test_all_entities[indoor][sensor.airgradient_nox_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -474,7 +474,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -486,7 +486,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'NOx learning offset', + 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -495,15 +495,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-state] +# name: test_all_entities[indoor][sensor.airgradient_nox_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient NOx learning offset', + 'friendly_name': 'Airgradient NOx index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -964,7 +964,7 @@ 'state': '99', }) # --- -# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-entry] +# name: test_all_entities[indoor][sensor.airgradient_voc_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -976,7 +976,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -988,7 +988,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'VOC learning offset', + 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -997,15 +997,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-state] +# name: test_all_entities[indoor][sensor.airgradient_voc_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient VOC learning offset', + 'friendly_name': 'Airgradient VOC index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1109,7 +1109,7 @@ 'state': '1', }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-entry] +# name: test_all_entities[outdoor][sensor.airgradient_nox_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1121,7 +1121,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1133,7 +1133,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'NOx learning offset', + 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -1142,15 +1142,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-state] +# name: test_all_entities[outdoor][sensor.airgradient_nox_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient NOx learning offset', + 'friendly_name': 'Airgradient NOx index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1357,7 +1357,7 @@ 'state': '49', }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-entry] +# name: test_all_entities[outdoor][sensor.airgradient_voc_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1369,7 +1369,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1381,7 +1381,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'VOC learning offset', + 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -1390,15 +1390,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-state] +# name: test_all_entities[outdoor][sensor.airgradient_voc_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient VOC learning offset', + 'friendly_name': 'Airgradient VOC index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index b4294112062..61679a15c07 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -83,7 +83,7 @@ async def test_cloud_creates_no_number( async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 7 mock_cloud_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config_cloud.json", DOMAIN)