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
|
/tests/components/todo/ @home-assistant/core
|
||||||
/homeassistant/components/todoist/ @boralyl
|
/homeassistant/components/todoist/ @boralyl
|
||||||
/tests/components/todoist/ @boralyl
|
/tests/components/todoist/ @boralyl
|
||||||
|
/homeassistant/components/togrill/ @elupus
|
||||||
|
/tests/components/togrill/ @elupus
|
||||||
/homeassistant/components/tolo/ @MatthiasLohr
|
/homeassistant/components/tolo/ @MatthiasLohr
|
||||||
/tests/components/tolo/ @MatthiasLohr
|
/tests/components/tolo/ @MatthiasLohr
|
||||||
/homeassistant/components/tomorrowio/ @raman325 @lymanepp
|
/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,
|
"manufacturer_id": 76,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"connectable": True,
|
||||||
|
"domain": "togrill",
|
||||||
|
"manufacturer_id": 34714,
|
||||||
|
"service_uuid": "0000cee0-0000-1000-8000-00805f9b34fb",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"connectable": False,
|
"connectable": False,
|
||||||
"domain": "xiaomi_ble",
|
"domain": "xiaomi_ble",
|
||||||
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -653,6 +653,7 @@ FLOWS = {
|
|||||||
"tilt_pi",
|
"tilt_pi",
|
||||||
"time_date",
|
"time_date",
|
||||||
"todoist",
|
"todoist",
|
||||||
|
"togrill",
|
||||||
"tolo",
|
"tolo",
|
||||||
"tomorrowio",
|
"tomorrowio",
|
||||||
"toon",
|
"toon",
|
||||||
|
@@ -6790,6 +6790,12 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
|
"togrill": {
|
||||||
|
"name": "ToGrill",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "local_push"
|
||||||
|
},
|
||||||
"tolo": {
|
"tolo": {
|
||||||
"name": "TOLO Sauna",
|
"name": "TOLO Sauna",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -2955,6 +2955,9 @@ tmb==0.0.4
|
|||||||
# homeassistant.components.todoist
|
# homeassistant.components.todoist
|
||||||
todoist-api-python==2.1.7
|
todoist-api-python==2.1.7
|
||||||
|
|
||||||
|
# homeassistant.components.togrill
|
||||||
|
togrill-bluetooth==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.tolo
|
# homeassistant.components.tolo
|
||||||
tololib==1.2.2
|
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
|
# homeassistant.components.todoist
|
||||||
todoist-api-python==2.1.7
|
todoist-api-python==2.1.7
|
||||||
|
|
||||||
|
# homeassistant.components.togrill
|
||||||
|
togrill-bluetooth==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.tolo
|
# homeassistant.components.tolo
|
||||||
tololib==1.2.2
|
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