Add ToGrill integration (#150075)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Joakim Plate
2025-08-09 00:24:54 +02:00
committed by GitHub
parent e9d39a826e
commit c876bed33f
21 changed files with 1692 additions and 0 deletions

2
CODEOWNERS generated
View File

@@ -1597,6 +1597,8 @@ build.json @home-assistant/supervisor
/tests/components/todo/ @home-assistant/core
/homeassistant/components/todoist/ @boralyl
/tests/components/todoist/ @boralyl
/homeassistant/components/togrill/ @elupus
/tests/components/togrill/ @elupus
/homeassistant/components/tolo/ @MatthiasLohr
/tests/components/tolo/ @MatthiasLohr
/homeassistant/components/tomorrowio/ @raman325 @lymanepp

View File

@@ -0,0 +1,33 @@
"""The ToGrill integration."""
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import DeviceNotFound, ToGrillConfigEntry, ToGrillCoordinator
_PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ToGrillConfigEntry) -> bool:
"""Set up ToGrill Bluetooth from a config entry."""
coordinator = ToGrillCoordinator(hass, entry)
try:
await coordinator.async_config_entry_first_refresh()
except ConfigEntryNotReady as exc:
if not isinstance(exc.__cause__, DeviceNotFound):
raise
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ToGrillConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

View File

