From 846e6d96a43a3f71cba63ba51ef11245e9ea088e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 29 Aug 2025 17:42:28 +0200 Subject: [PATCH] Add minimum and maximum targets (#151387) --- homeassistant/components/togrill/number.py | 102 +++- homeassistant/components/togrill/strings.json | 6 + .../togrill/snapshots/test_number.ambr | 472 ++++++++++++++++++ tests/components/togrill/test_number.py | 57 +++ 4 files changed, 612 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/togrill/number.py b/homeassistant/components/togrill/number.py index b649d2ead1b..57d1378fc8a 100644 --- a/homeassistant/components/togrill/number.py +++ b/homeassistant/components/togrill/number.py @@ -2,14 +2,16 @@ from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Callable, Generator, Mapping from dataclasses import dataclass from typing import Any from togrill_bluetooth.packets import ( + AlarmType, PacketA0Notify, PacketA6Write, PacketA8Notify, + PacketA300Write, PacketA301Write, PacketWrite, ) @@ -37,44 +39,94 @@ class ToGrillNumberEntityDescription(NumberEntityDescription): """Description of entity.""" get_value: Callable[[ToGrillCoordinator], float | None] - set_packet: Callable[[float], PacketWrite] + set_packet: Callable[[ToGrillCoordinator, float], PacketWrite] entity_supported: Callable[[Mapping[str, Any]], bool] = lambda _: True probe_number: int | None = None -def _get_temperature_target_description( +def _get_temperature_descriptions( probe_number: int, -) -> ToGrillNumberEntityDescription: - def _set_packet(value: float | None) -> PacketWrite: +) -> Generator[ToGrillNumberEntityDescription]: + def _get_description( + variant: str, + icon: str | None, + set_packet: Callable[[ToGrillCoordinator, float], PacketWrite], + get_value: Callable[[ToGrillCoordinator], float | None], + ) -> ToGrillNumberEntityDescription: + return ToGrillNumberEntityDescription( + key=f"temperature_{variant}_{probe_number}", + translation_key=f"temperature_{variant}", + translation_placeholders={"probe_number": f"{probe_number}"}, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + native_min_value=0, + native_max_value=250, + mode=NumberMode.BOX, + set_packet=set_packet, + get_value=get_value, + entity_supported=lambda x: probe_number <= x[CONF_PROBE_COUNT], + probe_number=probe_number, + ) + + def _get_temperatures( + coordinator: ToGrillCoordinator, alarm_type: AlarmType + ) -> tuple[None | float, None | float]: + if not (packet := coordinator.get_packet(PacketA8Notify, probe_number)): + return None, None + + if packet.alarm_type != alarm_type: + return None, None + + return packet.temperature_1, packet.temperature_2 + + def _set_target( + coordinator: ToGrillCoordinator, value: float | None + ) -> PacketWrite: if value == 0.0: value = None return PacketA301Write(probe=probe_number, target=value) - def _get_value(coordinator: ToGrillCoordinator) -> float | None: - if packet := coordinator.get_packet(PacketA8Notify, probe_number): - if packet.alarm_type == PacketA8Notify.AlarmType.TEMPERATURE_TARGET: - return packet.temperature_1 - return None + def _set_minimum( + coordinator: ToGrillCoordinator, value: float | None + ) -> PacketWrite: + _, maximum = _get_temperatures(coordinator, AlarmType.TEMPERATURE_RANGE) + if value == 0.0: + value = None + return PacketA300Write(probe=probe_number, minimum=value, maximum=maximum) - return ToGrillNumberEntityDescription( - key=f"temperature_target_{probe_number}", - translation_key="temperature_target", - device_class=NumberDeviceClass.TEMPERATURE, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - native_min_value=0, - native_max_value=250, - mode=NumberMode.BOX, - set_packet=_set_packet, - get_value=_get_value, - entity_supported=lambda x: probe_number <= x[CONF_PROBE_COUNT], - probe_number=probe_number, + def _set_maximum( + coordinator: ToGrillCoordinator, value: float | None + ) -> PacketWrite: + minimum, _ = _get_temperatures(coordinator, AlarmType.TEMPERATURE_RANGE) + if value == 0.0: + value = None + return PacketA300Write(probe=probe_number, minimum=minimum, maximum=value) + + yield _get_description( + "target", + None, + _set_target, + lambda x: _get_temperatures(x, AlarmType.TEMPERATURE_TARGET)[0], + ) + yield _get_description( + "minimum", + "mdi:thermometer-chevron-down", + _set_minimum, + lambda x: _get_temperatures(x, AlarmType.TEMPERATURE_RANGE)[0], + ) + yield _get_description( + "maximum", + "mdi:thermometer-chevron-up", + _set_maximum, + lambda x: _get_temperatures(x, AlarmType.TEMPERATURE_RANGE)[1], ) ENTITY_DESCRIPTIONS = ( *[ - _get_temperature_target_description(probe_number) + description for probe_number in range(1, MAX_PROBE_COUNT + 1) + for description in _get_temperature_descriptions(probe_number) ], ToGrillNumberEntityDescription( key="alarm_interval", @@ -85,7 +137,7 @@ ENTITY_DESCRIPTIONS = ( native_max_value=15, native_step=5, mode=NumberMode.BOX, - set_packet=lambda x: ( + set_packet=lambda coordinator, x: ( PacketA6Write(temperature_unit=None, alarm_interval=round(x)) ), get_value=lambda x: ( @@ -135,5 +187,5 @@ class ToGrillNumber(ToGrillEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set value on device.""" - packet = self.entity_description.set_packet(value) + packet = self.entity_description.set_packet(self.coordinator, value) await self._write_packet(packet) diff --git a/homeassistant/components/togrill/strings.json b/homeassistant/components/togrill/strings.json index 79be7e1780c..1a748546b75 100644 --- a/homeassistant/components/togrill/strings.json +++ b/homeassistant/components/togrill/strings.json @@ -43,6 +43,12 @@ "temperature_target": { "name": "Target temperature" }, + "temperature_minimum": { + "name": "Minimum temperature" + }, + "temperature_maximum": { + "name": "Maximum temperature" + }, "alarm_interval": { "name": "Alarm interval" } diff --git a/tests/components/togrill/snapshots/test_number.ambr b/tests/components/togrill/snapshots/test_number.ambr index e38bbd9d133..b91501f8ea6 100644 --- a/tests/components/togrill/snapshots/test_number.ambr +++ b/tests/components/togrill/snapshots/test_number.ambr @@ -58,6 +58,124 @@ 'state': '0', }) # --- +# name: test_setup[no_data][number.probe_1_maximum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.probe_1_maximum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum temperature', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_maximum', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_maximum_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][number.probe_1_maximum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Probe 1 Maximum temperature', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.probe_1_maximum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[no_data][number.probe_1_minimum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.probe_1_minimum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Minimum temperature', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_minimum', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_minimum_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][number.probe_1_minimum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Probe 1 Minimum temperature', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.probe_1_minimum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[no_data][number.probe_1_target_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -117,6 +235,124 @@ 'state': 'unknown', }) # --- +# name: test_setup[no_data][number.probe_2_maximum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.probe_2_maximum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum temperature', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_maximum', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_maximum_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][number.probe_2_maximum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Probe 2 Maximum temperature', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.probe_2_maximum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[no_data][number.probe_2_minimum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.probe_2_minimum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Minimum temperature', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_minimum', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_minimum_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][number.probe_2_minimum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Probe 2 Minimum temperature', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.probe_2_minimum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[no_data][number.probe_2_target_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -235,6 +471,124 @@ 'state': '5', }) # --- +# name: test_setup[one_probe_with_target_alarm][number.probe_1_maximum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.probe_1_maximum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum temperature', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_maximum', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_maximum_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.probe_1_maximum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Probe 1 Maximum temperature', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.probe_1_maximum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.probe_1_minimum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.probe_1_minimum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Minimum temperature', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_minimum', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_minimum_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.probe_1_minimum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Probe 1 Minimum temperature', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.probe_1_minimum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[one_probe_with_target_alarm][number.probe_1_target_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -294,6 +648,124 @@ 'state': '50.0', }) # --- +# name: test_setup[one_probe_with_target_alarm][number.probe_2_maximum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.probe_2_maximum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum temperature', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_maximum', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_maximum_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.probe_2_maximum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Probe 2 Maximum temperature', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.probe_2_maximum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.probe_2_minimum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.probe_2_minimum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Minimum temperature', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_minimum', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_minimum_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.probe_2_minimum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Probe 2 Minimum temperature', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.probe_2_minimum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[one_probe_with_target_alarm][number.probe_2_target_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/togrill/test_number.py b/tests/components/togrill/test_number.py index 6cf7dc4d479..fb88a0d466a 100644 --- a/tests/components/togrill/test_number.py +++ b/tests/components/togrill/test_number.py @@ -10,6 +10,7 @@ from togrill_bluetooth.packets import ( PacketA0Notify, PacketA6Write, PacketA8Notify, + PacketA300Write, PacketA301Write, ) @@ -105,6 +106,62 @@ async def test_setup( PacketA301Write(probe=1, target=None), id="probe_clear", ), + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_RANGE, + temperature_1=50.0, + temperature_2=80.0, + ), + ], + "number.probe_1_minimum_temperature", + 100.0, + PacketA300Write(probe=1, minimum=100.0, maximum=80.0), + id="minimum", + ), + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_RANGE, + temperature_1=None, + temperature_2=80.0, + ), + ], + "number.probe_1_minimum_temperature", + 0.0, + PacketA300Write(probe=1, minimum=None, maximum=80.0), + id="minimum_clear", + ), + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_RANGE, + temperature_1=50.0, + temperature_2=80.0, + ), + ], + "number.probe_1_maximum_temperature", + 100.0, + PacketA300Write(probe=1, minimum=50.0, maximum=100.0), + id="maximum", + ), + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_RANGE, + temperature_1=50.0, + temperature_2=None, + ), + ], + "number.probe_1_maximum_temperature", + 0.0, + PacketA300Write(probe=1, minimum=50.0, maximum=None), + id="maximum_clear", + ), pytest.param( [ PacketA0Notify(