mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 06:47:09 +00:00
Tilt Pi integration (#139726)
* Create component via script.scaffold * Create sensor definition * Define coordinator * Define config flow * Refine sensor definition and add tests * Refine coordinator after testing end to end * Redefine sensor in a more idiomatic way * Use entity (common-module) * Follow config-flow conventions more closely * Use custom ConfigEntry to conform to strict-typing * Define API object instead of using aio directly * Test before setup in init * Add diagnostics * Make some more quality changes * Move scan interval to const * Commit generated files * Add quality scale * feedback: Apply consistent language to Tilt Pi refs * feedback: Remove empty manifest fields * feedback: Use translations instead of hardcoded name * feedback: Remove diagnostics * feedback: Idiomatic and general improvements * Use tilt-pi library * feedback: Coordinator data returns dict * feedback: Move client creation to coordinator * feedback: Request only Tilt Pi URL from user * Update homeassistant/components/tilt_pi/entity.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/tilt_pi/sensor.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/tilt_pi/entity.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * feedback: Avoid redundant keyword arguments in function calls * feedback: Remove unused models and variables * feedback: Use icons.json * feedback: Style best practices * Update homeassistant/components/tilt_pi/entity.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update tests/components/tilt_pi/test_config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * feedback: Improve config flow unit tests * feedback: Patch TiltPi client mock * feedback: Mark entity-device-class as done * feedback: Align quaity scale with current state * feeback: Create brands file for Tilt brand * feedback: Demonstrate recovery in config flow * feedback: Test coordinator behavior via sensors * Update homeassistant/components/tilt_pi/config_flow.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/tilt_pi/coordinator.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/tilt_pi/quality_scale.yaml Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/tilt_pi/quality_scale.yaml Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/tilt_pi/quality_scale.yaml Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/tilt_pi/config_flow.py Co-authored-by: Josef Zweck <josef@zweck.dev> * feedback: Update tilt_pi quality scale * feedback: Move const to coordinator * feedback: Correct strings.json for incorrect and missing fields * feedback: Use tiltpi package version published via CI * Run ruff format manually * Add missing string for invalid host * Fix * Fix --------- Co-authored-by: Michael Heyman <michaelheyman@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
parent
0c08b4fc8b
commit
b48ebeaa8a
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -1580,6 +1580,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tile/ @bachya
|
/tests/components/tile/ @bachya
|
||||||
/homeassistant/components/tilt_ble/ @apt-itude
|
/homeassistant/components/tilt_ble/ @apt-itude
|
||||||
/tests/components/tilt_ble/ @apt-itude
|
/tests/components/tilt_ble/ @apt-itude
|
||||||
|
/homeassistant/components/tilt_pi/ @michaelheyman
|
||||||
|
/tests/components/tilt_pi/ @michaelheyman
|
||||||
/homeassistant/components/time/ @home-assistant/core
|
/homeassistant/components/time/ @home-assistant/core
|
||||||
/tests/components/time/ @home-assistant/core
|
/tests/components/time/ @home-assistant/core
|
||||||
/homeassistant/components/time_date/ @fabaff
|
/homeassistant/components/time_date/ @fabaff
|
||||||
|
5
homeassistant/brands/tilt.json
Normal file
5
homeassistant/brands/tilt.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "tilt",
|
||||||
|
"name": "Tilt",
|
||||||
|
"integrations": ["tilt_ble", "tilt_pi"]
|
||||||
|
}
|
28
homeassistant/components/tilt_pi/__init__.py
Normal file
28
homeassistant/components/tilt_pi/__init__.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""The Tilt Pi integration."""
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import TiltPiConfigEntry, TiltPiDataUpdateCoordinator
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: TiltPiConfigEntry) -> bool:
|
||||||
|
"""Set up Tilt Pi from a config entry."""
|
||||||
|
coordinator = TiltPiDataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: TiltPiConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
63
homeassistant/components/tilt_pi/config_flow.py
Normal file
63
homeassistant/components/tilt_pi/config_flow.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"""Config flow for Tilt Pi integration."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from tiltpi import TiltPiClient, TiltPiError
|
||||||
|
import voluptuous as vol
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_URL
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class TiltPiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Tilt Pi."""
|
||||||
|
|
||||||
|
async def _check_connection(self, host: str, port: int) -> str | None:
|
||||||
|
"""Check if we can connect to the TiltPi instance."""
|
||||||
|
client = TiltPiClient(
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
session=async_get_clientsession(self.hass),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await client.get_hydrometers()
|
||||||
|
except (TiltPiError, TimeoutError, aiohttp.ClientError):
|
||||||
|
return "cannot_connect"
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a configuration flow initialized by the user."""
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
url = URL(user_input[CONF_URL])
|
||||||
|
if (host := url.host) is None:
|
||||||
|
errors[CONF_URL] = "invalid_host"
|
||||||
|
else:
|
||||||
|
self._async_abort_entries_match({CONF_HOST: host})
|
||||||
|
port = url.port
|
||||||
|
assert port
|
||||||
|
error = await self._check_connection(host=host, port=port)
|
||||||
|
if error:
|
||||||
|
errors["base"] = error
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="Tilt Pi",
|
||||||
|
data={
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_URL): str}),
|
||||||
|
errors=errors,
|
||||||
|
)
|
8
homeassistant/components/tilt_pi/const.py
Normal file
8
homeassistant/components/tilt_pi/const.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""Constants for the Tilt Pi integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
DOMAIN: Final = "tilt_pi"
|
53
homeassistant/components/tilt_pi/coordinator.py
Normal file
53
homeassistant/components/tilt_pi/coordinator.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Data update coordinator for Tilt Pi."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from tiltpi import TiltHydrometerData, TiltPiClient, TiltPiError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import LOGGER
|
||||||
|
|
||||||
|
SCAN_INTERVAL: Final = timedelta(seconds=60)
|
||||||
|
|
||||||
|
type TiltPiConfigEntry = ConfigEntry[TiltPiDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class TiltPiDataUpdateCoordinator(DataUpdateCoordinator[dict[str, TiltHydrometerData]]):
|
||||||
|
"""Class to manage fetching Tilt Pi data."""
|
||||||
|
|
||||||
|
config_entry: TiltPiConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: TiltPiConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name="Tilt Pi",
|
||||||
|
update_interval=SCAN_INTERVAL,
|
||||||
|
)
|
||||||
|
self._api = TiltPiClient(
|
||||||
|
host=config_entry.data[CONF_HOST],
|
||||||
|
port=config_entry.data[CONF_PORT],
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
)
|
||||||
|
self.identifier = config_entry.entry_id
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, TiltHydrometerData]:
|
||||||
|
"""Fetch data from Tilt Pi and return as a dict keyed by mac_id."""
|
||||||
|
try:
|
||||||
|
hydrometers = await self._api.get_hydrometers()
|
||||||
|
except TiltPiError as err:
|
||||||
|
raise UpdateFailed(f"Error communicating with Tilt Pi: {err}") from err
|
||||||
|
|
||||||
|
return {h.mac_id: h for h in hydrometers}
|
39
homeassistant/components/tilt_pi/entity.py
Normal file
39
homeassistant/components/tilt_pi/entity.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""Base entity for Tilt Pi integration."""
|
||||||
|
|
||||||
|
from tiltpi import TiltHydrometerData
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .coordinator import TiltPiDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class TiltEntity(CoordinatorEntity[TiltPiDataUpdateCoordinator]):
|
||||||
|
"""Base class for Tilt entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: TiltPiDataUpdateCoordinator,
|
||||||
|
hydrometer: TiltHydrometerData,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._mac_id = hydrometer.mac_id
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, hydrometer.mac_id)},
|
||||||
|
name=f"Tilt {hydrometer.color}",
|
||||||
|
manufacturer="Tilt Hydrometer",
|
||||||
|
model=f"{hydrometer.color} Tilt Hydrometer",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_hydrometer(self) -> TiltHydrometerData:
|
||||||
|
"""Return the current hydrometer data for this entity."""
|
||||||
|
return self.coordinator.data[self._mac_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if the hydrometer is available (present in coordinator data)."""
|
||||||
|
return super().available and self._mac_id in self.coordinator.data
|
9
homeassistant/components/tilt_pi/icons.json
Normal file
9
homeassistant/components/tilt_pi/icons.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"gravity": {
|
||||||
|
"default": "mdi:water"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
homeassistant/components/tilt_pi/manifest.json
Normal file
10
homeassistant/components/tilt_pi/manifest.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"domain": "tilt_pi",
|
||||||
|
"name": "Tilt Pi",
|
||||||
|
"codeowners": ["@michaelheyman"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/tilt_pi",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["tilt-pi==0.2.1"]
|
||||||
|
}
|
80
homeassistant/components/tilt_pi/quality_scale.yaml
Normal file
80
homeassistant/components/tilt_pi/quality_scale.yaml
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide additional actions.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Entities of this integration does not explicitly subscribe to events.
|
||||||
|
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:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: No options to configure
|
||||||
|
docs-installation-parameters: done
|
||||||
|
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: todo
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category:
|
||||||
|
status: done
|
||||||
|
comment: |
|
||||||
|
The entities are categorized well by using default category.
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default:
|
||||||
|
status: exempt
|
||||||
|
comment: No disabled entities implemented
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No repairs/issues.
|
||||||
|
stale-devices: todo
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: todo
|
93
homeassistant/components/tilt_pi/sensor.py
Normal file
93
homeassistant/components/tilt_pi/sensor.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
"""Support for Tilt Hydrometer sensors."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from tiltpi import TiltHydrometerData
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import UnitOfTemperature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
|
from .coordinator import TiltPiConfigEntry, TiltPiDataUpdateCoordinator
|
||||||
|
from .entity import TiltEntity
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
ATTR_TEMPERATURE = "temperature"
|
||||||
|
ATTR_GRAVITY = "gravity"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TiltEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes TiltHydrometerData sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[TiltHydrometerData], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_TYPES: Final[list[TiltEntityDescription]] = [
|
||||||
|
TiltEntityDescription(
|
||||||
|
key=ATTR_TEMPERATURE,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.temperature,
|
||||||
|
),
|
||||||
|
TiltEntityDescription(
|
||||||
|
key=ATTR_GRAVITY,
|
||||||
|
translation_key="gravity",
|
||||||
|
native_unit_of_measurement="SG",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.gravity,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: TiltPiConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Tilt Hydrometer sensors."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
TiltSensor(
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
hydrometer,
|
||||||
|
)
|
||||||
|
for description in SENSOR_TYPES
|
||||||
|
for hydrometer in coordinator.data.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TiltSensor(TiltEntity, SensorEntity):
|
||||||
|
"""Defines a Tilt sensor."""
|
||||||
|
|
||||||
|
entity_description: TiltEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: TiltPiDataUpdateCoordinator,
|
||||||
|
description: TiltEntityDescription,
|
||||||
|
hydrometer: TiltHydrometerData,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator, hydrometer)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{hydrometer.mac_id}_{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the sensor value."""
|
||||||
|
return self.entity_description.value_fn(self.current_hydrometer)
|
31
homeassistant/components/tilt_pi/strings.json
Normal file
31
homeassistant/components/tilt_pi/strings.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"url": "[%key:common::config_flow::data::url%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"url": "The URL of the Tilt Pi instance."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_host": "[%key:common::config_flow::error::invalid_host%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"gravity": {
|
||||||
|
"name": "Gravity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -647,6 +647,7 @@ FLOWS = {
|
|||||||
"tibber",
|
"tibber",
|
||||||
"tile",
|
"tile",
|
||||||
"tilt_ble",
|
"tilt_ble",
|
||||||
|
"tilt_pi",
|
||||||
"time_date",
|
"time_date",
|
||||||
"todoist",
|
"todoist",
|
||||||
"tolo",
|
"tolo",
|
||||||
|
@ -6745,11 +6745,22 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
|
"tilt": {
|
||||||
|
"name": "Tilt",
|
||||||
|
"integrations": {
|
||||||
"tilt_ble": {
|
"tilt_ble": {
|
||||||
"name": "Tilt Hydrometer BLE",
|
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push",
|
||||||
|
"name": "Tilt Hydrometer BLE"
|
||||||
|
},
|
||||||
|
"tilt_pi": {
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"name": "Tilt Pi"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"time_date": {
|
"time_date": {
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
|
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@ -2936,6 +2936,9 @@ tikteck==0.4
|
|||||||
# homeassistant.components.tilt_ble
|
# homeassistant.components.tilt_ble
|
||||||
tilt-ble==0.2.3
|
tilt-ble==0.2.3
|
||||||
|
|
||||||
|
# homeassistant.components.tilt_pi
|
||||||
|
tilt-pi==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.tmb
|
# homeassistant.components.tmb
|
||||||
tmb==0.0.4
|
tmb==0.0.4
|
||||||
|
|
||||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -2413,6 +2413,9 @@ thinqconnect==1.0.5
|
|||||||
# homeassistant.components.tilt_ble
|
# homeassistant.components.tilt_ble
|
||||||
tilt-ble==0.2.3
|
tilt-ble==0.2.3
|
||||||
|
|
||||||
|
# homeassistant.components.tilt_pi
|
||||||
|
tilt-pi==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.todoist
|
# homeassistant.components.todoist
|
||||||
todoist-api-python==2.1.7
|
todoist-api-python==2.1.7
|
||||||
|
|
||||||
|
12
tests/components/tilt_pi/__init__.py
Normal file
12
tests/components/tilt_pi/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"""Tests for the Tilt Pi integration."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||||
|
"""Fixture for setting up the integration."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
70
tests/components/tilt_pi/conftest.py
Normal file
70
tests/components/tilt_pi/conftest.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
"""Common fixtures for the Tilt Pi tests."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tiltpi import TiltColor, TiltHydrometerData
|
||||||
|
|
||||||
|
from homeassistant.components.tilt_pi.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
TEST_NAME = "Test Tilt Pi"
|
||||||
|
TEST_HOST = "192.168.1.123"
|
||||||
|
TEST_PORT = 1880
|
||||||
|
TEST_URL = f"http://{TEST_HOST}:{TEST_PORT}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tilt_pi.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Return the default mocked config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: TEST_HOST,
|
||||||
|
CONF_PORT: TEST_PORT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_tiltpi_client() -> Generator[AsyncMock]:
|
||||||
|
"""Mock a TiltPi client."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.tilt_pi.coordinator.TiltPiClient",
|
||||||
|
autospec=True,
|
||||||
|
) as mock_client,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.tilt_pi.config_flow.TiltPiClient",
|
||||||
|
new=mock_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
client = mock_client.return_value
|
||||||
|
client.get_hydrometers.return_value = [
|
||||||
|
TiltHydrometerData(
|
||||||
|
mac_id="00:1A:2B:3C:4D:5E",
|
||||||
|
color=TiltColor.BLACK,
|
||||||
|
temperature=55.0,
|
||||||
|
gravity=1.010,
|
||||||
|
),
|
||||||
|
TiltHydrometerData(
|
||||||
|
mac_id="00:1s:99:f1:d2:4f",
|
||||||
|
color=TiltColor.YELLOW,
|
||||||
|
temperature=68.0,
|
||||||
|
gravity=1.015,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
yield client
|
217
tests/components/tilt_pi/snapshots/test_sensor.ambr
Normal file
217
tests/components/tilt_pi/snapshots/test_sensor.ambr
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_all_sensors[sensor.tilt_black_gravity-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.tilt_black_gravity',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Gravity',
|
||||||
|
'platform': 'tilt_pi',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'gravity',
|
||||||
|
'unique_id': '00:1A:2B:3C:4D:5E_gravity',
|
||||||
|
'unit_of_measurement': 'SG',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_sensors[sensor.tilt_black_gravity-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Tilt Black Gravity',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'SG',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.tilt_black_gravity',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1.01',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_sensors[sensor.tilt_black_temperature-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.tilt_black_temperature',
|
||||||
|
'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': 'Temperature',
|
||||||
|
'platform': 'tilt_pi',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '00:1A:2B:3C:4D:5E_temperature',
|
||||||
|
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_sensors[sensor.tilt_black_temperature-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'temperature',
|
||||||
|
'friendly_name': 'Tilt Black Temperature',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.tilt_black_temperature',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '12.7777777777778',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_sensors[sensor.tilt_yellow_gravity-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.tilt_yellow_gravity',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Gravity',
|
||||||
|
'platform': 'tilt_pi',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'gravity',
|
||||||
|
'unique_id': '00:1s:99:f1:d2:4f_gravity',
|
||||||
|
'unit_of_measurement': 'SG',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_sensors[sensor.tilt_yellow_gravity-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Tilt Yellow Gravity',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'SG',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.tilt_yellow_gravity',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1.015',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_sensors[sensor.tilt_yellow_temperature-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.tilt_yellow_temperature',
|
||||||
|
'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': 'Temperature',
|
||||||
|
'platform': 'tilt_pi',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '00:1s:99:f1:d2:4f_temperature',
|
||||||
|
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_sensors[sensor.tilt_yellow_temperature-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'temperature',
|
||||||
|
'friendly_name': 'Tilt Yellow Temperature',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.tilt_yellow_temperature',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '20.0',
|
||||||
|
})
|
||||||
|
# ---
|
125
tests/components/tilt_pi/test_config_flow.py
Normal file
125
tests/components/tilt_pi/test_config_flow.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"""Test the Tilt config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from homeassistant.components.tilt_pi.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_URL
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_step_user_gets_form_and_creates_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_tiltpi_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the we can view the form and that the config flow creates an entry."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_URL: "http://192.168.1.123:1880"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_HOST: "192.168.1.123",
|
||||||
|
CONF_PORT: 1880,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abort_if_already_configured(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that we abort if we attempt to submit the same entry twice."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
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={CONF_URL: "http://192.168.1.123:1880"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_successful_recovery_after_invalid_host(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_tiltpi_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test error shown when user submits invalid host."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simulate a invalid host error by providing an invalid URL
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_URL: "not-a-valid-url"},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"url": "invalid_host"}
|
||||||
|
|
||||||
|
# Demonstrate successful connection on retry
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_URL: "http://192.168.1.123:1880"},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_HOST: "192.168.1.123",
|
||||||
|
CONF_PORT: 1880,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_successful_recovery_after_connection_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_tiltpi_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test error shown when connection fails."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simulate a connection error by raising a TimeoutError
|
||||||
|
mock_tiltpi_client.get_hydrometers.side_effect = TimeoutError()
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_URL: "http://192.168.1.123:1880"},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
# Simulate successful connection on retry
|
||||||
|
mock_tiltpi_client.get_hydrometers.side_effect = None
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_URL: "http://192.168.1.123:1880"},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_HOST: "192.168.1.123",
|
||||||
|
CONF_PORT: 1880,
|
||||||
|
}
|
84
tests/components/tilt_pi/test_sensor.py
Normal file
84
tests/components/tilt_pi/test_sensor.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""Test the Tilt Hydrometer sensors."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
from tiltpi import TiltColor, TiltPiConnectionError
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||||
|
|
||||||
|
|
||||||
|
async def test_all_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_tiltpi_client: AsyncMock,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test the Tilt Pi sensors.
|
||||||
|
|
||||||
|
When making changes to this test, ensure that the snapshot reflects the
|
||||||
|
new data by generating it via:
|
||||||
|
|
||||||
|
$ pytest tests/components/tilt_pi/test_sensor.py -v --snapshot-update
|
||||||
|
"""
|
||||||
|
with patch("homeassistant.components.tilt_pi.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_availability(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_tiltpi_client: AsyncMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that entities become unavailable when the coordinator fails."""
|
||||||
|
with patch("homeassistant.components.tilt_pi.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
# Simulate a coordinator update failure
|
||||||
|
mock_tiltpi_client.get_hydrometers.side_effect = TiltPiConnectionError()
|
||||||
|
freezer.tick(60)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Check that entities are unavailable
|
||||||
|
for color in (TiltColor.BLACK, TiltColor.YELLOW):
|
||||||
|
temperature_entity_id = f"sensor.tilt_{color}_temperature"
|
||||||
|
gravity_entity_id = f"sensor.tilt_{color}_gravity"
|
||||||
|
|
||||||
|
temperature_state = hass.states.get(temperature_entity_id)
|
||||||
|
assert temperature_state is not None
|
||||||
|
assert temperature_state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
gravity_state = hass.states.get(gravity_entity_id)
|
||||||
|
assert gravity_state is not None
|
||||||
|
assert gravity_state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
# Simulate a coordinator update success
|
||||||
|
mock_tiltpi_client.get_hydrometers.side_effect = None
|
||||||
|
freezer.tick(60)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Check that entities are now available
|
||||||
|
for color in (TiltColor.BLACK, TiltColor.YELLOW):
|
||||||
|
temperature_entity_id = f"sensor.tilt_{color}_temperature"
|
||||||
|
gravity_entity_id = f"sensor.tilt_{color}_gravity"
|
||||||
|
|
||||||
|
temperature_state = hass.states.get(temperature_entity_id)
|
||||||
|
assert temperature_state is not None
|
||||||
|
assert temperature_state.state != STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
gravity_state = hass.states.get(gravity_entity_id)
|
||||||
|
assert gravity_state is not None
|
||||||
|
assert gravity_state.state != STATE_UNAVAILABLE
|
Loading…
x
Reference in New Issue
Block a user