@@ -0,0 +1,136 @@
"""Config flow for the ToGrill integration."""
from __future__ import annotations
from typing import Any
from bleak.exc import BleakError
from togrill_bluetooth import SUPPORTED_DEVICES
from togrill_bluetooth.client import Client
from togrill_bluetooth.packets import PacketA0Notify
import voluptuous as vol
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS, CONF_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow
from .const import CONF_PROBE_COUNT, DOMAIN
from .coordinator import LOGGER
_TIMEOUT = 10
async def read_config_data(
hass: HomeAssistant, info: BluetoothServiceInfoBleak
) -> dict[str, Any]:
"""Read config from device."""
try:
client = await Client.connect(info.device)
except BleakError as exc:
LOGGER.debug("Failed to connect", exc_info=True)
raise AbortFlow("failed_to_read_config") from exc
try:
packet_a0 = await client.read(PacketA0Notify)
except BleakError as exc:
LOGGER.debug("Failed to read data", exc_info=True)
raise AbortFlow("failed_to_read_config") from exc
finally:
await client.disconnect()
return {
CONF_MODEL: info.name,
CONF_ADDRESS: info.address,
CONF_PROBE_COUNT: packet_a0.probe_count,
}
class ToGrillBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for ToGrillBluetooth."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovery_infos: dict[str, BluetoothServiceInfoBleak] = {}
async def _async_create_entry_internal(
self, info: BluetoothServiceInfoBleak
) -> ConfigFlowResult:
config_data = await read_config_data(self.hass, info)
return self.async_create_entry(
title=config_data[CONF_MODEL],
data=config_data,
)
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> ConfigFlowResult:
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
if discovery_info.name not in SUPPORTED_DEVICES:
return self.async_abort(reason="not_supported")
self._discovery_info = discovery_info
return await self.async_step_bluetooth_confirm()
async def async_step_bluetooth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
assert self._discovery_info is not None
discovery_info = self._discovery_info
if user_input is not None:
return await self._async_create_entry_internal(discovery_info)
self._set_confirm_only()
placeholders = {"name": discovery_info.name}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="bluetooth_confirm", description_placeholders=placeholders
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step to pick discovered device."""
if user_input is not None:
address = user_input[CONF_ADDRESS]
await self.async_set_unique_id(address, raise_on_progress=False)
self._abort_if_unique_id_configured()
return await self._async_create_entry_internal(
self._discovery_infos[address]
)
current_addresses = self._async_current_ids()
for discovery_info in async_discovered_service_info(self.hass, True):
address = discovery_info.address
if (
address in current_addresses
or address in self._discovery_infos
or discovery_info.name not in SUPPORTED_DEVICES
):
continue
self._discovery_infos[address] = discovery_info
if not self._discovery_infos:
return self.async_abort(reason="no_devices_found")
addresses = {info.address: info.name for info in self._discovery_infos.values()}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(addresses)}),
)

View File

@@ -0,0 +1,8 @@
"""Constants for the ToGrill integration."""
DOMAIN = "togrill"
MAX_PROBE_COUNT = 6
CONF_PROBE_COUNT = "probe_count"
CONF_VERSION = "version"

View File

@@ -0,0 +1,148 @@
"""Coordinator for the ToGrill Bluetooth integration."""
from __future__ import annotations
from datetime import timedelta
import logging
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 homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothCallbackMatcher,
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfoBleak,
async_register_callback,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_MODEL
from homeassistant.core import HomeAssistant, callback
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
type ToGrillConfigEntry = ConfigEntry[ToGrillCoordinator]
SCAN_INTERVAL = timedelta(seconds=30)
LOGGER = logging.getLogger(__name__)
def get_version_string(packet: PacketA0Notify) -> str:
"""Construct a version string from packet data."""
return f"{packet.version_major}.{packet.version_minor}"
class DeviceNotFound(UpdateFailed):
"""Update failed due to device disconnected."""
class DeviceFailed(UpdateFailed):
"""Update failed due to device disconnected."""
class ToGrillCoordinator(DataUpdateCoordinator[dict[int, Packet]]):
"""Class to manage fetching data."""
config_entry: ToGrillConfigEntry
client: Client | None = None
def __init__(
self,
hass: HomeAssistant,
config_entry: ToGrillConfigEntry,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass=hass,
logger=LOGGER,
config_entry=config_entry,
name="ToGrill",
update_interval=SCAN_INTERVAL,
)
self.address = config_entry.data[CONF_ADDRESS]
self.data = {}
self.device_info = DeviceInfo(
connections={(CONNECTION_BLUETOOTH, self.address)}
)
config_entry.async_on_unload(
async_register_callback(
hass,
self._async_handle_bluetooth_event,
BluetoothCallbackMatcher(address=self.address, connectable=True),
BluetoothScanningMode.ACTIVE,
)
)
async def _connect_and_update_registry(self) -> Client:
"""Update device registry data."""
device = bluetooth.async_ble_device_from_address(
self.hass, self.address, connectable=True
)
if not device:
raise DeviceNotFound("Unable to find device")
client = await Client.connect(device, self._notify_callback)
try:
packet_a0 = await client.read(PacketA0Notify)
except (BleakError, DecodeError) as exc:
await client.disconnect()
raise DeviceFailed(f"Device failed {exc}") from exc
config_entry = self.config_entry
device_registry = dr.async_get(self.hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(CONNECTION_BLUETOOTH, self.address)},
name=config_entry.data[CONF_MODEL],
model=config_entry.data[CONF_MODEL],
sw_version=get_version_string(packet_a0),
)
return client
async def async_shutdown(self) -> None:
"""Shutdown coordinator and disconnect from device."""
await super().async_shutdown()
if self.client:
await self.client.disconnect()
self.client = None
async def _get_connected_client(self) -> Client:
if self.client and not self.client.is_connected:
await self.client.disconnect()
self.client = None
if self.client:
return self.client
self.client = await self._connect_and_update_registry()
return self.client
def _notify_callback(self, packet: Packet):
self.data[packet.type] = packet
self.async_update_listeners()
async def _async_update_data(self) -> dict[int, Packet]:
"""Poll the device."""
client = await self._get_connected_client()
try:
await client.request(PacketA0Notify)
await client.request(PacketA1Notify)
except BleakError as exc:
raise DeviceFailed(f"Device failed {exc}") from exc
return self.data
@callback
def _async_handle_bluetooth_event(
self,
service_info: BluetoothServiceInfoBleak,
change: BluetoothChange,
) -> None:
"""Handle a Bluetooth event."""
if not self.client and isinstance(self.last_exception, DeviceNotFound):
self.hass.async_create_task(self.async_refresh())

View File

@@ -0,0 +1,18 @@
"""Provides the base entities."""
from __future__ import annotations
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ToGrillCoordinator
class ToGrillEntity(CoordinatorEntity[ToGrillCoordinator]):
"""Coordinator entity for Gardena Bluetooth."""
_attr_has_entity_name = True
def __init__(self, coordinator: ToGrillCoordinator) -> None:
"""Initialize coordinator entity."""
super().__init__(coordinator)
self._attr_device_info = coordinator.device_info

View File

@@ -0,0 +1,18 @@
{
"domain": "togrill",
"name": "ToGrill",
"bluetooth": [
{
"manufacturer_id": 34714,
"service_uuid": "0000cee0-0000-1000-8000-00805f9b34fb",
"connectable": true
}
],
"codeowners": ["@elupus"],
"config_flow": true,
"dependencies": ["bluetooth"],
"documentation": "https://www.home-assistant.io/integrations/togrill",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["togrill-bluetooth==0.4.0"]
}

View File

@@ -0,0 +1,68 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: This integration does not require authentication.
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: This integration only has a single device.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: exempt
comment: This integration only has a single device.
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: This integration does not need any websession
strict-typing: todo

View File

@@ -0,0 +1,127 @@
"""Support for sensor entities."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any, cast
from togrill_bluetooth.packets import Packet, PacketA0Notify, PacketA1Notify
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
StateType,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
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 ToGrillSensorEntityDescription(SensorEntityDescription):
"""Description of entity."""
packet_type: int
packet_extract: Callable[[Packet], StateType]
entity_supported: Callable[[Mapping[str, Any]], bool] = lambda _: True
def _get_temperature_description(probe_number: int):
def _get(packet: Packet) -> StateType:
assert isinstance(packet, PacketA1Notify)
if len(packet.temperatures) < probe_number:
return None
temperature = packet.temperatures[probe_number - 1]
if temperature is None:
return None
return temperature
def _supported(config: Mapping[str, Any]):
return probe_number <= config[CONF_PROBE_COUNT]
return ToGrillSensorEntityDescription(
key=f"temperature_{probe_number}",
translation_key="temperature",
translation_placeholders={"probe_number": f"{probe_number}"},
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
packet_type=PacketA1Notify.type,
packet_extract=_get,
entity_supported=_supported,
)
ENTITY_DESCRIPTIONS = (
ToGrillSensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
packet_type=PacketA0Notify.type,
packet_extract=lambda packet: cast(PacketA0Notify, packet).battery,
),
*[
_get_temperature_description(probe_number)
for probe_number in range(1, MAX_PROBE_COUNT + 1)
],
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ToGrillConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensor based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
ToGrillSensor(coordinator, entity_description)
for entity_description in ENTITY_DESCRIPTIONS
if entity_description.entity_supported(entry.data)
)
class ToGrillSensor(ToGrillEntity, SensorEntity):
"""Representation of a sensor."""
entity_description: ToGrillSensorEntityDescription
def __init__(
self,
coordinator: ToGrillCoordinator,
entity_description: ToGrillSensorEntityDescription,
) -> None:
"""Initialize sensor."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_device_info = coordinator.device_info
self._attr_unique_id = f"{coordinator.address}_{entity_description.key}"
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.native_value is not None
@property
def native_value(self) -> StateType:
"""Get current value."""
if packet := self.coordinator.data.get(self.entity_description.packet_type):
return self.entity_description.packet_extract(packet)
return None

View File

@@ -0,0 +1,32 @@
{
"config": {
"flow_title": "{name}",
"step": {
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:common::config_flow::data::device%]"
},
"data_description": {
"address": "Select the device to add."
}
},
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
}
},
"abort": {
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"failed_to_read_config": "Failed to read config from device"
}
},
"entity": {
"sensor": {
"temperature": {
"name": "Probe {probe_number}"
}
}
}
}

View File

@@ -834,6 +834,12 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
],
"manufacturer_id": 76,
},
{
"connectable": True,
"domain": "togrill",
"manufacturer_id": 34714,
"service_uuid": "0000cee0-0000-1000-8000-00805f9b34fb",
},
{
"connectable": False,
"domain": "xiaomi_ble",

View File

@@ -653,6 +653,7 @@ FLOWS = {
"tilt_pi",
"time_date",
"todoist",
"togrill",
"tolo",
"tomorrowio",
"toon",

View File

@@ -6790,6 +6790,12 @@
"config_flow": true,
"iot_class": "cloud_polling"
},
"togrill": {
"name": "ToGrill",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
"tolo": {
"name": "TOLO Sauna",
"integration_type": "hub",

3
requirements_all.txt generated
View File

@@ -2955,6 +2955,9 @@ tmb==0.0.4
# homeassistant.components.todoist
todoist-api-python==2.1.7
# homeassistant.components.togrill
togrill-bluetooth==0.4.0
# homeassistant.components.tolo
tololib==1.2.2

View File

@@ -2432,6 +2432,9 @@ tilt-pi==0.2.1
# homeassistant.components.todoist
todoist-api-python==2.1.7
# homeassistant.components.togrill
togrill-bluetooth==0.4.0
# homeassistant.components.tolo
tololib==1.2.2

View File

@@ -0,0 +1,40 @@
"""Tests for the ToGrill Bluetooth integration."""
from unittest.mock import patch
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from tests.common import MockConfigEntry
TOGRILL_SERVICE_INFO = BluetoothServiceInfo(
name="Pro-05",
address="00000000-0000-0000-0000-000000000001",
rssi=-63,
service_data={},
manufacturer_data={34714: b"\xd9\xe3\xbe\xf3\x00"},
service_uuids=["0000cee0-0000-1000-8000-00805f9b34fb"],
source="local",
)
TOGRILL_SERVICE_INFO_NO_NAME = BluetoothServiceInfo(
name="",
address="00000000-0000-0000-0000-000000000002",
rssi=-63,
service_data={},
manufacturer_data={34714: b"\xd9\xe3\xbe\xf3\x00"},
service_uuids=["0000cee0-0000-1000-8000-00805f9b34fb"],
source="local",
)
async def setup_entry(
hass: HomeAssistant, mock_entry: MockConfigEntry, platforms: list[Platform]
) -> None:
"""Make sure the device is available."""
with patch("homeassistant.components.togrill._PLATFORMS", platforms):
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -0,0 +1,96 @@
"""Common fixtures for the ToGrill tests."""
from collections.abc import Callable, Generator
from unittest.mock import AsyncMock, Mock, patch
import pytest
from togrill_bluetooth.client import Client
from togrill_bluetooth.packets import Packet, PacketA0Notify, PacketNotify
from homeassistant.components.togrill.const import CONF_PROBE_COUNT, DOMAIN
from homeassistant.const import CONF_ADDRESS, CONF_MODEL
from . import TOGRILL_SERVICE_INFO
from tests.common import MockConfigEntry
@pytest.fixture
def mock_entry() -> MockConfigEntry:
"""Create hass config fixture."""
return MockConfigEntry(
domain=DOMAIN,
data={
CONF_ADDRESS: TOGRILL_SERVICE_INFO.address,
CONF_MODEL: "Pro-05",
CONF_PROBE_COUNT: 2,
},
unique_id=TOGRILL_SERVICE_INFO.address,
)
@pytest.fixture(scope="module")
def mock_unload_entry() -> Generator[AsyncMock]:
"""Override async_unload_entry."""
with patch(
"homeassistant.components.togrill.async_unload_entry",
return_value=True,
) as mock_unload_entry:
yield mock_unload_entry
@pytest.fixture(scope="module")
def mock_setup_entry(mock_unload_entry) -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.togrill.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(autouse=True)
def mock_client(enable_bluetooth: None, mock_client_class: Mock) -> Generator[Mock]:
"""Auto mock bluetooth."""
client_object = Mock(spec=Client)
client_object.mocked_notify = None
async def _connect(
address: str, callback: Callable[[Packet], None] | None = None
) -> Mock:
client_object.mocked_notify = callback
return client_object
async def _disconnect() -> None:
pass
async def _request(packet_type: type[Packet]) -> None:
if packet_type is PacketA0Notify:
client_object.mocked_notify(PacketA0Notify(0, 0, 0, 0, 0, False, 0, False))
async def _read(packet_type: type[PacketNotify]) -> PacketNotify:
if packet_type is PacketA0Notify:
return PacketA0Notify(0, 0, 0, 0, 0, False, 0, False)
raise NotImplementedError
mock_client_class.connect.side_effect = _connect
client_object.request.side_effect = _request
client_object.read.side_effect = _read
client_object.disconnect.side_effect = _disconnect
client_object.is_connected = True
return client_object
@pytest.fixture(autouse=True)
def mock_client_class() -> Generator[Mock]:
"""Auto mock bluetooth."""
with (
patch(
"homeassistant.components.togrill.config_flow.Client", autospec=True
) as client_class,
patch("homeassistant.components.togrill.coordinator.Client", new=client_class),
):
yield client_class

View File

@@ -0,0 +1,673 @@
# serializer version: 1
# name: test_setup[battery][sensor.pro_05_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.pro_05_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000-0000-0000-0000-000000000001_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_setup[battery][sensor.pro_05_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Pro-05 Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '45',
})
# ---
# name: test_setup[battery][sensor.pro_05_probe_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pro_05_probe_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe 1',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[battery][sensor.pro_05_probe_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Probe 1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_probe_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_setup[battery][sensor.pro_05_probe_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pro_05_probe_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe 2',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_2',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[battery][sensor.pro_05_probe_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Probe 2',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_probe_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_setup[no_data][sensor.pro_05_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.pro_05_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000-0000-0000-0000-000000000001_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_setup[no_data][sensor.pro_05_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Pro-05 Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_setup[no_data][sensor.pro_05_probe_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pro_05_probe_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe 1',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[no_data][sensor.pro_05_probe_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Probe 1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_probe_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_setup[no_data][sensor.pro_05_probe_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pro_05_probe_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe 2',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_2',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[no_data][sensor.pro_05_probe_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Probe 2',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_probe_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_setup[temp_data][sensor.pro_05_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.pro_05_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000-0000-0000-0000-000000000001_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_setup[temp_data][sensor.pro_05_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Pro-05 Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_setup[temp_data][sensor.pro_05_probe_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pro_05_probe_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe 1',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[temp_data][sensor.pro_05_probe_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Probe 1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_probe_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
# name: test_setup[temp_data][sensor.pro_05_probe_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pro_05_probe_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe 2',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_2',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[temp_data][sensor.pro_05_probe_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Probe 2',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_probe_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_setup[temp_data_missing_probe][sensor.pro_05_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.pro_05_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000-0000-0000-0000-000000000001_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_setup[temp_data_missing_probe][sensor.pro_05_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Pro-05 Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_setup[temp_data_missing_probe][sensor.pro_05_probe_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pro_05_probe_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe 1',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[temp_data_missing_probe][sensor.pro_05_probe_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Probe 1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_probe_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
# name: test_setup[temp_data_missing_probe][sensor.pro_05_probe_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pro_05_probe_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe 2',
'platform': 'togrill',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': '00000000-0000-0000-0000-000000000001_temperature_2',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_setup[temp_data_missing_probe][sensor.pro_05_probe_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Pro-05 Probe 2',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pro_05_probe_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---

View File

@@ -0,0 +1,155 @@
"""Test the ToGrill config flow."""
from unittest.mock import Mock
from bleak.exc import BleakError
import pytest
from homeassistant import config_entries
from homeassistant.components.togrill.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import TOGRILL_SERVICE_INFO, TOGRILL_SERVICE_INFO_NO_NAME, setup_entry
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
async def test_user_selection(
hass: HomeAssistant,
) -> None:
"""Test we can select a device."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO_NO_NAME)
await hass.async_block_till_done(wait_background_tasks=True)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"address": TOGRILL_SERVICE_INFO.address},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
"address": TOGRILL_SERVICE_INFO.address,
"model": "Pro-05",
"probe_count": 0,
}
assert result["title"] == "Pro-05"
assert result["result"].unique_id == TOGRILL_SERVICE_INFO.address
async def test_failed_connect(
hass: HomeAssistant,
mock_client: Mock,
mock_client_class: Mock,
) -> None:
"""Test failure to connect result."""
mock_client_class.connect.side_effect = BleakError("Failed to connect")
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"address": TOGRILL_SERVICE_INFO.address},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "failed_to_read_config"
async def test_failed_read(
hass: HomeAssistant,
mock_client: Mock,
) -> None:
"""Test failure to read from device."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_client.read.side_effect = BleakError("something went wrong")
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"address": TOGRILL_SERVICE_INFO.address},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "failed_to_read_config"
async def test_no_devices(
hass: HomeAssistant,
) -> None:
"""Test missing device."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO_NO_NAME)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_duplicate_setup(
hass: HomeAssistant,
mock_entry: MockConfigEntry,
) -> None:
"""Test we can not setup a device again."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
await hass.async_block_till_done(wait_background_tasks=True)
await setup_entry(hass, mock_entry, [])
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_bluetooth(
hass: HomeAssistant,
) -> None:
"""Test bluetooth device discovery."""
# Inject the service info will trigger the flow to start
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
await hass.async_block_till_done(wait_background_tasks=True)
result = next(iter(hass.config_entries.flow.async_progress_by_handler(DOMAIN)))
assert result["step_id"] == "bluetooth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
"address": TOGRILL_SERVICE_INFO.address,
"model": "Pro-05",
"probe_count": 0,
}
assert result["title"] == "Pro-05"
assert result["result"].unique_id == TOGRILL_SERVICE_INFO.address

View File

@@ -0,0 +1,60 @@
"""Test for initialization of ToGrill integration."""
from unittest.mock import Mock
from bleak.exc import BleakError
from syrupy.assertion import SnapshotAssertion
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import TOGRILL_SERVICE_INFO, setup_entry
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info
async def test_setup_device_present(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_entry: MockConfigEntry,
mock_client: Mock,
mock_client_class: Mock,
) -> None:
"""Test that setup works with device present."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
await setup_entry(hass, mock_entry, [])
assert mock_entry.state is ConfigEntryState.LOADED
async def test_setup_device_not_present(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_entry: MockConfigEntry,
mock_client: Mock,
mock_client_class: Mock,
) -> None:
"""Test that setup succeeds if device is missing."""
await setup_entry(hass, mock_entry, [])
assert mock_entry.state is ConfigEntryState.LOADED
async def test_setup_device_failing(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_entry: MockConfigEntry,
mock_client: Mock,
mock_client_class: Mock,
) -> None:
"""Test that setup fails if device is not responding."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
mock_client.is_connected = False
mock_client.read.side_effect = BleakError("Failed to read data")
await setup_entry(hass, mock_entry, [])
assert mock_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -0,0 +1,59 @@
"""Test sensors for ToGrill integration."""
from unittest.mock import Mock
import pytest
from syrupy.assertion import SnapshotAssertion
from togrill_bluetooth.packets import PacketA0Notify, PacketA1Notify
from homeassistant.const import 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,
)
],
id="battery",
),
pytest.param([PacketA1Notify([10, None])], id="temp_data"),
pytest.param([PacketA1Notify([10])], id="temp_data_missing_probe"),
],
)
async def test_setup(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_entry: MockConfigEntry,
mock_client: Mock,
packets,
) -> None:
"""Test the sensors."""
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
await setup_entry(hass, mock_entry, [Platform.SENSOR])
for packet in packets:
mock_client.mocked_notify(packet)
await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id)