Add number entity to togrill (#150609)

This commit is contained in:
Joakim Plate
2025-08-17 23:48:53 +02:00
committed by GitHub
parent 794deaa5fd
commit 3ab4fd3035
9 changed files with 825 additions and 8 deletions

View File

@@ -8,7 +8,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import DeviceNotFound, ToGrillConfigEntry, ToGrillCoordinator
_PLATFORMS: list[Platform] = [Platform.SENSOR]
_PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.NUMBER]
async def async_setup_entry(hass: HomeAssistant, entry: ToGrillConfigEntry) -> bool:

View File

@@ -4,11 +4,17 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import TypeVar
from bleak.exc import BleakError
from togrill_bluetooth.client import Client
from togrill_bluetooth.exceptions import DecodeError
from togrill_bluetooth.packets import Packet, PacketA0Notify, PacketA1Notify
from togrill_bluetooth.packets import (
Packet,
PacketA0Notify,
PacketA1Notify,
PacketA8Write,
)
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
@@ -25,11 +31,15 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_PROBE_COUNT
type ToGrillConfigEntry = ConfigEntry[ToGrillCoordinator]
SCAN_INTERVAL = timedelta(seconds=30)
LOGGER = logging.getLogger(__name__)
PacketType = TypeVar("PacketType", bound=Packet)
def get_version_string(packet: PacketA0Notify) -> str:
"""Construct a version string from packet data."""
@@ -44,7 +54,7 @@ class DeviceFailed(UpdateFailed):
"""Update failed due to device disconnected."""
class ToGrillCoordinator(DataUpdateCoordinator[dict[int, Packet]]):
class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Packet]]):
"""Class to manage fetching data."""
config_entry: ToGrillConfigEntry
@@ -86,7 +96,12 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[int, Packet]]):
if not device:
raise DeviceNotFound("Unable to find device")
client = await Client.connect(device, self._notify_callback)
try:
client = await Client.connect(device, self._notify_callback)
except BleakError as exc:
self.logger.debug("Connection failed", exc_info=True)
raise DeviceNotFound("Unable to connect to device") from exc
try:
packet_a0 = await client.read(PacketA0Notify)
except (BleakError, DecodeError) as exc:
@@ -123,16 +138,29 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[int, Packet]]):
self.client = await self._connect_and_update_registry()
return self.client
def get_packet(
self, packet_type: type[PacketType], probe=None
) -> PacketType | None:
"""Get a cached packet of a certain type."""
if packet := self.data.get((packet_type.type, probe)):
assert isinstance(packet, packet_type)
return packet
return None
def _notify_callback(self, packet: Packet):
self.data[packet.type] = packet
probe = getattr(packet, "probe", None)
self.data[(packet.type, probe)] = packet
self.async_update_listeners()
async def _async_update_data(self) -> dict[int, Packet]:
async def _async_update_data(self) -> dict[tuple[int, int | None], Packet]:
"""Poll the device."""
client = await self._get_connected_client()
try:
await client.request(PacketA0Notify)
await client.request(PacketA1Notify)
for probe in range(1, self.config_entry.data[CONF_PROBE_COUNT] + 1):
await client.write(PacketA8Write(probe=probe))
except BleakError as exc:
raise DeviceFailed(f"Device failed {exc}") from exc
return self.data

View File

@@ -2,9 +2,16 @@
from __future__ import annotations
from bleak.exc import BleakError
from togrill_bluetooth.client import Client
from togrill_bluetooth.exceptions import BaseError
from togrill_bluetooth.packets import PacketWrite
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ToGrillCoordinator
from .const import DOMAIN
from .coordinator import LOGGER, ToGrillCoordinator
class ToGrillEntity(CoordinatorEntity[ToGrillCoordinator]):
@@ -16,3 +23,27 @@ class ToGrillEntity(CoordinatorEntity[ToGrillCoordinator]):
"""Initialize coordinator entity."""
super().__init__(coordinator)
self._attr_device_info = coordinator.device_info
def _get_client(self) -> Client:
client = self.coordinator.client
if client is None or not client.is_connected:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="disconnected"
)
return client
async def _write_packet(self, packet: PacketWrite) -> None:
client = self._get_client()
try:
await client.write(packet)
except BleakError as exc:
LOGGER.debug("Failed to write", exc_info=True)
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="communication_failed"
) from exc
except BaseError as exc:
LOGGER.debug("Failed to write", exc_info=True)
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="rejected"
) from exc
await self.coordinator.async_request_refresh()

