Add Chef iQ integration (#174171)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Horowitz
2026-06-19 12:41:57 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 35a2dcb222
commit 876ffe3b6c
20 changed files with 1468 additions and 0 deletions
+1
View File
@@ -142,6 +142,7 @@ homeassistant.components.canary.*
homeassistant.components.casper_glow.*
homeassistant.components.centriconnect.*
homeassistant.components.cert_expiry.*
homeassistant.components.chef_iq.*
homeassistant.components.clickatell.*
homeassistant.components.clicksend.*
homeassistant.components.climate.*
Generated
+2
View File
@@ -299,6 +299,8 @@ CLAUDE.md @home-assistant/core
/tests/components/cert_expiry/ @jjlawren
/homeassistant/components/chacon_dio/ @cnico
/tests/components/chacon_dio/ @cnico
/homeassistant/components/chef_iq/ @Invader444
/tests/components/chef_iq/ @Invader444
/homeassistant/components/chess_com/ @joostlek
/tests/components/chess_com/ @joostlek
/homeassistant/components/cielo_home/ @ihsan-cielo @mudasar-cielo
@@ -0,0 +1,46 @@
"""The Chef iQ integration."""
import logging
from chefiq_ble import ChefIqBluetoothDeviceData
from homeassistant.components.bluetooth import BluetoothScanningMode
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
type ChefIqConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ChefIqConfigEntry) -> bool:
"""Set up Chef iQ BLE device from a config entry."""
address = entry.unique_id
assert address is not None
data = ChefIqBluetoothDeviceData()
coordinator = entry.runtime_data = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.PASSIVE,
update_method=data.update,
# The probe is broadcast-only; all data comes from advertisements and it
# is never connected to.
connectable=False,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# only start after all platforms have had a chance to subscribe
entry.async_on_unload(coordinator.async_start())
return True
async def async_unload_entry(hass: HomeAssistant, entry: ChefIqConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,92 @@
"""Config flow for the Chef iQ integration."""
from typing import Any
from chefiq_ble import ChefIqBluetoothDeviceData as DeviceData
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
from .const import DOMAIN
class ChefIqConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Chef iQ."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_device: DeviceData | None = None
self._discovered_devices: dict[str, str] = {}
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()
device = DeviceData()
if not device.supported(discovery_info):
return self.async_abort(reason="not_supported")
self._discovery_info = discovery_info
self._discovered_device = device
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._discovered_device is not None
device = self._discovered_device
assert self._discovery_info is not None
discovery_info = self._discovery_info
title = device.title or device.get_device_name() or discovery_info.name
if user_input is not None:
return self.async_create_entry(title=title, data={})
self._set_confirm_only()
placeholders = {"name": title}
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 self.async_create_entry(
title=self._discovered_devices[address], data={}
)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
if address in current_addresses or address in self._discovered_devices:
continue
device = DeviceData()
if device.supported(discovery_info):
self._discovered_devices[address] = (
device.title or device.get_device_name() or discovery_info.name
)
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)}
),
)
@@ -0,0 +1,3 @@
"""Constants for the Chef iQ integration."""
DOMAIN = "chef_iq"
@@ -0,0 +1,14 @@
"""Support for Chef iQ devices."""
from chefiq_ble import DeviceKey
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothEntityKey,
)
def device_key_to_bluetooth_entity_key(
device_key: DeviceKey,
) -> PassiveBluetoothEntityKey:
"""Convert a device key to an entity key."""
return PassiveBluetoothEntityKey(device_key.key, device_key.device_id)
@@ -0,0 +1,19 @@
{
"domain": "chef_iq",
"name": "Chef iQ",
"bluetooth": [
{
"connectable": false,
"manufacturer_id": 1485
}
],
"codeowners": ["@Invader444"],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/chef_iq",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["chefiq_ble"],
"quality_scale": "bronze",
"requirements": ["chefiq-ble==1.0.1"]
}
@@ -0,0 +1,114 @@
rules:
# Bronze
action-setup:
status: exempt
comment: No custom actions are defined.
appropriate-polling:
status: exempt
comment: |
Passive integration; data is pushed via Bluetooth advertisements.
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: done
comment: |
Subscriptions are handled by the PassiveBluetoothProcessorEntity base class.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure:
status: exempt
comment: |
Passive device; presence is confirmed by the discovered advertisement, no
connection is made.
test-before-setup:
status: exempt
comment: |
Sleepy passive device that may not be advertising at setup time; no
connection is made.
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: The integration has no configuration parameters.
docs-installation-parameters: done
entity-unavailable:
status: done
comment: |
Availability is provided by the passive bluetooth coordinator. These are
sleepy devices, so once seen the entities use assumed_state rather than
becoming unavailable.
integration-owner: done
log-when-unavailable:
status: done
comment: Handled by the passive bluetooth coordinator.
parallel-updates: done
reauthentication-flow:
status: exempt
comment: No authentication is required.
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
The device is identified by its static Bluetooth address; there is no
network information to update.
discovery:
status: done
comment: The integration is discovered via Bluetooth.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
comment: Each probe is set up as its own config entry.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations:
status: exempt
comment: No custom exceptions are defined.
icon-translations:
status: exempt
comment: Entities derive their icons from their device classes.
reconfiguration-flow:
status: exempt
comment: |
There is nothing to reconfigure; the device is identified by its fixed
Bluetooth address.
repair-issues:
status: exempt
comment: No repair issues are raised.
stale-devices:
status: exempt
comment: A single device per config entry; removing the entry removes it.
# Platinum
async-dependency:
status: done
comment: |
The chefiq-ble dependency is a pure advertisement parser and performs no
blocking I/O.
inject-websession:
status: exempt
comment: The integration does not make HTTP requests.
strict-typing: done
+165
View File
@@ -0,0 +1,165 @@
"""Support for Chef iQ sensors."""
from chefiq_ble import ChefIqSensor, SensorUpdate
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
PassiveBluetoothProcessorEntity,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
from . import ChefIqConfigEntry
from .device import device_key_to_bluetooth_entity_key
PARALLEL_UPDATES = 0
def _temperature_description(
sensor: ChefIqSensor, *, enabled_default: bool = True
) -> SensorEntityDescription:
"""Build a standard Celsius temperature description for a Chef iQ sensor."""
return SensorEntityDescription(
key=sensor,
translation_key=sensor,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=enabled_default,
)
SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
ChefIqSensor.FOOD_TEMPERATURE: _temperature_description(
ChefIqSensor.FOOD_TEMPERATURE
),
ChefIqSensor.AMBIENT_TEMPERATURE: _temperature_description(
ChefIqSensor.AMBIENT_TEMPERATURE
),
ChefIqSensor.PROBE_TIP_1_TEMPERATURE: _temperature_description(
ChefIqSensor.PROBE_TIP_1_TEMPERATURE, enabled_default=False
),
ChefIqSensor.PROBE_TIP_2_TEMPERATURE: _temperature_description(
ChefIqSensor.PROBE_TIP_2_TEMPERATURE, enabled_default=False
),
ChefIqSensor.PROBE_TIP_3_TEMPERATURE: _temperature_description(
ChefIqSensor.PROBE_TIP_3_TEMPERATURE, enabled_default=False
),
ChefIqSensor.PROBE_TIP_4_TEMPERATURE: _temperature_description(
ChefIqSensor.PROBE_TIP_4_TEMPERATURE, enabled_default=False
),
ChefIqSensor.SOC_TEMPERATURE: SensorEntityDescription(
key=ChefIqSensor.SOC_TEMPERATURE,
translation_key=ChefIqSensor.SOC_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
ChefIqSensor.BATTERY_PERCENT: SensorEntityDescription(
key=ChefIqSensor.BATTERY_PERCENT,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
# signal_strength (RSSI) is emitted automatically by the BluetoothData base
# class, so it must have a description here.
ChefIqSensor.SIGNAL_STRENGTH: SensorEntityDescription(
key=ChefIqSensor.SIGNAL_STRENGTH,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
}
def sensor_update_to_bluetooth_data_update(
sensor_update: SensorUpdate,
) -> PassiveBluetoothDataUpdate:
"""Convert a sensor update to a bluetooth data update."""
return PassiveBluetoothDataUpdate(
devices={
device_id: sensor_device_info_to_hass_device_info(device_info)
for device_id, device_info in sensor_update.devices.items()
},
entity_descriptions={
device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[
device_key.key
]
for device_key in sensor_update.entity_descriptions
if device_key.key in SENSOR_DESCRIPTIONS
},
entity_data={
device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value
for device_key, sensor_values in sensor_update.entity_values.items()
if device_key.key in SENSOR_DESCRIPTIONS
},
entity_names={},
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ChefIqConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Chef iQ BLE sensors."""
coordinator = entry.runtime_data
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
entry.async_on_unload(
processor.async_add_entities_listener(
ChefIqBluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(
coordinator.async_register_processor(processor, SensorEntityDescription)
)
class ChefIqBluetoothSensorEntity(
PassiveBluetoothProcessorEntity[
PassiveBluetoothDataProcessor[float | int | None, SensorUpdate]
],
SensorEntity,
):
"""Representation of a Chef iQ sensor."""
@property
def native_value(self) -> float | int | None:
"""Return the native value."""
return self.processor.entity_data.get(self.entity_key)
@property
def available(self) -> bool:
"""Return True if entity is available.
The sensor is only created when the device is seen.
Since these are sleepy devices which stop broadcasting
when not in use, we can't rely on the last update time
so once we have seen the device we always return True.
"""
return True
@property
def assumed_state(self) -> bool:
"""Return True if the device is no longer broadcasting."""
return not self.processor.available
@@ -0,0 +1,50 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"not_supported": "Device not supported"
},
"flow_title": "{name}",
"step": {
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
},
"user": {
"data": {
"address": "[%key:common::config_flow::data::device%]"
},
"data_description": {
"address": "Select the Chef iQ probe you want to set up"
},
"description": "[%key:component::bluetooth::config::step::user::description%]"
}
}
},
"entity": {
"sensor": {
"ambient_temperature": {
"name": "Ambient temperature"
},
"food_temperature": {
"name": "Food temperature"
},
"probe_tip_1_temperature": {
"name": "Probe tip 1 temperature"
},
"probe_tip_2_temperature": {
"name": "Probe tip 2 temperature"
},
"probe_tip_3_temperature": {
"name": "Probe tip 3 temperature"
},
"probe_tip_4_temperature": {
"name": "Probe tip 4 temperature"
},
"soc_temperature": {
"name": "SoC temperature"
}
}
}
}
+5
View File
@@ -93,6 +93,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
"domain": "casper_glow",
"local_name": "Jar*",
},
{
"connectable": False,
"domain": "chef_iq",
"manufacturer_id": 1485,
},
{
"domain": "dormakaba_dkey",
"service_uuid": "e7a60000-6639-429f-94fd-86de8ea26897",
+1
View File
@@ -128,6 +128,7 @@ FLOWS = {
"centriconnect",
"cert_expiry",
"chacon_dio",
"chef_iq",
"chess_com",
"cielo_home",
"cloudflare",
@@ -1031,6 +1031,12 @@
"config_flow": false,
"iot_class": "local_polling"
},
"chef_iq": {
"name": "Chef iQ",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_push"
},
"chess_com": {
"name": "Chess.com",
"integration_type": "service",
Generated
+10
View File
@@ -1176,6 +1176,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.chef_iq.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.clickatell.*]
check_untyped_defs = true
disallow_incomplete_defs = true
+3
View File
@@ -741,6 +741,9 @@ cached-ipaddress==1.1.2
# homeassistant.components.caldav
caldav==2.1.0
# homeassistant.components.chef_iq
chefiq-ble==1.0.1
# homeassistant.components.chess_com
chess-com-api==1.1.0
+65
View File
@@ -0,0 +1,65 @@
"""Tests for the Chef iQ integration."""
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
ADDRESS = "C9:14:65:CA:07:9A"
TITLE = "CQ60 079A"
NOT_CHEFIQ_SERVICE_INFO = BluetoothServiceInfo(
name="Not it",
address="00:00:00:00:00:01",
rssi=-63,
manufacturer_data={3234: b"\x00\x01"},
service_data={},
service_uuids=[],
source="local",
)
# Real CQ60 captures (firmware 5.0.0). The temperature packet (type 0x1) carries
# the food/ambient/tip temperatures; the status packet (type 0x3) carries the
# MAC address, battery percentage and SoC temperature.
CHEFIQ_TEMPERATURE_SERVICE_INFO = BluetoothServiceInfo(
name="CQ60",
address="C9:14:65:CA:07:9A",
rssi=-60,
manufacturer_data={1485: bytes.fromhex("015024012b0135012e012b0130012401315a")},
service_data={},
service_uuids=[],
source="local",
)
CHEFIQ_STATUS_SERVICE_INFO = BluetoothServiceInfo(
name="CQ60",
address="C9:14:65:CA:07:9A",
rssi=-60,
manufacturer_data={1485: bytes.fromhex("0350c91465ca079a64201e010301007ac4")},
service_data={},
service_uuids=[],
source="local",
)
# A 0x05CD advertisement that passes the cheap probe pre-filter but is not a
# recognised probe packet (unknown packet type 0x2); must be rejected.
CHEFIQ_UNSUPPORTED_SERVICE_INFO = BluetoothServiceInfo(
name="CQ60",
address="C9:14:65:CA:07:9A",
rssi=-60,
manufacturer_data={1485: bytes.fromhex("0250")},
service_data={},
service_uuids=[],
source="local",
)
# The iQ Sense base/hub advertises under the same manufacturer id (0x05CD) but
# is not a probe; the config flow must reject it.
IQ_SENSE_SERVICE_INFO = BluetoothServiceInfo(
name="iQ Sense 540",
address="94:54:C5:6D:8A:D6",
rssi=-66,
manufacturer_data={
1485: bytes.fromhex("50754e427588e5133f8d4bb3a403113f437a871e05")
},
service_data={},
service_uuids=[],
source="local",
)
+32
View File
@@ -0,0 +1,32 @@
"""Chef iQ test fixtures."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.chef_iq.const import DOMAIN
from . import ADDRESS
from tests.common import MockConfigEntry
@pytest.fixture(autouse=True)
def mock_bluetooth(enable_bluetooth: None) -> None:
"""Auto-enable Bluetooth for all tests."""
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.chef_iq.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return a mock config entry."""
return MockConfigEntry(domain=DOMAIN, unique_id=ADDRESS)
@@ -0,0 +1,517 @@
# serializer version: 1
# name: test_sensors[sensor.cq60_079a_ambient_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_ambient_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Ambient temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Ambient temperature',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <ChefIqSensor.AMBIENT_TEMPERATURE: 'ambient_temperature'>,
'unique_id': 'C9:14:65:CA:07:9A-ambient_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.cq60_079a_ambient_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'CQ60 079A Ambient temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_ambient_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '29.2',
})
# ---
# name: test_sensors[sensor.cq60_079a_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'C9:14:65:CA:07:9A-battery_percent',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[sensor.cq60_079a_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'CQ60 079A Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_sensors[sensor.cq60_079a_food_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_food_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Food temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Food temperature',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <ChefIqSensor.FOOD_TEMPERATURE: 'food_temperature'>,
'unique_id': 'C9:14:65:CA:07:9A-food_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.cq60_079a_food_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'CQ60 079A Food temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_food_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '29.9',
})
# ---
# name: test_sensors[sensor.cq60_079a_probe_tip_1_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_probe_tip_1_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Probe tip 1 temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe tip 1 temperature',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <ChefIqSensor.PROBE_TIP_1_TEMPERATURE: 'probe_tip_1_temperature'>,
'unique_id': 'C9:14:65:CA:07:9A-probe_tip_1_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.cq60_079a_probe_tip_1_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'CQ60 079A Probe tip 1 temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_probe_tip_1_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30.9',
})
# ---
# name: test_sensors[sensor.cq60_079a_probe_tip_2_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_probe_tip_2_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Probe tip 2 temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe tip 2 temperature',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <ChefIqSensor.PROBE_TIP_2_TEMPERATURE: 'probe_tip_2_temperature'>,
'unique_id': 'C9:14:65:CA:07:9A-probe_tip_2_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.cq60_079a_probe_tip_2_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'CQ60 079A Probe tip 2 temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_probe_tip_2_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30.2',
})
# ---
# name: test_sensors[sensor.cq60_079a_probe_tip_3_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_probe_tip_3_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Probe tip 3 temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe tip 3 temperature',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <ChefIqSensor.PROBE_TIP_3_TEMPERATURE: 'probe_tip_3_temperature'>,
'unique_id': 'C9:14:65:CA:07:9A-probe_tip_3_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.cq60_079a_probe_tip_3_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'CQ60 079A Probe tip 3 temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_probe_tip_3_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '29.9',
})
# ---
# name: test_sensors[sensor.cq60_079a_probe_tip_4_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_probe_tip_4_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Probe tip 4 temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe tip 4 temperature',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <ChefIqSensor.PROBE_TIP_4_TEMPERATURE: 'probe_tip_4_temperature'>,
'unique_id': 'C9:14:65:CA:07:9A-probe_tip_4_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.cq60_079a_probe_tip_4_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'CQ60 079A Probe tip 4 temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_probe_tip_4_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30.4',
})
# ---
# name: test_sensors[sensor.cq60_079a_signal_strength-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_signal_strength',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Signal strength',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
'original_icon': None,
'original_name': 'Signal strength',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'C9:14:65:CA:07:9A-signal_strength',
'unit_of_measurement': 'dBm',
})
# ---
# name: test_sensors[sensor.cq60_079a_signal_strength-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'signal_strength',
'friendly_name': 'CQ60 079A Signal strength',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'dBm',
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_signal_strength',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-60',
})
# ---
# name: test_sensors[sensor.cq60_079a_soc_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.cq60_079a_soc_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'SoC temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'SoC temperature',
'platform': 'chef_iq',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <ChefIqSensor.SOC_TEMPERATURE: 'soc_temperature'>,
'unique_id': 'C9:14:65:CA:07:9A-soc_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.cq60_079a_soc_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'CQ60 079A SoC temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cq60_079a_soc_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '32',
})
# ---
@@ -0,0 +1,240 @@
"""Test the Chef iQ config flow."""
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.chef_iq.const import DOMAIN
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_IGNORE, SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from . import (
ADDRESS,
CHEFIQ_TEMPERATURE_SERVICE_INFO,
CHEFIQ_UNSUPPORTED_SERVICE_INFO,
IQ_SENSE_SERVICE_INFO,
NOT_CHEFIQ_SERVICE_INFO,
TITLE,
)
from tests.common import MockConfigEntry
DISCOVERY = "homeassistant.components.chef_iq.config_flow.async_discovered_service_info"
async def test_async_step_bluetooth_valid_device(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test discovery via bluetooth with a valid device."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_BLUETOOTH},
data=CHEFIQ_TEMPERATURE_SERVICE_INFO,
)
assert result["type"] is FlowResultType.FORM
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["title"] == TITLE
assert result["data"] == {}
assert result["result"].unique_id == ADDRESS
@pytest.mark.parametrize(
"service_info",
[
NOT_CHEFIQ_SERVICE_INFO,
IQ_SENSE_SERVICE_INFO,
CHEFIQ_UNSUPPORTED_SERVICE_INFO,
],
)
async def test_async_step_bluetooth_not_supported(
hass: HomeAssistant, service_info: BluetoothServiceInfo
) -> None:
"""Test bluetooth discovery rejects advertisements that are not a probe."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_BLUETOOTH},
data=service_info,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "not_supported"
async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None:
"""Test setup from service info cache with no devices found."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_async_step_user_with_found_devices(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test setup from service info cache with devices found."""
with patch(DISCOVERY, return_value=[CHEFIQ_TEMPERATURE_SERVICE_INFO]):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": 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": ADDRESS},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TITLE
assert result["data"] == {}
assert result["result"].unique_id == ADDRESS
@pytest.mark.parametrize(
("discovered", "existing_entry"),
[
([IQ_SENSE_SERVICE_INFO], False),
([CHEFIQ_TEMPERATURE_SERVICE_INFO], True),
],
)
async def test_async_step_user_no_eligible_devices(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
discovered: list[BluetoothServiceInfo],
existing_entry: bool,
) -> None:
"""Test the user step aborts when no eligible device can be offered.
Either the only discovered device is the unsupported iQ Sense hub, or the
discovered probe is already configured.
"""
if existing_entry:
mock_config_entry.add_to_hass(hass)
with patch(DISCOVERY, return_value=discovered):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_async_step_bluetooth_devices_already_setup(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test we can't start a flow if there is already a config entry."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_BLUETOOTH},
data=CHEFIQ_TEMPERATURE_SERVICE_INFO,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> None:
"""Test we can't start a flow for the same device twice."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_BLUETOOTH},
data=CHEFIQ_TEMPERATURE_SERVICE_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "bluetooth_confirm"
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_BLUETOOTH},
data=CHEFIQ_TEMPERATURE_SERVICE_INFO,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_in_progress"
async def test_async_step_user_device_added_between_steps(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test the device gets added via another flow between steps."""
with patch(DISCOVERY, return_value=[CHEFIQ_TEMPERATURE_SERVICE_INFO]):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"address": ADDRESS},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_async_step_user_replace_ignored(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test setup from service info can replace an ignored entry."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=ADDRESS,
source=SOURCE_IGNORE,
data={},
)
entry.add_to_hass(hass)
with patch(DISCOVERY, return_value=[CHEFIQ_TEMPERATURE_SERVICE_INFO]):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": 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": ADDRESS},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TITLE
assert result["result"].unique_id == ADDRESS
async def test_async_step_user_takes_precedence_over_discovery(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test manual setup takes precedence over discovery."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_BLUETOOTH},
data=CHEFIQ_TEMPERATURE_SERVICE_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "bluetooth_confirm"
with patch(DISCOVERY, return_value=[CHEFIQ_TEMPERATURE_SERVICE_INFO]):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"address": ADDRESS},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TITLE
assert result["result"].unique_id == ADDRESS
# Verify the original discovery flow was aborted.
assert not hass.config_entries.flow.async_progress(DOMAIN)
+83
View File
@@ -0,0 +1,83 @@
"""Test the Chef iQ sensors."""
from datetime import timedelta
import time
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.bluetooth import (
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
)
from homeassistant.const import ATTR_ASSUMED_STATE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from . import CHEFIQ_STATUS_SERVICE_INFO, CHEFIQ_TEMPERATURE_SERVICE_INFO
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.components.bluetooth import (
inject_bluetooth_service_info,
patch_all_discovered_devices,
patch_bluetooth_time,
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting up creates the sensors from the rotating packet types."""
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert not hass.states.async_all("sensor")
# The temperature packet creates the six temperatures (plus signal strength);
# the status packet adds battery and SoC temperature.
inject_bluetooth_service_info(hass, CHEFIQ_TEMPERATURE_SERVICE_INFO)
await hass.async_block_till_done()
inject_bluetooth_service_info(hass, CHEFIQ_STATUS_SERVICE_INFO)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sleepy_device_keeps_state(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the probe keeps its state and goes to assumed_state when idle."""
start_monotonic = time.monotonic()
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
inject_bluetooth_service_info(hass, CHEFIQ_TEMPERATURE_SERVICE_INFO)
await hass.async_block_till_done()
food = hass.states.get("sensor.cq60_079a_food_temperature")
assert food.state == "29.9"
assert ATTR_ASSUMED_STATE not in food.attributes
# Fast-forward past the stale-advertisement window with no advertisements.
monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
with (
patch_bluetooth_time(monotonic_now),
patch_all_discovered_devices([]),
):
async_fire_time_changed(
hass,
dt_util.utcnow()
+ timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1),
)
await hass.async_block_till_done()
# Sleepy devices keep their last value and report assumed_state.
food = hass.states.get("sensor.cq60_079a_food_temperature")
assert food.state == "29.9"
assert food.attributes[ATTR_ASSUMED_STATE] is True