mirror of
https://github.com/home-assistant/core.git
synced 2025-09-09 14:51:34 +02:00
Add ToGrill integration (#150075)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -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
|
||||
|
33
homeassistant/components/togrill/__init__.py
Normal file
33
homeassistant/components/togrill/__init__.py
Normal 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)
|
136
homeassistant/components/togrill/config_flow.py
Normal file
136
homeassistant/components/togrill/config_flow.py
Normal 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)}),
|
||||
)
|
8
homeassistant/components/togrill/const.py
Normal file
8
homeassistant/components/togrill/const.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Constants for the ToGrill integration."""
|
||||
|
||||
DOMAIN = "togrill"
|
||||
|
||||
MAX_PROBE_COUNT = 6
|
||||
|
||||
CONF_PROBE_COUNT = "probe_count"
|
||||
CONF_VERSION = "version"
|
148
homeassistant/components/togrill/coordinator.py
Normal file
148
homeassistant/components/togrill/coordinator.py
Normal 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())
|
18
homeassistant/components/togrill/entity.py
Normal file
18
homeassistant/components/togrill/entity.py
Normal 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
|
18
homeassistant/components/togrill/manifest.json
Normal file
18
homeassistant/components/togrill/manifest.json
Normal 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"]
|
||||
}
|
68
homeassistant/components/togrill/quality_scale.yaml
Normal file
68
homeassistant/components/togrill/quality_scale.yaml
Normal 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
|
127
homeassistant/components/togrill/sensor.py
Normal file
127
homeassistant/components/togrill/sensor.py
Normal 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
|
32
homeassistant/components/togrill/strings.json
Normal file
32
homeassistant/components/togrill/strings.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
homeassistant/generated/bluetooth.py
generated
6
homeassistant/generated/bluetooth.py
generated
@@ -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",
|
||||
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -653,6 +653,7 @@ FLOWS = {
|
||||
"tilt_pi",
|
||||
"time_date",
|
||||
"todoist",
|
||||
"togrill",
|
||||
"tolo",
|
||||
"tomorrowio",
|
||||
"toon",
|
||||
|
@@ -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
3
requirements_all.txt
generated
@@ -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
|
||||
|
||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -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
|
||||
|
||||
|
40
tests/components/togrill/__init__.py
Normal file
40
tests/components/togrill/__init__.py
Normal 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()
|
96
tests/components/togrill/conftest.py
Normal file
96
tests/components/togrill/conftest.py
Normal 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
|
673
tests/components/togrill/snapshots/test_sensor.ambr
Normal file
673
tests/components/togrill/snapshots/test_sensor.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
155
tests/components/togrill/test_config_flow.py
Normal file
155
tests/components/togrill/test_config_flow.py
Normal 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
|
60
tests/components/togrill/test_init.py
Normal file
60
tests/components/togrill/test_init.py
Normal 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
|
59
tests/components/togrill/test_sensor.py
Normal file
59
tests/components/togrill/test_sensor.py
Normal 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)
|
Reference in New Issue
Block a user