View File

@@ -13,6 +13,7 @@
"dependencies": ["bluetooth"],
"documentation": "https://www.home-assistant.io/integrations/togrill",
"iot_class": "local_push",
"loggers": ["togrill_bluetooth"],
"quality_scale": "bronze",
"requirements": ["togrill-bluetooth==0.7.0"]
}

View File

@@ -0,0 +1,138 @@
"""Support for number entities."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any
from togrill_bluetooth.packets import (
PacketA0Notify,
PacketA6Write,
PacketA8Notify,
PacketA301Write,
PacketWrite,
)
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import UnitOfTemperature, UnitOfTime
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
@dataclass(kw_only=True, frozen=True)
class ToGrillNumberEntityDescription(NumberEntityDescription):
"""Description of entity."""
get_value: Callable[[ToGrillCoordinator], float | None]
set_packet: Callable[[float], PacketWrite]
entity_supported: Callable[[Mapping[str, Any]], bool] = lambda _: True
def _get_temperature_target_description(
probe_number: int,
) -> ToGrillNumberEntityDescription:
def _set_packet(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
return ToGrillNumberEntityDescription(
key=f"temperature_target_{probe_number}",
translation_key="temperature_target",
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],
)
ENTITY_DESCRIPTIONS = (
*[
_get_temperature_target_description(probe_number)
for probe_number in range(1, MAX_PROBE_COUNT + 1)
],
ToGrillNumberEntityDescription(
key="alarm_interval",
translation_key="alarm_interval",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_min_value=0,
native_max_value=15,
native_step=5,
mode=NumberMode.BOX,
set_packet=lambda x: (
PacketA6Write(temperature_unit=None, alarm_interval=round(x))
),
get_value=lambda x: (
packet.alarm_interval if (packet := x.get_packet(PacketA0Notify)) else None
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ToGrillConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up number based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
ToGrillNumber(coordinator, entity_description)
for entity_description in ENTITY_DESCRIPTIONS
if entity_description.entity_supported(entry.data)
)
class ToGrillNumber(ToGrillEntity, NumberEntity):
"""Representation of a number."""
entity_description: ToGrillNumberEntityDescription
def __init__(
self,
coordinator: ToGrillCoordinator,
entity_description: ToGrillNumberEntityDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = f"{coordinator.address}_{entity_description.key}"
@property
def native_value(self) -> float | None:
"""Return the value reported by the number."""
return self.entity_description.get_value(self.coordinator)
async def async_set_native_value(self, value: float) -> None:
"""Set value on device."""
packet = self.entity_description.set_packet(value)
await self._write_packet(packet)

View File

@@ -122,6 +122,8 @@ class ToGrillSensor(ToGrillEntity, SensorEntity):
@property
def native_value(self) -> StateType:
"""Get current value."""
if packet := self.coordinator.data.get(self.entity_description.packet_type):
if packet := self.coordinator.data.get(
(self.entity_description.packet_type, None)
):
return self.entity_description.packet_extract(packet)
return None

View File

@@ -22,11 +22,30 @@
"failed_to_read_config": "Failed to read config from device"
}
},
"exceptions": {
"disconnected": {
"message": "The device is disconnected"
},
"communication_failed": {
"message": "Communication failed with the device"
},
"rejected": {
"message": "Data was rejected by device"
}
},
"entity": {
"sensor": {
"temperature": {
"name": "Probe {probe_number}"
}
},
"number": {
"temperature_target": {
"name": "Target {probe_number}"
},
"alarm_interval": {
"name": "Alarm interval"
}
}
}
}

View File

@@ -0,0 +1,355 @@
# serializer version: 1
# name: test_setup[no_data][number.pro_05_alarm_interval-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 15,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.pro_05_alarm_interval',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Alarm interval',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'alarm_interval',
'unique_id': '00000000-0000-0000-0000-000000000001_alarm_interval',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_setup[no_data][number.pro_05_alarm_interval-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Pro-05 Alarm interval',
'max': 15,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 5,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'number.pro_05_alarm_interval',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_setup[no_data][number.pro_05_target_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 250,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.pro_05_target_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Target 1',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature_target',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_target_1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[no_data][number.pro_05_target_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Target 1',
'max': 250,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.pro_05_target_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_setup[no_data][number.pro_05_target_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 250,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.pro_05_target_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Target 2',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature_target',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_target_2',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[no_data][number.pro_05_target_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Target 2',
'max': 250,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.pro_05_target_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_setup[one_probe_with_target_alarm][number.pro_05_alarm_interval-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 15,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.pro_05_alarm_interval',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Alarm interval',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'alarm_interval',
'unique_id': '00000000-0000-0000-0000-000000000001_alarm_interval',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_setup[one_probe_with_target_alarm][number.pro_05_alarm_interval-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Pro-05 Alarm interval',
'max': 15,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 5,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'number.pro_05_alarm_interval',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5',
})
# ---
# name: test_setup[one_probe_with_target_alarm][number.pro_05_target_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 250,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.pro_05_target_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Target 1',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature_target',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_target_1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[one_probe_with_target_alarm][number.pro_05_target_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Target 1',
'max': 250,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.pro_05_target_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '50.0',
})
# ---
# name: test_setup[one_probe_with_target_alarm][number.pro_05_target_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 250,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.pro_05_target_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Target 2',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature_target',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_target_2',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[one_probe_with_target_alarm][number.pro_05_target_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Target 2',
'max': 250,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.pro_05_target_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -0,0 +1,243 @@
"""Test numbers for ToGrill integration."""
from unittest.mock import Mock
from bleak.exc import BleakError
import pytest
from syrupy.assertion import SnapshotAssertion
from togrill_bluetooth.exceptions import BaseError
from togrill_bluetooth.packets import (
PacketA0Notify,
PacketA6Write,
PacketA8Notify,
PacketA301Write,
)
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
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=PacketA8Notify.AlarmType.TEMPERATURE_TARGET,
temperature_1=50.0,
),
PacketA8Notify(probe=2, alarm_type=None),
],
id="one_probe_with_target_alarm",
),
],
)
async def test_setup(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_entry: MockConfigEntry,
mock_client: Mock,
packets,
) -> None:
"""Test the numbers."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
await setup_entry(hass, mock_entry, [Platform.NUMBER])
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,
),
],
"number.pro_05_target_1",
100.0,
PacketA301Write(probe=1, target=100),
id="probe",
),
pytest.param(
[
PacketA8Notify(
probe=1,
alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET,
temperature_1=50.0,
),
],
"number.pro_05_target_1",
0.0,
PacketA301Write(probe=1, target=None),
id="probe_clear",
),
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,
)
],
"number.pro_05_alarm_interval",
15,
PacketA6Write(temperature_unit=None, alarm_interval=15),
id="alarm_interval",
),
],
)
async def test_set_number(
hass: HomeAssistant,
mock_entry: MockConfigEntry,
mock_client: Mock,
packets,
entity_id,
value,
write_packet,
) -> None:
"""Test the number set."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
await setup_entry(hass, mock_entry, [Platform.NUMBER])
for packet in packets:
mock_client.mocked_notify(packet)
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
service_data={
ATTR_VALUE: value,
},
target={
ATTR_ENTITY_ID: entity_id,
},
blocking=True,
)
mock_client.write.assert_any_call(write_packet)
@pytest.mark.parametrize(
("error", "message"),
[
pytest.param(
BleakError("Some error"),
"Communication failed with the device",
id="bleak",
),
pytest.param(
BaseError("Some error"),
"Data was rejected by device",
id="base",
),
],
)
async def test_set_number_write_error(
hass: HomeAssistant,
mock_entry: MockConfigEntry,
mock_client: Mock,
error,
message,
) -> None:
"""Test the number set."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
await setup_entry(hass, mock_entry, [Platform.NUMBER])
mock_client.mocked_notify(
PacketA8Notify(
probe=1,
alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET,
temperature_1=50.0,
),
)
mock_client.write.side_effect = error
with pytest.raises(HomeAssistantError, match=message):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
service_data={
ATTR_VALUE: 100,
},
target={
ATTR_ENTITY_ID: "number.pro_05_target_1",
},
blocking=True,
)
async def test_set_number_disconnected(
hass: HomeAssistant,
mock_entry: MockConfigEntry,
mock_client: Mock,
) -> None:
"""Test the number set."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
await setup_entry(hass, mock_entry, [Platform.NUMBER])
mock_client.mocked_notify(
PacketA8Notify(
probe=1,
alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET,
temperature_1=50.0,
),
)
mock_client.is_connected = False
with pytest.raises(HomeAssistantError, match=""):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
service_data={
ATTR_VALUE: 100,
},
target={
ATTR_ENTITY_ID: "number.pro_05_target_1",
},
blocking=True,
)