diff --git a/homeassistant/components/togrill/__init__.py b/homeassistant/components/togrill/__init__.py index 696b7395f1e2..f7e6568575e7 100644 --- a/homeassistant/components/togrill/__init__.py +++ b/homeassistant/components/togrill/__init__.py @@ -8,7 +8,12 @@ from homeassistant.exceptions import ConfigEntryNotReady from .coordinator import DeviceNotFound, ToGrillConfigEntry, ToGrillCoordinator -_PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.NUMBER] +_PLATFORMS: list[Platform] = [ + Platform.EVENT, + Platform.SELECT, + Platform.SENSOR, + Platform.NUMBER, +] async def async_setup_entry(hass: HomeAssistant, entry: ToGrillConfigEntry) -> bool: diff --git a/homeassistant/components/togrill/icons.json b/homeassistant/components/togrill/icons.json new file mode 100644 index 000000000000..a379bf8d978f --- /dev/null +++ b/homeassistant/components/togrill/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "select": { + "grill_type": { + "default": "mdi:grill", + "state": { + "turkey": "mdi:food-turkey", + "sausage": "mdi:sausage", + "fish": "mdi:fish", + "hamburger": "mdi:hamburger", + "bbq_smoke": "mdi:smoke", + "hot_smoke": "mdi:smoke", + "cold_smoke": "mdi:smoke" + } + }, + "taste": { + "default": "mdi:food-steak" + } + } + } +} diff --git a/homeassistant/components/togrill/select.py b/homeassistant/components/togrill/select.py new file mode 100644 index 000000000000..39644313cf2b --- /dev/null +++ b/homeassistant/components/togrill/select.py @@ -0,0 +1,176 @@ +"""Support for select entities.""" + +from __future__ import annotations + +from collections.abc import Callable, Generator, Mapping +from dataclasses import dataclass +from enum import Enum +from typing import Any, TypeVar + +from togrill_bluetooth.packets import ( + GrillType, + PacketA8Notify, + PacketA303Write, + PacketWrite, + Taste, +) + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import ToGrillConfigEntry +from .const import CONF_PROBE_COUNT, MAX_PROBE_COUNT +from .coordinator import ToGrillCoordinator +from .entity import ToGrillEntity + +PARALLEL_UPDATES = 0 + +OPTION_NONE = "none" + + +@dataclass(kw_only=True, frozen=True) +class ToGrillSelectEntityDescription(SelectEntityDescription): + """Description of entity.""" + + get_value: Callable[[ToGrillCoordinator], str | None] + set_packet: Callable[[ToGrillCoordinator, str], PacketWrite] + entity_supported: Callable[[Mapping[str, Any]], bool] = lambda _: True + probe_number: int | None = None + + +_ENUM = TypeVar("_ENUM", bound=Enum) + + +def _get_enum_from_name(type_: type[_ENUM], value: str) -> _ENUM | None: + """Return enum value or None.""" + if value == OPTION_NONE: + return None + return type_[value.upper()] + + +def _get_enum_from_value(type_: type[_ENUM], value: int | None) -> _ENUM | None: + """Return enum value or None.""" + if value is None: + return None + try: + return type_(value) + except ValueError: + return None + + +def _get_enum_options(type_: type[_ENUM]) -> list[str]: + """Return a list of enum options.""" + values = [OPTION_NONE] + values.extend(option.name.lower() for option in type_) + return values + + +def _get_probe_descriptions( + probe_number: int, +) -> Generator[ToGrillSelectEntityDescription]: + def _get_grill_info( + coordinator: ToGrillCoordinator, + ) -> tuple[GrillType | None, Taste | None]: + if not (packet := coordinator.get_packet(PacketA8Notify, probe_number)): + return None, None + + return _get_enum_from_value(GrillType, packet.grill_type), _get_enum_from_value( + Taste, packet.taste + ) + + def _set_grill_type(coordinator: ToGrillCoordinator, value: str) -> PacketWrite: + _, taste = _get_grill_info(coordinator) + grill_type = _get_enum_from_name(GrillType, value) + return PacketA303Write(probe=probe_number, grill_type=grill_type, taste=taste) + + def _set_taste(coordinator: ToGrillCoordinator, value: str) -> PacketWrite: + grill_type, _ = _get_grill_info(coordinator) + taste = _get_enum_from_name(Taste, value) + return PacketA303Write(probe=probe_number, grill_type=grill_type, taste=taste) + + def _get_grill_type(coordinator: ToGrillCoordinator) -> str | None: + grill_type, _ = _get_grill_info(coordinator) + if grill_type is None: + return OPTION_NONE + return grill_type.name.lower() + + def _get_taste(coordinator: ToGrillCoordinator) -> str | None: + _, taste = _get_grill_info(coordinator) + if taste is None: + return OPTION_NONE + return taste.name.lower() + + yield ToGrillSelectEntityDescription( + key=f"grill_type_{probe_number}", + translation_key="grill_type", + options=_get_enum_options(GrillType), + set_packet=_set_grill_type, + get_value=_get_grill_type, + entity_supported=lambda x: probe_number <= x[CONF_PROBE_COUNT], + probe_number=probe_number, + ) + + yield ToGrillSelectEntityDescription( + key=f"taste_{probe_number}", + translation_key="taste", + options=_get_enum_options(Taste), + set_packet=_set_taste, + get_value=_get_taste, + entity_supported=lambda x: probe_number <= x[CONF_PROBE_COUNT], + probe_number=probe_number, + ) + + +ENTITY_DESCRIPTIONS = ( + *[ + description + for probe_number in range(1, MAX_PROBE_COUNT + 1) + for description in _get_probe_descriptions(probe_number) + ], +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ToGrillConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up select based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + ToGrillSelect(coordinator, entity_description) + for entity_description in ENTITY_DESCRIPTIONS + if entity_description.entity_supported(entry.data) + ) + + +class ToGrillSelect(ToGrillEntity, SelectEntity): + """Representation of a select entity.""" + + entity_description: ToGrillSelectEntityDescription + + def __init__( + self, + coordinator: ToGrillCoordinator, + entity_description: ToGrillSelectEntityDescription, + ) -> None: + """Initialize.""" + + super().__init__(coordinator, probe_number=entity_description.probe_number) + self.entity_description = entity_description + self._attr_unique_id = f"{coordinator.address}_{entity_description.key}" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + + return self.entity_description.get_value(self.coordinator) + + async def async_select_option(self, option: str) -> None: + """Set value on device.""" + + packet = self.entity_description.set_packet(self.coordinator, option) + await self._write_packet(packet) diff --git a/homeassistant/components/togrill/strings.json b/homeassistant/components/togrill/strings.json index 1a748546b756..5461ab52e935 100644 --- a/homeassistant/components/togrill/strings.json +++ b/homeassistant/components/togrill/strings.json @@ -75,6 +75,40 @@ } } } + }, + "select": { + "taste": { + "name": "Taste", + "state": { + "none": "Not set", + "rare": "Rare", + "medium_rare": "Medium rare", + "medium": "Medium", + "medium_well": "Medium well", + "well_done": "Well done" + } + }, + "grill_type": { + "name": "Grill type", + "state": { + "none": "[%key:component::togrill::entity::select::taste::state::none%]", + "beef": "Beef", + "veal": "Veal", + "lamb": "Lamb", + "pork": "Pork", + "turkey": "Turkey", + "chicken": "Chicken", + "sausage": "Sausage", + "fish": "Fish", + "hamburger": "Hamburger", + "bbq_smoke": "BBQ smoke", + "hot_smoke": "Hot smoke", + "cold_smoke": "Cold smoke", + "mark_a": "Mark A", + "mark_b": "Mark B", + "mark_c": "Mark C" + } + } } } } diff --git a/tests/components/togrill/snapshots/test_select.ambr b/tests/components/togrill/snapshots/test_select.ambr new file mode 100644 index 000000000000..7755b51d2f66 --- /dev/null +++ b/tests/components/togrill/snapshots/test_select.ambr @@ -0,0 +1,901 @@ +# serializer version: 1 +# name: test_setup[no_data][select.probe_1_grill_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_1_grill_type', + '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': 'Grill type', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grill_type', + 'unique_id': '00000000-0000-0000-0000-000000000001_grill_type_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[no_data][select.probe_1_grill_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 1 Grill type', + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'context': , + 'entity_id': 'select.probe_1_grill_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[no_data][select.probe_1_taste-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_1_taste', + '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': 'Taste', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'taste', + 'unique_id': '00000000-0000-0000-0000-000000000001_taste_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[no_data][select.probe_1_taste-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 1 Taste', + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'context': , + 'entity_id': 'select.probe_1_taste', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[no_data][select.probe_2_grill_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_2_grill_type', + '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': 'Grill type', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grill_type', + 'unique_id': '00000000-0000-0000-0000-000000000001_grill_type_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[no_data][select.probe_2_grill_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 2 Grill type', + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'context': , + 'entity_id': 'select.probe_2_grill_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[no_data][select.probe_2_taste-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_2_taste', + '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': 'Taste', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'taste', + 'unique_id': '00000000-0000-0000-0000-000000000001_taste_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[no_data][select.probe_2_taste-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 2 Taste', + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'context': , + 'entity_id': 'select.probe_2_taste', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[probes_with_different_data][select.probe_1_grill_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_1_grill_type', + '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': 'Grill type', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grill_type', + 'unique_id': '00000000-0000-0000-0000-000000000001_grill_type_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[probes_with_different_data][select.probe_1_grill_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 1 Grill type', + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'context': , + 'entity_id': 'select.probe_1_grill_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'beef', + }) +# --- +# name: test_setup[probes_with_different_data][select.probe_1_taste-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_1_taste', + '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': 'Taste', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'taste', + 'unique_id': '00000000-0000-0000-0000-000000000001_taste_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[probes_with_different_data][select.probe_1_taste-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 1 Taste', + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'context': , + 'entity_id': 'select.probe_1_taste', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[probes_with_different_data][select.probe_2_grill_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_2_grill_type', + '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': 'Grill type', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grill_type', + 'unique_id': '00000000-0000-0000-0000-000000000001_grill_type_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[probes_with_different_data][select.probe_2_grill_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 2 Grill type', + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'context': , + 'entity_id': 'select.probe_2_grill_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[probes_with_different_data][select.probe_2_taste-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_2_taste', + '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': 'Taste', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'taste', + 'unique_id': '00000000-0000-0000-0000-000000000001_taste_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[probes_with_different_data][select.probe_2_taste-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 2 Taste', + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'context': , + 'entity_id': 'select.probe_2_taste', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[probes_with_unknown_data][select.probe_1_grill_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_1_grill_type', + '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': 'Grill type', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grill_type', + 'unique_id': '00000000-0000-0000-0000-000000000001_grill_type_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[probes_with_unknown_data][select.probe_1_grill_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 1 Grill type', + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'context': , + 'entity_id': 'select.probe_1_grill_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[probes_with_unknown_data][select.probe_1_taste-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_1_taste', + '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': 'Taste', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'taste', + 'unique_id': '00000000-0000-0000-0000-000000000001_taste_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[probes_with_unknown_data][select.probe_1_taste-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 1 Taste', + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'context': , + 'entity_id': 'select.probe_1_taste', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[probes_with_unknown_data][select.probe_2_grill_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_2_grill_type', + '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': 'Grill type', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grill_type', + 'unique_id': '00000000-0000-0000-0000-000000000001_grill_type_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[probes_with_unknown_data][select.probe_2_grill_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 2 Grill type', + 'options': list([ + 'none', + 'beef', + 'veal', + 'lamb', + 'pork', + 'turkey', + 'chicken', + 'sausage', + 'fish', + 'hamburger', + 'bbq_smoke', + 'hot_smoke', + 'cold_smoke', + 'mark_a', + 'mark_b', + 'mark_c', + ]), + }), + 'context': , + 'entity_id': 'select.probe_2_grill_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_setup[probes_with_unknown_data][select.probe_2_taste-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.probe_2_taste', + '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': 'Taste', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'taste', + 'unique_id': '00000000-0000-0000-0000-000000000001_taste_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[probes_with_unknown_data][select.probe_2_taste-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Probe 2 Taste', + 'options': list([ + 'none', + 'rare', + 'medium_rare', + 'medium', + 'medium_well', + 'well_done', + ]), + }), + 'context': , + 'entity_id': 'select.probe_2_taste', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- diff --git a/tests/components/togrill/test_select.py b/tests/components/togrill/test_select.py new file mode 100644 index 000000000000..0a9e858966d3 --- /dev/null +++ b/tests/components/togrill/test_select.py @@ -0,0 +1,172 @@ +"""Test select for ToGrill integration.""" + +from unittest.mock import Mock + +import pytest +from syrupy.assertion import SnapshotAssertion +from togrill_bluetooth.packets import ( + GrillType, + PacketA0Notify, + PacketA8Notify, + PacketA303Write, + Taste, +) + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import TOGRILL_SERVICE_INFO, setup_entry + +from tests.common import MockConfigEntry, snapshot_platform +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.mark.parametrize( + "packets", + [ + pytest.param([], id="no_data"), + pytest.param( + [ + PacketA0Notify( + battery=45, + version_major=1, + version_minor=5, + function_type=1, + probe_count=2, + ambient=False, + alarm_interval=5, + alarm_sound=True, + ), + PacketA8Notify( + probe=1, + alarm_type=0, + grill_type=1, + ), + PacketA8Notify( + probe=2, + alarm_type=0, + taste=1, + ), + PacketA8Notify(probe=2, alarm_type=None), + ], + id="probes_with_different_data", + ), + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=0, + grill_type=99, + ), + PacketA8Notify( + probe=2, + alarm_type=0, + taste=99, + ), + ], + id="probes_with_unknown_data", + ), + ], +) +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + packets, +) -> None: + """Test the setup.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.SELECT]) + + for packet in packets: + mock_client.mocked_notify(packet) + + await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id) + + +@pytest.mark.parametrize( + ("packets", "entity_id", "value", "write_packet"), + [ + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET, + temperature_1=50.0, + ), + ], + "select.probe_1_grill_type", + "veal", + PacketA303Write(probe=1, grill_type=GrillType.VEAL, taste=None), + id="grill_type", + ), + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET, + grill_type=GrillType.BEEF, + ), + ], + "select.probe_1_taste", + "medium", + PacketA303Write(probe=1, grill_type=GrillType.BEEF, taste=Taste.MEDIUM), + id="taste", + ), + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET, + grill_type=GrillType.BEEF, + taste=Taste.MEDIUM, + ), + ], + "select.probe_1_taste", + "none", + PacketA303Write(probe=1, grill_type=GrillType.BEEF, taste=None), + id="taste_none", + ), + ], +) +async def test_set_option( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_client: Mock, + packets, + entity_id, + value, + write_packet, +) -> None: + """Test the selection of option.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.SELECT]) + + for packet in packets: + mock_client.mocked_notify(packet) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + service_data={ + ATTR_OPTION: value, + }, + target={ + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + + mock_client.write.assert_any_call(write_packet)