mirror of
https://github.com/home-assistant/core.git
synced 2025-09-11 07:41:35 +02:00
Add number entity to togrill (#150609)
This commit is contained in:
@@ -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:
|
||||
|
@@ -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")
|
||||
|
||||
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
|
||||
|
@@ -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()
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
138
homeassistant/components/togrill/number.py
Normal file
138
homeassistant/components/togrill/number.py
Normal 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)
|
@@ -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
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
355
tests/components/togrill/snapshots/test_number.ambr
Normal file
355
tests/components/togrill/snapshots/test_number.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
243
tests/components/togrill/test_number.py
Normal file
243
tests/components/togrill/test_number.py
Normal 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,
|
||||
)
|
Reference in New Issue
Block a user