mirror of
https://github.com/home-assistant/core.git
synced 2025-08-05 21:55:10 +02:00
Merge remote-tracking branch 'upstream/sub_devices_esphome' into sub_devices_esphome
This commit is contained in:
@@ -67,6 +67,7 @@ homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.alexa_devices.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.altruist.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
homeassistant.components.ambient_network.*
|
||||
|
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -93,6 +93,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/alexa_devices/ @chemelli74
|
||||
/tests/components/alexa_devices/ @chemelli74
|
||||
/homeassistant/components/altruist/ @airalab @LoSk-p
|
||||
/tests/components/altruist/ @airalab @LoSk-p
|
||||
/homeassistant/components/amazon_polly/ @jschlyter
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
@@ -1580,6 +1582,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tile/ @bachya
|
||||
/homeassistant/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
|
||||
/tests/components/time/ @home-assistant/core
|
||||
/homeassistant/components/time_date/ @fabaff
|
||||
|
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"integrations": ["switchbot", "switchbot_cloud"]
|
||||
"integrations": ["switchbot", "switchbot_cloud"],
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
|
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"]
|
||||
}
|
@@ -5,6 +5,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import (
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
@@ -17,9 +18,17 @@ from homeassistant.helpers import config_validation as cv, storage
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
|
||||
from .const import DATA_COMPONENT, DATA_PREFERENCES, DOMAIN, AITaskEntityFeature
|
||||
from .const import (
|
||||
ATTR_INSTRUCTIONS,
|
||||
ATTR_TASK_NAME,
|
||||
DATA_COMPONENT,
|
||||
DATA_PREFERENCES,
|
||||
DOMAIN,
|
||||
SERVICE_GENERATE_TEXT,
|
||||
AITaskEntityFeature,
|
||||
)
|
||||
from .entity import AITaskEntity
|
||||
from .http import async_setup as async_setup_conversation_http
|
||||
from .http import async_setup as async_setup_http
|
||||
from .task import GenTextTask, GenTextTaskResult, async_generate_text
|
||||
|
||||
__all__ = [
|
||||
@@ -45,16 +54,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.data[DATA_COMPONENT] = entity_component
|
||||
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||
await hass.data[DATA_PREFERENCES].async_load()
|
||||
async_setup_conversation_http(hass)
|
||||
async_setup_http(hass)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"generate_text",
|
||||
SERVICE_GENERATE_TEXT,
|
||||
async_service_generate_text,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required("task_name"): cv.string,
|
||||
vol.Optional("entity_id"): cv.entity_id,
|
||||
vol.Required("instructions"): cv.string,
|
||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntFlag
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -17,6 +17,11 @@ DOMAIN = "ai_task"
|
||||
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
||||
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
||||
|
||||
SERVICE_GENERATE_TEXT = "generate_text"
|
||||
|
||||
ATTR_INSTRUCTIONS: Final = "instructions"
|
||||
ATTR_TASK_NAME: Final = "task_name"
|
||||
|
||||
DEFAULT_SYSTEM_PROMPT = (
|
||||
"You are a Home Assistant expert and help users with their tasks."
|
||||
)
|
||||
|
@@ -6,7 +6,7 @@ generate_text:
|
||||
selector:
|
||||
text:
|
||||
instructions:
|
||||
example: "Generate a funny notification that garage door was left open"
|
||||
example: "Generate a funny notification that the garage door was left open"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"description": "Use AI to run a task that generates text.",
|
||||
"fields": {
|
||||
"task_name": {
|
||||
"name": "Task Name",
|
||||
"name": "Task name",
|
||||
"description": "Name of the task."
|
||||
},
|
||||
"instructions": {
|
||||
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
||||
|
||||
@@ -21,14 +22,16 @@ async def async_generate_text(
|
||||
entity_id = hass.data[DATA_PREFERENCES].gen_text_entity_id
|
||||
|
||||
if entity_id is None:
|
||||
raise ValueError("No entity_id provided and no preferred entity set")
|
||||
raise HomeAssistantError("No entity_id provided and no preferred entity set")
|
||||
|
||||
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||
if entity is None:
|
||||
raise ValueError(f"AI Task entity {entity_id} not found")
|
||||
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
|
||||
|
||||
if AITaskEntityFeature.GENERATE_TEXT not in entity.supported_features:
|
||||
raise ValueError(f"AI Task entity {entity_id} does not support generating text")
|
||||
raise HomeAssistantError(
|
||||
f"AI Task entity {entity_id} does not support generating text"
|
||||
)
|
||||
|
||||
return await entity.internal_async_generate_text(
|
||||
GenTextTask(
|
||||
|
27
homeassistant/components/altruist/__init__.py
Normal file
27
homeassistant/components/altruist/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""The Altruist integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AltruistConfigEntry, AltruistDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AltruistConfigEntry) -> bool:
|
||||
"""Set up Altruist from a config entry."""
|
||||
|
||||
coordinator = AltruistDataUpdateCoordinator(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: AltruistConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
107
homeassistant/components/altruist/config_flow.py
Normal file
107
homeassistant/components/altruist/config_flow.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Config flow for the Altruist integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from altruistclient import AltruistClient, AltruistDeviceModel, AltruistError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import CONF_HOST, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AltruistConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Altruist."""
|
||||
|
||||
device: AltruistDeviceModel
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
ip_address = ""
|
||||
if user_input is not None:
|
||||
ip_address = user_input[CONF_HOST]
|
||||
try:
|
||||
client = await AltruistClient.from_ip_address(
|
||||
async_get_clientsession(self.hass), ip_address
|
||||
)
|
||||
except AltruistError:
|
||||
errors["base"] = "no_device_found"
|
||||
else:
|
||||
self.device = client.device
|
||||
await self.async_set_unique_id(
|
||||
client.device_id, raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self.device.id,
|
||||
data={
|
||||
CONF_HOST: ip_address,
|
||||
},
|
||||
)
|
||||
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
{CONF_HOST: ip_address},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"ip_address": ip_address,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
_LOGGER.debug("Zeroconf discovery: %s", discovery_info)
|
||||
try:
|
||||
client = await AltruistClient.from_ip_address(
|
||||
async_get_clientsession(self.hass), str(discovery_info.ip_address)
|
||||
)
|
||||
except AltruistError:
|
||||
return self.async_abort(reason="no_device_found")
|
||||
|
||||
self.device = client.device
|
||||
_LOGGER.debug("Zeroconf device: %s", client.device)
|
||||
await self.async_set_unique_id(client.device_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
self.context.update(
|
||||
{
|
||||
"title_placeholders": {
|
||||
"name": self.device.id,
|
||||
}
|
||||
}
|
||||
)
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self.device.id,
|
||||
data={
|
||||
CONF_HOST: self.device.ip_address,
|
||||
},
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={
|
||||
"model": self.device.id,
|
||||
},
|
||||
)
|
5
homeassistant/components/altruist/const.py
Normal file
5
homeassistant/components/altruist/const.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Constants for the Altruist integration."""
|
||||
|
||||
DOMAIN = "altruist"
|
||||
|
||||
CONF_HOST = "host"
|
64
homeassistant/components/altruist/coordinator.py
Normal file
64
homeassistant/components/altruist/coordinator.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Coordinator module for Altruist integration in Home Assistant.
|
||||
|
||||
This module defines the AltruistDataUpdateCoordinator class, which manages
|
||||
data updates for Altruist sensors using the AltruistClient.
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from altruistclient import AltruistClient, AltruistError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
type AltruistConfigEntry = ConfigEntry[AltruistDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AltruistDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
"""Coordinates data updates for Altruist sensors."""
|
||||
|
||||
client: AltruistClient
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: AltruistConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the data update coordinator for Altruist sensors."""
|
||||
device_id = config_entry.unique_id
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"Altruist {device_id}",
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
self._ip_address = config_entry.data[CONF_HOST]
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
try:
|
||||
self.client = await AltruistClient.from_ip_address(
|
||||
async_get_clientsession(self.hass), self._ip_address
|
||||
)
|
||||
await self.client.fetch_data()
|
||||
except AltruistError as e:
|
||||
raise ConfigEntryNotReady("Error in Altruist setup") from e
|
||||
|
||||
async def _async_update_data(self) -> dict[str, str]:
|
||||
try:
|
||||
fetched_data = await self.client.fetch_data()
|
||||
except AltruistError as ex:
|
||||
raise UpdateFailed(
|
||||
f"The Altruist {self.client.device_id} is unavailable: {ex}"
|
||||
) from ex
|
||||
return {item["value_type"]: item["value"] for item in fetched_data}
|
15
homeassistant/components/altruist/icons.json
Normal file
15
homeassistant/components/altruist/icons.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"pm_10": {
|
||||
"default": "mdi:thought-bubble"
|
||||
},
|
||||
"pm_25": {
|
||||
"default": "mdi:thought-bubble-outline"
|
||||
},
|
||||
"radiation": {
|
||||
"default": "mdi:radioactive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
homeassistant/components/altruist/manifest.json
Normal file
12
homeassistant/components/altruist/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "altruist",
|
||||
"name": "Altruist",
|
||||
"codeowners": ["@airalab", "@LoSk-p"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/altruist",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["altruistclient==0.1.1"],
|
||||
"zeroconf": ["_altruist._tcp.local."]
|
||||
}
|
83
homeassistant/components/altruist/quality_scale.yaml
Normal file
83
homeassistant/components/altruist/quality_scale.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
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: |
|
||||
This integration does not provide additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
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: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration
|
||||
entity-category: todo
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No known use cases for repair issues or flows, yet
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
249
homeassistant/components/altruist/sensor.py
Normal file
249
homeassistant/components/altruist/sensor.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""Defines the Altruist sensor platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AltruistConfigEntry
|
||||
from .coordinator import AltruistDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AltruistSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class to describe a Sensor entity."""
|
||||
|
||||
native_value_fn: Callable[[str], float] = float
|
||||
state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS = [
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
key="BME280_humidity",
|
||||
translation_key="humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "BME280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
key="BME280_pressure",
|
||||
translation_key="pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.PA,
|
||||
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||
suggested_display_precision=0,
|
||||
translation_placeholders={"sensor_name": "BME280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="BME280_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "BME280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
key="BMP_pressure",
|
||||
translation_key="pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.PA,
|
||||
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||
suggested_display_precision=0,
|
||||
translation_placeholders={"sensor_name": "BMP"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="BMP_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "BMP"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="BMP280_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "BMP280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
key="BMP280_pressure",
|
||||
translation_key="pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.PA,
|
||||
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||
suggested_display_precision=0,
|
||||
translation_placeholders={"sensor_name": "BMP280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
key="HTU21D_humidity",
|
||||
translation_key="humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "HTU21D"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="HTU21D_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "HTU21D"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
translation_key="pm_10",
|
||||
key="SDS_P1",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
translation_key="pm_25",
|
||||
key="SDS_P2",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
key="SHT3X_humidity",
|
||||
translation_key="humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "SHT3X"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="SHT3X_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "SHT3X"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
key="signal",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
key="PCBA_noiseMax",
|
||||
translation_key="noise_max",
|
||||
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
key="PCBA_noiseAvg",
|
||||
translation_key="noise_avg",
|
||||
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
translation_key="co2",
|
||||
key="CCS_CO2",
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "CCS"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
key="CCS_TVOC",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
key="GC",
|
||||
native_unit_of_measurement="μR/h",
|
||||
translation_key="radiation",
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
translation_key="co2",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
key="SCD4x_co2",
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "SCD4x"},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AltruistConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add sensors for passed config_entry in HA."""
|
||||
coordinator = config_entry.runtime_data
|
||||
async_add_entities(
|
||||
AltruistSensor(coordinator, sensor_description)
|
||||
for sensor_description in SENSOR_DESCRIPTIONS
|
||||
if sensor_description.key in coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AltruistSensor(CoordinatorEntity[AltruistDataUpdateCoordinator], SensorEntity):
|
||||
"""Implementation of a Altruist sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AltruistDataUpdateCoordinator,
|
||||
description: AltruistSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Altruist sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._device = coordinator.client.device
|
||||
self.entity_description: AltruistSensorEntityDescription = description
|
||||
self._attr_unique_id = f"{self._device.id}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, self._device.id)},
|
||||
manufacturer="Robonomics",
|
||||
model="Altruist",
|
||||
sw_version=self._device.fw_version,
|
||||
configuration_url=f"http://{self._device.ip_address}",
|
||||
serial_number=self._device.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available and self.entity_description.key in self.coordinator.data
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int:
|
||||
"""Return the native value of the sensor."""
|
||||
string_value = self.coordinator.data[self.entity_description.key]
|
||||
return self.entity_description.native_value_fn(string_value)
|
51
homeassistant/components/altruist/strings.json
Normal file
51
homeassistant/components/altruist/strings.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to start setup {model}?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Altruist IP address or hostname in the local network"
|
||||
},
|
||||
"description": "Fill in Altruist IP address or hostname in your local network"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"no_device_found": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"humidity": {
|
||||
"name": "{sensor_name} humidity"
|
||||
},
|
||||
"pressure": {
|
||||
"name": "{sensor_name} pressure"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "{sensor_name} temperature"
|
||||
},
|
||||
"noise_max": {
|
||||
"name": "Maximum noise"
|
||||
},
|
||||
"noise_avg": {
|
||||
"name": "Average noise"
|
||||
},
|
||||
"co2": {
|
||||
"name": "{sensor_name} CO2"
|
||||
},
|
||||
"radiation": {
|
||||
"name": "Radiation level"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,6 +11,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bosch-alarm-mode2==0.4.6"]
|
||||
}
|
||||
|
@@ -28,38 +28,41 @@ rules:
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
No options flow is provided.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
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: |
|
||||
Device type integration
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
|
@@ -13,7 +13,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["demetriek"],
|
||||
"requirements": ["demetriek==1.2.0"],
|
||||
"requirements": ["demetriek==1.3.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:LaMetric:1"
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["thinqconnect"],
|
||||
"requirements": ["thinqconnect==1.0.5"]
|
||||
"requirements": ["thinqconnect==1.0.7"]
|
||||
}
|
||||
|
@@ -2,25 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from aiolyric import Lyric
|
||||
from aiolyric.exceptions import LyricAuthenticationException, LyricException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import (
|
||||
ConfigEntryLyricClient,
|
||||
@@ -28,11 +19,10 @@ from .api import (
|
||||
OAuth2SessionLyric,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LyricDataUpdateCoordinator
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
|
||||
|
||||
|
||||
@@ -54,53 +44,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
client_id = implementation.client_id
|
||||
lyric = Lyric(client, client_id)
|
||||
|
||||
async def async_update_data(force_refresh_token: bool = False) -> Lyric:
|
||||
"""Fetch data from Lyric."""
|
||||
try:
|
||||
if not force_refresh_token:
|
||||
await oauth_session.async_ensure_token_valid()
|
||||
else:
|
||||
await oauth_session.force_refresh_token()
|
||||
except ClientResponseError as exception:
|
||||
if exception.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
||||
raise ConfigEntryAuthFailed from exception
|
||||
raise UpdateFailed(exception) from exception
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(60):
|
||||
await lyric.get_locations()
|
||||
await asyncio.gather(
|
||||
*(
|
||||
lyric.get_thermostat_rooms(
|
||||
location.location_id, device.device_id
|
||||
)
|
||||
for location in lyric.locations
|
||||
for device in location.devices
|
||||
if device.device_class == "Thermostat"
|
||||
and device.device_id.startswith("LCC")
|
||||
)
|
||||
)
|
||||
|
||||
except LyricAuthenticationException as exception:
|
||||
# Attempt to refresh the token before failing.
|
||||
# Honeywell appear to have issues keeping tokens saved.
|
||||
_LOGGER.debug("Authentication failed. Attempting to refresh token")
|
||||
if not force_refresh_token:
|
||||
return await async_update_data(force_refresh_token=True)
|
||||
raise ConfigEntryAuthFailed from exception
|
||||
except (LyricException, ClientResponseError) as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
return lyric
|
||||
|
||||
coordinator = DataUpdateCoordinator[Lyric](
|
||||
coordinator = LyricDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
# Name of the data. For logging purposes.
|
||||
name="lyric_coordinator",
|
||||
update_method=async_update_data,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=timedelta(seconds=300),
|
||||
oauth_session=oauth_session,
|
||||
lyric=lyric,
|
||||
)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
|
@@ -8,7 +8,6 @@ import logging
|
||||
from time import localtime, strftime, time
|
||||
from typing import Any
|
||||
|
||||
from aiolyric import Lyric
|
||||
from aiolyric.objects.device import LyricDevice
|
||||
from aiolyric.objects.location import LyricLocation
|
||||
import voluptuous as vol
|
||||
@@ -37,7 +36,6 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
@@ -48,6 +46,7 @@ from .const import (
|
||||
PRESET_TEMPORARY_HOLD,
|
||||
PRESET_VACATION_HOLD,
|
||||
)
|
||||
from .coordinator import LyricDataUpdateCoordinator
|
||||
from .entity import LyricDeviceEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -126,7 +125,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Honeywell Lyric climate platform based on a config entry."""
|
||||
coordinator: DataUpdateCoordinator[Lyric] = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: LyricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
@@ -164,7 +163,7 @@ class LyricThermostatType(enum.Enum):
|
||||
class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
"""Defines a Honeywell Lyric climate entity."""
|
||||
|
||||
coordinator: DataUpdateCoordinator[Lyric]
|
||||
coordinator: LyricDataUpdateCoordinator
|
||||
entity_description: ClimateEntityDescription
|
||||
|
||||
_attr_name = None
|
||||
@@ -178,7 +177,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[Lyric],
|
||||
coordinator: LyricDataUpdateCoordinator,
|
||||
description: ClimateEntityDescription,
|
||||
location: LyricLocation,
|
||||
device: LyricDevice,
|
||||
|
87
homeassistant/components/lyric/coordinator.py
Normal file
87
homeassistant/components/lyric/coordinator.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""The Honeywell Lyric integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from aiolyric import Lyric
|
||||
from aiolyric.exceptions import LyricAuthenticationException, LyricException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import OAuth2SessionLyric
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LyricDataUpdateCoordinator(DataUpdateCoordinator[Lyric]):
|
||||
"""Data update coordinator for Honeywell Lyric."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
oauth_session: OAuth2SessionLyric,
|
||||
lyric: Lyric,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name="lyric_coordinator",
|
||||
update_interval=timedelta(seconds=300),
|
||||
)
|
||||
self.oauth_session = oauth_session
|
||||
self.lyric = lyric
|
||||
|
||||
async def _async_update_data(self) -> Lyric:
|
||||
"""Fetch data from Lyric."""
|
||||
return await self._run_update(False)
|
||||
|
||||
async def _run_update(self, force_refresh_token: bool) -> Lyric:
|
||||
"""Fetch data from Lyric."""
|
||||
try:
|
||||
if not force_refresh_token:
|
||||
await self.oauth_session.async_ensure_token_valid()
|
||||
else:
|
||||
await self.oauth_session.force_refresh_token()
|
||||
except ClientResponseError as exception:
|
||||
if exception.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
||||
raise ConfigEntryAuthFailed from exception
|
||||
raise UpdateFailed(exception) from exception
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(60):
|
||||
await self.lyric.get_locations()
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.lyric.get_thermostat_rooms(
|
||||
location.location_id, device.device_id
|
||||
)
|
||||
for location in self.lyric.locations
|
||||
for device in location.devices
|
||||
if device.device_class == "Thermostat"
|
||||
and device.device_id.startswith("LCC")
|
||||
)
|
||||
)
|
||||
|
||||
except LyricAuthenticationException as exception:
|
||||
# Attempt to refresh the token before failing.
|
||||
# Honeywell appear to have issues keeping tokens saved.
|
||||
_LOGGER.debug("Authentication failed. Attempting to refresh token")
|
||||
if not force_refresh_token:
|
||||
return await self._run_update(True)
|
||||
raise ConfigEntryAuthFailed from exception
|
||||
except (LyricException, ClientResponseError) as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
return self.lyric
|
@@ -2,27 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiolyric import Lyric
|
||||
from aiolyric.objects.device import LyricDevice
|
||||
from aiolyric.objects.location import LyricLocation
|
||||
from aiolyric.objects.priority import LyricAccessory, LyricRoom
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import LyricDataUpdateCoordinator
|
||||
|
||||
|
||||
class LyricEntity(CoordinatorEntity[DataUpdateCoordinator[Lyric]]):
|
||||
class LyricEntity(CoordinatorEntity[LyricDataUpdateCoordinator]):
|
||||
"""Defines a base Honeywell Lyric entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[Lyric],
|
||||
coordinator: LyricDataUpdateCoordinator,
|
||||
location: LyricLocation,
|
||||
device: LyricDevice,
|
||||
key: str,
|
||||
@@ -71,7 +69,7 @@ class LyricAccessoryEntity(LyricDeviceEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[Lyric],
|
||||
coordinator: LyricDataUpdateCoordinator,
|
||||
location: LyricLocation,
|
||||
device: LyricDevice,
|
||||
room: LyricRoom,
|
||||
|
@@ -6,7 +6,6 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from aiolyric import Lyric
|
||||
from aiolyric.objects.device import LyricDevice
|
||||
from aiolyric.objects.location import LyricLocation
|
||||
from aiolyric.objects.priority import LyricAccessory, LyricRoom
|
||||
@@ -22,7 +21,6 @@ from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
@@ -33,6 +31,7 @@ from .const import (
|
||||
PRESET_TEMPORARY_HOLD,
|
||||
PRESET_VACATION_HOLD,
|
||||
)
|
||||
from .coordinator import LyricDataUpdateCoordinator
|
||||
from .entity import LyricAccessoryEntity, LyricDeviceEntity
|
||||
|
||||
LYRIC_SETPOINT_STATUS_NAMES = {
|
||||
@@ -164,7 +163,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Honeywell Lyric sensor platform based on a config entry."""
|
||||
coordinator: DataUpdateCoordinator[Lyric] = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: LyricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
LyricSensor(
|
||||
@@ -199,7 +198,7 @@ class LyricSensor(LyricDeviceEntity, SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[Lyric],
|
||||
coordinator: LyricDataUpdateCoordinator,
|
||||
description: LyricSensorEntityDescription,
|
||||
location: LyricLocation,
|
||||
device: LyricDevice,
|
||||
@@ -231,7 +230,7 @@ class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[Lyric],
|
||||
coordinator: LyricDataUpdateCoordinator,
|
||||
description: LyricSensorAccessoryEntityDescription,
|
||||
location: LyricLocation,
|
||||
parentDevice: LyricDevice,
|
||||
|
@@ -81,9 +81,21 @@
|
||||
"valve_position": {
|
||||
"default": "mdi:valve"
|
||||
},
|
||||
"battery_charge_state": {
|
||||
"default": "mdi:battery-charging"
|
||||
},
|
||||
"battery_replacement_description": {
|
||||
"default": "mdi:battery-sync-outline"
|
||||
},
|
||||
"battery_time_remaining": {
|
||||
"default": "mdi:battery-clock-outline"
|
||||
},
|
||||
"battery_time_to_full_charge": {
|
||||
"default": "mdi:battery-clock"
|
||||
},
|
||||
"esa_opt_out_state": {
|
||||
"default": "mdi:home-lightning-bolt"
|
||||
},
|
||||
"evse_state": {
|
||||
"default": "mdi:ev-station"
|
||||
},
|
||||
|
@@ -38,6 +38,7 @@ from homeassistant.const import (
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
@@ -84,6 +85,21 @@ BOOST_STATE_MAP = {
|
||||
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kUnknownEnumValue: None,
|
||||
}
|
||||
|
||||
CHARGE_STATE_MAP = {
|
||||
clusters.PowerSource.Enums.BatChargeStateEnum.kUnknown: None,
|
||||
clusters.PowerSource.Enums.BatChargeStateEnum.kIsNotCharging: "not_charging",
|
||||
clusters.PowerSource.Enums.BatChargeStateEnum.kIsCharging: "charging",
|
||||
clusters.PowerSource.Enums.BatChargeStateEnum.kIsAtFullCharge: "full_charge",
|
||||
clusters.PowerSource.Enums.BatChargeStateEnum.kUnknownEnumValue: None,
|
||||
}
|
||||
|
||||
DEM_OPT_OUT_STATE_MAP = {
|
||||
clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut: "no_opt_out",
|
||||
clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kLocalOptOut: "local_opt_out",
|
||||
clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kGridOptOut: "grid_opt_out",
|
||||
clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kOptOut: "opt_out",
|
||||
}
|
||||
|
||||
ESA_STATE_MAP = {
|
||||
clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOffline: "offline",
|
||||
clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline: "online",
|
||||
@@ -355,6 +371,47 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.PowerSource.Attributes.BatVoltage,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="PowerSourceBatTimeRemaining",
|
||||
translation_key="battery_time_remaining",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.PowerSource.Attributes.BatTimeRemaining,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="PowerSourceBatChargeState",
|
||||
translation_key="battery_charge_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[state for state in CHARGE_STATE_MAP.values() if state is not None],
|
||||
measurement_to_ha=CHARGE_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.PowerSource.Attributes.BatChargeState,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="PowerSourceBatTimeToFullCharge",
|
||||
translation_key="battery_time_to_full_charge",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.PowerSource.Attributes.BatTimeToFullCharge,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
@@ -1109,6 +1166,19 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DeviceEnergyManagement.Attributes.ESAState,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="ESAOptOutState",
|
||||
translation_key="esa_opt_out_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(DEM_OPT_OUT_STATE_MAP.values()),
|
||||
measurement_to_ha=DEM_OPT_OUT_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DeviceEnergyManagement.Attributes.OptOutState,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
|
@@ -330,6 +330,20 @@
|
||||
"battery_replacement_description": {
|
||||
"name": "Battery type"
|
||||
},
|
||||
"battery_charge_state": {
|
||||
"name": "Battery charge state",
|
||||
"state": {
|
||||
"charging": "[%key:common::state::charging%]",
|
||||
"full_charge": "Full charge",
|
||||
"not_charging": "Not charging"
|
||||
}
|
||||
},
|
||||
"battery_time_remaining": {
|
||||
"name": "Time remaining"
|
||||
},
|
||||
"battery_time_to_full_charge": {
|
||||
"name": "Time to full charge"
|
||||
},
|
||||
"battery_voltage": {
|
||||
"name": "Battery voltage"
|
||||
},
|
||||
@@ -349,6 +363,15 @@
|
||||
"paused": "[%key:common::state::paused%]"
|
||||
}
|
||||
},
|
||||
"esa_opt_out_state": {
|
||||
"name": "Energy optimization opt-out",
|
||||
"state": {
|
||||
"no_opt_out": "[%key:common::state::off%]",
|
||||
"local_opt_out": "Local",
|
||||
"grid_opt_out": "Grid",
|
||||
"opt_out": "Local and grid"
|
||||
}
|
||||
},
|
||||
"evse_fault_state": {
|
||||
"name": "Fault state",
|
||||
"state": {
|
||||
|
@@ -835,6 +835,15 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
except InvalidAuthError:
|
||||
self.config_entry.async_start_reauth(self.hass)
|
||||
return
|
||||
except RpcCallError as err:
|
||||
# Ignore 404 (No handler for) error
|
||||
if err.code != 404:
|
||||
LOGGER.debug(
|
||||
"Error during shutdown for device %s: %s",
|
||||
self.name,
|
||||
err.message,
|
||||
)
|
||||
return
|
||||
except DeviceConnectionError as err:
|
||||
# If the device is restarting or has gone offline before
|
||||
# the ping/pong timeout happens, the shutdown command
|
||||
|
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
|
||||
AlarmControlPanelEntity,
|
||||
@@ -42,6 +43,7 @@ from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_OBJECT_ID, DOMAIN
|
||||
from .coordinator import TriggerUpdateCoordinator
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
@@ -49,6 +51,7 @@ from .template_entity import (
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_VALID_STATES = [
|
||||
@@ -253,6 +256,13 @@ async def async_setup_platform(
|
||||
)
|
||||
return
|
||||
|
||||
if "coordinator" in discovery_info:
|
||||
async_add_entities(
|
||||
TriggerAlarmControlPanelEntity(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
)
|
||||
return
|
||||
|
||||
_async_create_template_tracking_entities(
|
||||
async_add_entities,
|
||||
hass,
|
||||
@@ -276,8 +286,11 @@ class AbstractTemplateAlarmControlPanel(
|
||||
self._attr_code_format = config[CONF_CODE_FORMAT].value
|
||||
|
||||
self._state: AlarmControlPanelState | None = None
|
||||
self._attr_supported_features: AlarmControlPanelEntityFeature = (
|
||||
AlarmControlPanelEntityFeature(0)
|
||||
)
|
||||
|
||||
def _register_scripts(
|
||||
def _iterate_scripts(
|
||||
self, config: dict[str, Any]
|
||||
) -> Generator[
|
||||
tuple[str, Sequence[dict[str, Any]], AlarmControlPanelEntityFeature | int]
|
||||
@@ -423,8 +436,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AbstractTemplateAlarmControlPane
|
||||
if TYPE_CHECKING:
|
||||
assert name is not None
|
||||
|
||||
self._attr_supported_features = AlarmControlPanelEntityFeature(0)
|
||||
for action_id, action_config, supported_feature in self._register_scripts(
|
||||
for action_id, action_config, supported_feature in self._iterate_scripts(
|
||||
config
|
||||
):
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
@@ -456,3 +468,55 @@ class AlarmControlPanelTemplate(TemplateEntity, AbstractTemplateAlarmControlPane
|
||||
"_state", self._template, None, self._update_state
|
||||
)
|
||||
super()._async_setup_templates()
|
||||
|
||||
|
||||
class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControlPanel):
|
||||
"""Alarm Control Panel entity based on trigger data."""
|
||||
|
||||
domain = ALARM_CONTROL_PANEL_DOMAIN
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: TriggerUpdateCoordinator,
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateAlarmControlPanel.__init__(self, config)
|
||||
|
||||
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||
|
||||
if isinstance(config.get(CONF_STATE), template.Template):
|
||||
self._to_render_simple.append(CONF_STATE)
|
||||
self._parse_result.add(CONF_STATE)
|
||||
|
||||
for action_id, action_config, supported_feature in self._iterate_scripts(
|
||||
config
|
||||
):
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
self._attr_device_info = async_device_info_to_link_from_device_id(
|
||||
hass,
|
||||
config.get(CONF_DEVICE_ID),
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
await self._async_handle_restored_state()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle update of the data."""
|
||||
self._process_data()
|
||||
|
||||
if not self.available:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if (rendered := self._rendered.get(CONF_STATE)) is not None:
|
||||
self._handle_state(rendered)
|
||||
self.async_set_context(self.coordinator.data["context"])
|
||||
self.async_write_ha_state()
|
||||
|
@@ -157,10 +157,8 @@ CONFIG_SECTION_SCHEMA = vol.All(
|
||||
},
|
||||
),
|
||||
ensure_domains_do_not_have_trigger_or_action(
|
||||
DOMAIN_ALARM_CONTROL_PANEL,
|
||||
DOMAIN_BUTTON,
|
||||
DOMAIN_FAN,
|
||||
DOMAIN_LOCK,
|
||||
DOMAIN_VACUUM,
|
||||
),
|
||||
)
|
||||
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lock import (
|
||||
DOMAIN as LOCK_DOMAIN,
|
||||
PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA,
|
||||
LockEntity,
|
||||
LockEntityFeature,
|
||||
@@ -23,11 +24,12 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError, TemplateError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_PICTURE, DOMAIN
|
||||
from .coordinator import TriggerUpdateCoordinator
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
@@ -36,6 +38,7 @@ from .template_entity import (
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
CONF_CODE_FORMAT_TEMPLATE = "code_format_template"
|
||||
CONF_CODE_FORMAT = "code_format"
|
||||
@@ -123,6 +126,13 @@ async def async_setup_platform(
|
||||
)
|
||||
return
|
||||
|
||||
if "coordinator" in discovery_info:
|
||||
async_add_entities(
|
||||
TriggerLockEntity(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
)
|
||||
return
|
||||
|
||||
_async_create_template_tracking_entities(
|
||||
async_add_entities,
|
||||
hass,
|
||||
@@ -147,7 +157,7 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
|
||||
self._optimistic = config.get(CONF_OPTIMISTIC)
|
||||
self._attr_assumed_state = bool(self._optimistic)
|
||||
|
||||
def _register_scripts(
|
||||
def _iterate_scripts(
|
||||
self, config: dict[str, Any]
|
||||
) -> Generator[tuple[str, Sequence[dict[str, Any]], LockEntityFeature | int]]:
|
||||
for action_id, supported_feature in (
|
||||
@@ -314,7 +324,7 @@ class TemplateLock(TemplateEntity, AbstractTemplateLock):
|
||||
if TYPE_CHECKING:
|
||||
assert name is not None
|
||||
|
||||
for action_id, action_config, supported_feature in self._register_scripts(
|
||||
for action_id, action_config, supported_feature in self._iterate_scripts(
|
||||
config
|
||||
):
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
@@ -346,3 +356,60 @@ class TemplateLock(TemplateEntity, AbstractTemplateLock):
|
||||
self._update_code_format,
|
||||
)
|
||||
super()._async_setup_templates()
|
||||
|
||||
|
||||
class TriggerLockEntity(TriggerEntity, AbstractTemplateLock):
|
||||
"""Lock entity based on trigger data."""
|
||||
|
||||
domain = LOCK_DOMAIN
|
||||
extra_template_keys = (CONF_STATE,)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: TriggerUpdateCoordinator,
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateLock.__init__(self, config)
|
||||
|
||||
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||
|
||||
if isinstance(config.get(CONF_CODE_FORMAT), template.Template):
|
||||
self._to_render_simple.append(CONF_CODE_FORMAT)
|
||||
self._parse_result.add(CONF_CODE_FORMAT)
|
||||
|
||||
for action_id, action_config, supported_feature in self._iterate_scripts(
|
||||
config
|
||||
):
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle update of the data."""
|
||||
self._process_data()
|
||||
|
||||
if not self.available:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
write_ha_state = False
|
||||
for key, updater in (
|
||||
(CONF_STATE, self._handle_state),
|
||||
(CONF_CODE_FORMAT, self._update_code_format),
|
||||
):
|
||||
if (rendered := self._rendered.get(key)) is not None:
|
||||
updater(rendered)
|
||||
write_ha_state = True
|
||||
|
||||
if not self._optimistic:
|
||||
self.async_set_context(self.coordinator.data["context"])
|
||||
write_ha_state = True
|
||||
elif self._optimistic and len(self._rendered) > 0:
|
||||
# In case any non optimistic template
|
||||
write_ha_state = True
|
||||
|
||||
if write_ha_state:
|
||||
self.async_write_ha_state()
|
||||
|
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -27,6 +27,7 @@ from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_SENSORS,
|
||||
CONF_UNIQUE_ID,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
@@ -89,6 +90,7 @@ SENSOR_SCHEMA = vol.All(
|
||||
vol.Optional(CONF_MIN_GRADIENT, default=0.0): vol.Coerce(float),
|
||||
vol.Optional(CONF_SAMPLE_DURATION, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_SAMPLES, default=2): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
),
|
||||
_validate_min_max,
|
||||
@@ -121,6 +123,7 @@ async def async_setup_platform(
|
||||
min_samples=sensor_config[CONF_MIN_SAMPLES],
|
||||
max_samples=sensor_config[CONF_MAX_SAMPLES],
|
||||
device_class=sensor_config.get(CONF_DEVICE_CLASS),
|
||||
unique_id=sensor_config.get(CONF_UNIQUE_ID),
|
||||
sensor_entity_id=generate_entity_id(
|
||||
ENTITY_ID_FORMAT, sensor_name, hass=hass
|
||||
),
|
||||
|
2
homeassistant/generated/config_flows.py
generated
2
homeassistant/generated/config_flows.py
generated
@@ -48,6 +48,7 @@ FLOWS = {
|
||||
"airzone_cloud",
|
||||
"alarmdecoder",
|
||||
"alexa_devices",
|
||||
"altruist",
|
||||
"amberelectric",
|
||||
"ambient_network",
|
||||
"ambient_station",
|
||||
@@ -647,6 +648,7 @@ FLOWS = {
|
||||
"tibber",
|
||||
"tile",
|
||||
"tilt_ble",
|
||||
"tilt_pi",
|
||||
"time_date",
|
||||
"todoist",
|
||||
"tolo",
|
||||
|
@@ -204,6 +204,12 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"altruist": {
|
||||
"name": "Altruist",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"amazon": {
|
||||
"name": "Amazon",
|
||||
"integrations": {
|
||||
@@ -6426,7 +6432,10 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "SwitchBot Cloud"
|
||||
}
|
||||
}
|
||||
},
|
||||
"iot_standards": [
|
||||
"matter"
|
||||
]
|
||||
},
|
||||
"switcher_kis": {
|
||||
"name": "Switcher",
|
||||
@@ -6742,11 +6751,22 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"tilt_ble": {
|
||||
"name": "Tilt Hydrometer BLE",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
"tilt": {
|
||||
"name": "Tilt",
|
||||
"integrations": {
|
||||
"tilt_ble": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"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": {
|
||||
"integration_type": "service",
|
||||
|
5
homeassistant/generated/zeroconf.py
generated
5
homeassistant/generated/zeroconf.py
generated
@@ -342,6 +342,11 @@ ZEROCONF = {
|
||||
"domain": "apple_tv",
|
||||
},
|
||||
],
|
||||
"_altruist._tcp.local.": [
|
||||
{
|
||||
"domain": "altruist",
|
||||
},
|
||||
],
|
||||
"_amzn-alexa._tcp.local.": [
|
||||
{
|
||||
"domain": "roomba",
|
||||
|
10
mypy.ini
generated
10
mypy.ini
generated
@@ -425,6 +425,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.altruist.*]
|
||||
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.amazon_polly.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
10
requirements_all.txt
generated
10
requirements_all.txt
generated
@@ -461,6 +461,9 @@ airtouch5py==0.3.0
|
||||
# homeassistant.components.alpha_vantage
|
||||
alpha-vantage==2.3.1
|
||||
|
||||
# homeassistant.components.altruist
|
||||
altruistclient==0.1.1
|
||||
|
||||
# homeassistant.components.amberelectric
|
||||
amberelectric==2.0.12
|
||||
|
||||
@@ -776,7 +779,7 @@ defusedxml==0.7.1
|
||||
deluge-client==1.10.2
|
||||
|
||||
# homeassistant.components.lametric
|
||||
demetriek==1.2.0
|
||||
demetriek==1.3.0
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==1.1.1
|
||||
@@ -2928,7 +2931,7 @@ thermopro-ble==0.13.0
|
||||
thingspeak==1.0.0
|
||||
|
||||
# homeassistant.components.lg_thinq
|
||||
thinqconnect==1.0.5
|
||||
thinqconnect==1.0.7
|
||||
|
||||
# homeassistant.components.tikteck
|
||||
tikteck==0.4
|
||||
@@ -2936,6 +2939,9 @@ tikteck==0.4
|
||||
# homeassistant.components.tilt_ble
|
||||
tilt-ble==0.2.3
|
||||
|
||||
# homeassistant.components.tilt_pi
|
||||
tilt-pi==0.2.1
|
||||
|
||||
# homeassistant.components.tmb
|
||||
tmb==0.0.4
|
||||
|
||||
|
10
requirements_test_all.txt
generated
10
requirements_test_all.txt
generated
@@ -440,6 +440,9 @@ airtouch4pyapi==1.0.5
|
||||
# homeassistant.components.airtouch5
|
||||
airtouch5py==0.3.0
|
||||
|
||||
# homeassistant.components.altruist
|
||||
altruistclient==0.1.1
|
||||
|
||||
# homeassistant.components.amberelectric
|
||||
amberelectric==2.0.12
|
||||
|
||||
@@ -676,7 +679,7 @@ defusedxml==0.7.1
|
||||
deluge-client==1.10.2
|
||||
|
||||
# homeassistant.components.lametric
|
||||
demetriek==1.2.0
|
||||
demetriek==1.3.0
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==1.1.1
|
||||
@@ -2408,11 +2411,14 @@ thermobeacon-ble==0.10.0
|
||||
thermopro-ble==0.13.0
|
||||
|
||||
# homeassistant.components.lg_thinq
|
||||
thinqconnect==1.0.5
|
||||
thinqconnect==1.0.7
|
||||
|
||||
# homeassistant.components.tilt_ble
|
||||
tilt-ble==0.2.3
|
||||
|
||||
# homeassistant.components.tilt_pi
|
||||
tilt-pi==0.2.1
|
||||
|
||||
# homeassistant.components.todoist
|
||||
todoist-api-python==2.1.7
|
||||
|
||||
|
@@ -8,6 +8,7 @@ from homeassistant.components.ai_task import AITaskEntityFeature, async_generate
|
||||
from homeassistant.components.conversation import async_get_chat_log
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import chat_session
|
||||
|
||||
from .conftest import TEST_ENTITY_ID, MockAITaskEntity
|
||||
@@ -25,7 +26,7 @@ async def test_run_task_preferred_entity(
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="No entity_id provided and no preferred entity set"
|
||||
HomeAssistantError, match="No entity_id provided and no preferred entity set"
|
||||
):
|
||||
await async_generate_text(
|
||||
hass,
|
||||
@@ -42,7 +43,9 @@ async def test_run_task_preferred_entity(
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
with pytest.raises(ValueError, match="AI Task entity ai_task.unknown not found"):
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match="AI Task entity ai_task.unknown not found"
|
||||
):
|
||||
await async_generate_text(
|
||||
hass,
|
||||
task_name="Test Task",
|
||||
@@ -74,7 +77,7 @@ async def test_run_task_preferred_entity(
|
||||
|
||||
mock_ai_task_entity.supported_features = AITaskEntityFeature(0)
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
HomeAssistantError,
|
||||
match="AI Task entity ai_task.test_task_entity does not support generating text",
|
||||
):
|
||||
await async_generate_text(
|
||||
@@ -91,7 +94,7 @@ async def test_run_text_task_unknown_entity(
|
||||
"""Test running a text task with an unknown entity."""
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="AI Task entity ai_task.unknown_entity not found"
|
||||
HomeAssistantError, match="AI Task entity ai_task.unknown_entity not found"
|
||||
):
|
||||
await async_generate_text(
|
||||
hass,
|
||||
|
13
tests/components/altruist/__init__.py
Normal file
13
tests/components/altruist/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Tests for the Altruist 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 component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
82
tests/components/altruist/conftest.py
Normal file
82
tests/components/altruist/conftest.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Altruist tests configuration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
import json
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from altruistclient import AltruistDeviceModel, AltruistError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.altruist.const import CONF_HOST, DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.altruist.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,
|
||||
data={CONF_HOST: "192.168.1.100"},
|
||||
unique_id="5366960e8b18",
|
||||
title="5366960e8b18",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_altruist_device() -> Mock:
|
||||
"""Return a mock AltruistDeviceModel."""
|
||||
device = Mock(spec=AltruistDeviceModel)
|
||||
device.id = "5366960e8b18"
|
||||
device.name = "Altruist Sensor"
|
||||
device.ip_address = "192.168.1.100"
|
||||
device.fw_version = "R_2025-03"
|
||||
return device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_altruist_client(mock_altruist_device: Mock) -> Generator[AsyncMock]:
|
||||
"""Return a mock AltruistClient."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.altruist.coordinator.AltruistClient",
|
||||
autospec=True,
|
||||
) as mock_client_class,
|
||||
patch(
|
||||
"homeassistant.components.altruist.config_flow.AltruistClient",
|
||||
new=mock_client_class,
|
||||
),
|
||||
):
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.device = mock_altruist_device
|
||||
mock_instance.device_id = mock_altruist_device.id
|
||||
mock_instance.sensor_names = json.loads(
|
||||
load_fixture("sensor_names.json", DOMAIN)
|
||||
)
|
||||
mock_instance.fetch_data.return_value = json.loads(
|
||||
load_fixture("real_data.json", DOMAIN)
|
||||
)
|
||||
|
||||
mock_client_class.from_ip_address = AsyncMock(return_value=mock_instance)
|
||||
|
||||
yield mock_instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_altruist_client_fails_once(mock_altruist_client: AsyncMock) -> Generator[None]:
|
||||
"""Patch AltruistClient to fail once and then succeed."""
|
||||
with patch(
|
||||
"homeassistant.components.altruist.config_flow.AltruistClient.from_ip_address",
|
||||
side_effect=[AltruistError("Connection failed"), mock_altruist_client],
|
||||
):
|
||||
yield
|
38
tests/components/altruist/fixtures/real_data.json
Normal file
38
tests/components/altruist/fixtures/real_data.json
Normal file
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"value_type": "signal",
|
||||
"value": "-48"
|
||||
},
|
||||
{
|
||||
"value_type": "SDS_P1",
|
||||
"value": "0.1"
|
||||
},
|
||||
{
|
||||
"value_type": "SDS_P2",
|
||||
"value": "0.23"
|
||||
},
|
||||
{
|
||||
"value_type": "BME280_humidity",
|
||||
"value": "54.94141"
|
||||
},
|
||||
{
|
||||
"value_type": "BME280_temperature",
|
||||
"value": "22.95313"
|
||||
},
|
||||
{
|
||||
"value_type": "BME280_pressure",
|
||||
"value": "99978.16"
|
||||
},
|
||||
{
|
||||
"value_type": "PCBA_noiseMax",
|
||||
"value": "60"
|
||||
},
|
||||
{
|
||||
"value_type": "PCBA_noiseAvg",
|
||||
"value": "51"
|
||||
},
|
||||
{
|
||||
"value_type": "GC",
|
||||
"value": "15.2"
|
||||
}
|
||||
]
|
11
tests/components/altruist/fixtures/sensor_names.json
Normal file
11
tests/components/altruist/fixtures/sensor_names.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
"signal",
|
||||
"SDS_P1",
|
||||
"SDS_P2",
|
||||
"BME280_humidity",
|
||||
"BME280_temperature",
|
||||
"BME280_pressure",
|
||||
"PCBA_noiseMax",
|
||||
"PCBA_noiseAvg",
|
||||
"GC"
|
||||
]
|
507
tests/components/altruist/snapshots/test_sensor.ambr
Normal file
507
tests/components/altruist/snapshots/test_sensor.ambr
Normal file
@@ -0,0 +1,507 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[sensor.5366960e8b18_average_noise-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.5366960e8b18_average_noise',
|
||||
'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.SOUND_PRESSURE: 'sound_pressure'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Average noise',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'noise_avg',
|
||||
'unique_id': '5366960e8b18-PCBA_noiseAvg',
|
||||
'unit_of_measurement': <UnitOfSoundPressure.DECIBEL: 'dB'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_average_noise-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'sound_pressure',
|
||||
'friendly_name': '5366960e8b18 Average noise',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSoundPressure.DECIBEL: 'dB'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_average_noise',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '51.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_bme280_humidity-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.5366960e8b18_bme280_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'BME280 humidity',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'humidity',
|
||||
'unique_id': '5366960e8b18-BME280_humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_bme280_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': '5366960e8b18 BME280 humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_bme280_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '54.94141',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_bme280_pressure-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.5366960e8b18_bme280_pressure',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfPressure.MMHG: 'mmHg'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'BME280 pressure',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'pressure',
|
||||
'unique_id': '5366960e8b18-BME280_pressure',
|
||||
'unit_of_measurement': <UnitOfPressure.MMHG: 'mmHg'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_bme280_pressure-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pressure',
|
||||
'friendly_name': '5366960e8b18 BME280 pressure',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPressure.MMHG: 'mmHg'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_bme280_pressure',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '749.897762397492',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_bme280_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.5366960e8b18_bme280_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'BME280 temperature',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature',
|
||||
'unique_id': '5366960e8b18-BME280_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_bme280_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': '5366960e8b18 BME280 temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_bme280_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '22.95313',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_maximum_noise-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.5366960e8b18_maximum_noise',
|
||||
'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.SOUND_PRESSURE: 'sound_pressure'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Maximum noise',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'noise_max',
|
||||
'unique_id': '5366960e8b18-PCBA_noiseMax',
|
||||
'unit_of_measurement': <UnitOfSoundPressure.DECIBEL: 'dB'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_maximum_noise-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'sound_pressure',
|
||||
'friendly_name': '5366960e8b18 Maximum noise',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSoundPressure.DECIBEL: 'dB'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_maximum_noise',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '60.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_pm10-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.5366960e8b18_pm10',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PM10: 'pm10'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM10',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'pm_10',
|
||||
'unique_id': '5366960e8b18-SDS_P1',
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_pm10-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pm10',
|
||||
'friendly_name': '5366960e8b18 PM10',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_pm10',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.1',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_pm2_5-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.5366960e8b18_pm2_5',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PM25: 'pm25'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM2.5',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'pm_25',
|
||||
'unique_id': '5366960e8b18-SDS_P2',
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_pm2_5-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pm25',
|
||||
'friendly_name': '5366960e8b18 PM2.5',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_pm2_5',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.23',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_radiation_level-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.5366960e8b18_radiation_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Radiation level',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'radiation',
|
||||
'unique_id': '5366960e8b18-GC',
|
||||
'unit_of_measurement': 'μR/h',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_radiation_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': '5366960e8b18 Radiation level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μR/h',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_radiation_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '15.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_signal_strength-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.5366960e8b18_signal_strength',
|
||||
'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.SIGNAL_STRENGTH: 'signal_strength'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Signal strength',
|
||||
'platform': 'altruist',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '5366960e8b18-signal',
|
||||
'unit_of_measurement': 'dBm',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.5366960e8b18_signal_strength-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'signal_strength',
|
||||
'friendly_name': '5366960e8b18 Signal strength',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'dBm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.5366960e8b18_signal_strength',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-48.0',
|
||||
})
|
||||
# ---
|
169
tests/components/altruist/test_config_flow.py
Normal file
169
tests/components/altruist/test_config_flow.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""Test the Altruist config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.components.altruist.const import CONF_HOST, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.100"),
|
||||
ip_addresses=[ip_address("192.168.1.100")],
|
||||
hostname="altruist-purple.local.",
|
||||
name="altruist-purple._altruist._tcp.local.",
|
||||
port=80,
|
||||
type="_altruist._tcp.local.",
|
||||
properties={
|
||||
"PATH": "/config",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_form_user_step_success(
|
||||
hass: HomeAssistant,
|
||||
mock_altruist_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test user step shows form and succeeds with valid input."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "192.168.1.100"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "5366960e8b18"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "192.168.1.100",
|
||||
}
|
||||
assert result["result"].unique_id == "5366960e8b18"
|
||||
|
||||
|
||||
async def test_form_user_step_cannot_connect_then_recovers(
|
||||
hass: HomeAssistant,
|
||||
mock_altruist_client: AsyncMock,
|
||||
mock_altruist_client_fails_once: None,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we handle connection error and allow recovery."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
# First attempt triggers an error
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "192.168.1.100"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_device_found"}
|
||||
|
||||
# Second attempt recovers with a valid client
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "192.168.1.100"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "5366960e8b18"
|
||||
assert result["result"].unique_id == "5366960e8b18"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "192.168.1.100",
|
||||
}
|
||||
|
||||
|
||||
async def test_form_user_step_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_altruist_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we abort if already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "192.168.1.100"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_discovery(
|
||||
hass: HomeAssistant,
|
||||
mock_altruist_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test zeroconf discovery."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "5366960e8b18"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "192.168.1.100",
|
||||
}
|
||||
assert result["result"].unique_id == "5366960e8b18"
|
||||
|
||||
|
||||
async def test_zeroconf_discovery_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_altruist_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test zeroconf discovery when already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_discovery_cant_create_client(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_altruist_client_fails_once: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test zeroconf discovery when already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_device_found"
|
53
tests/components/altruist/test_init.py
Normal file
53
tests/components/altruist/test_init.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Test the Altruist integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_entry_client_creation_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_altruist_client_fails_once: None,
|
||||
) -> None:
|
||||
"""Test setup failure when client creation fails."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_entry_fetch_data_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_altruist_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setup failure when initial data fetch fails."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_altruist_client.fetch_data.side_effect = Exception("Fetch failed")
|
||||
|
||||
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_altruist_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test unloading of config entry."""
|
||||
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 mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Now test unloading
|
||||
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
55
tests/components/altruist/test_sensor.py
Normal file
55
tests/components/altruist/test_sensor.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Tests for the Altruist integration sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from altruistclient import AltruistError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_altruist_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.altruist.PLATFORMS", [Platform.SENSOR]):
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_altruist_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test coordinator error handling during update."""
|
||||
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()
|
||||
|
||||
mock_altruist_client.fetch_data.side_effect = AltruistError()
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.5366960e8b18_bme280_temperature").state
|
||||
== STATE_UNAVAILABLE
|
||||
)
|
@@ -46,6 +46,7 @@
|
||||
'name': '**REDACTED**',
|
||||
'os_version': '2.2.2',
|
||||
'serial_number': '**REDACTED**',
|
||||
'update': None,
|
||||
'wifi': dict({
|
||||
'active': True,
|
||||
'available': True,
|
||||
|
@@ -76,6 +76,7 @@ async def integration_fixture(
|
||||
params=[
|
||||
"air_purifier",
|
||||
"air_quality_sensor",
|
||||
"battery_storage",
|
||||
"color_temperature_light",
|
||||
"cooktop",
|
||||
"dimmable_light",
|
||||
|
271
tests/components/matter/fixtures/nodes/battery_storage.json
Normal file
271
tests/components/matter/fixtures/nodes/battery_storage.json
Normal file
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"node_id": 25,
|
||||
"date_commissioned": "2025-06-19T17:13:40.727316",
|
||||
"last_interview": "2025-06-19T17:13:40.727333",
|
||||
"interview_version": 6,
|
||||
"available": true,
|
||||
"is_bridge": false,
|
||||
"attributes": {
|
||||
"0/29/0": [
|
||||
{
|
||||
"0": 22,
|
||||
"1": 3
|
||||
},
|
||||
{
|
||||
"0": 18,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"0/29/1": [29, 31, 40, 48, 49, 51, 60, 62, 63, 42],
|
||||
"0/29/2": [41],
|
||||
"0/29/3": [1],
|
||||
"0/29/65532": 0,
|
||||
"0/29/65533": 3,
|
||||
"0/29/65528": [],
|
||||
"0/29/65529": [],
|
||||
"0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/31/0": [
|
||||
{
|
||||
"1": 5,
|
||||
"2": 2,
|
||||
"3": [112233],
|
||||
"4": null,
|
||||
"254": 3
|
||||
}
|
||||
],
|
||||
"0/31/2": 4,
|
||||
"0/31/4": 4,
|
||||
"0/31/3": 3,
|
||||
"0/31/65532": 0,
|
||||
"0/31/65533": 2,
|
||||
"0/31/65528": [],
|
||||
"0/31/65529": [],
|
||||
"0/31/65531": [0, 2, 4, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/40/65532": 0,
|
||||
"0/40/0": 19,
|
||||
"0/40/6": "**REDACTED**",
|
||||
"0/40/1": "TEST_VENDOR",
|
||||
"0/40/2": 65521,
|
||||
"0/40/3": "Mock Battery Storage",
|
||||
"0/40/4": 32768,
|
||||
"0/40/7": 0,
|
||||
"0/40/8": "TEST_VERSION",
|
||||
"0/40/9": 1,
|
||||
"0/40/10": "1.0",
|
||||
"0/40/18": "6C89C9D11F0BDAAD",
|
||||
"0/40/19": {
|
||||
"0": 3,
|
||||
"1": 3
|
||||
},
|
||||
"0/40/21": 17104896,
|
||||
"0/40/22": 1,
|
||||
"0/40/65533": 5,
|
||||
"0/40/5": "",
|
||||
"0/40/65528": [],
|
||||
"0/40/65529": [],
|
||||
"0/40/65531": [
|
||||
65532, 0, 6, 1, 2, 3, 4, 7, 8, 9, 10, 18, 19, 21, 22, 65533, 5, 65528,
|
||||
65529, 65531
|
||||
],
|
||||
"0/48/65532": 0,
|
||||
"0/48/2": 0,
|
||||
"0/48/3": 0,
|
||||
"0/48/1": {
|
||||
"0": 60,
|
||||
"1": 900
|
||||
},
|
||||
"0/48/4": true,
|
||||
"0/48/65533": 2,
|
||||
"0/48/0": 0,
|
||||
"0/48/65528": [1, 3, 5],
|
||||
"0/48/65529": [0, 2, 4],
|
||||
"0/48/65531": [65532, 2, 3, 1, 4, 65533, 0, 65528, 65529, 65531],
|
||||
"0/49/0": 1,
|
||||
"0/49/1": [
|
||||
{
|
||||
"0": "RnJlZWJveC03Mjg2ODE=",
|
||||
"1": true
|
||||
}
|
||||
],
|
||||
"0/49/4": true,
|
||||
"0/49/5": 0,
|
||||
"0/49/6": "RnJlZWJveC03Mjg2ODE=",
|
||||
"0/49/7": null,
|
||||
"0/49/2": 10,
|
||||
"0/49/3": 30,
|
||||
"0/49/8": [0],
|
||||
"0/49/65532": 1,
|
||||
"0/49/65533": 2,
|
||||
"0/49/65528": [1, 5, 7],
|
||||
"0/49/65529": [0, 2, 4, 6, 8],
|
||||
"0/49/65531": [
|
||||
0, 1, 4, 5, 6, 7, 2, 3, 8, 65532, 65533, 65528, 65529, 65531
|
||||
],
|
||||
"0/51/0": [
|
||||
{
|
||||
"0": "WIFI_STA_DEF",
|
||||
"1": true,
|
||||
"2": null,
|
||||
"3": null,
|
||||
"4": "YFX59wI0",
|
||||
"5": ["wKgBqA=="],
|
||||
"6": ["/oAAAAAAAABiVfn//vcCNA==", "KgEOCgKzOZBiVfn//vcCNA=="],
|
||||
"7": 1
|
||||
}
|
||||
],
|
||||
"0/51/1": 1,
|
||||
"0/51/2": 245,
|
||||
"0/51/8": false,
|
||||
"0/51/65532": 0,
|
||||
"0/51/65533": 2,
|
||||
"0/51/65528": [2],
|
||||
"0/51/65529": [0, 1],
|
||||
"0/51/65531": [0, 1, 2, 8, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/60/65532": 0,
|
||||
"0/60/0": 0,
|
||||
"0/60/1": null,
|
||||
"0/60/2": null,
|
||||
"0/60/65533": 1,
|
||||
"0/60/65528": [],
|
||||
"0/60/65529": [0, 2],
|
||||
"0/60/65531": [65532, 0, 1, 2, 65533, 65528, 65529, 65531],
|
||||
"0/62/65532": 0,
|
||||
"0/62/0": [
|
||||
{
|
||||
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRGRgkBwEkCAEwCUEEdGR9Cz5LAJceV7SCSogqC7oif2ZaaFbkT0aMcnoFyyfBgkEg7K/IzbpMUEbatodbeOpCPFebunhR9wCXs7B8lTcKNQEoARgkAgE2AwQCBAEYMAQUTYn5+OBsvnwU4qs/Er+byaEnS/AwBRS5+zzv8ZPGnI9mC3wH9vq10JnwlhgwC0D4oAj5zm+W4u/MaHn8Xzqh3zzGdKh2OrSqols1utweoW2ODVMf+AT0WNmG9sOxeaoOPppaFVorZf5T1KtB0T9gGA==",
|
||||
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE/DujEcdTsX19xbxX+KuKKWiMaA5D9u99P/pVxIOmscd2BA2PadEMNnjvtPOpf+WE2Zxar4rby1IfAClGUUuQrTcKNQEpARgkAmAwBBS5+zzv8ZPGnI9mC3wH9vq10JnwljAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQGkPpvsbkAFEbfPN6H3Kf23R0zzmW/gpAA3kgaL6wKB2Ofm+Tmylw22qM536Kj8mOMwaV0EL1dCCGcuxF98aL6gY",
|
||||
"254": 3
|
||||
}
|
||||
],
|
||||
"0/62/2": 5,
|
||||
"0/62/3": 3,
|
||||
"0/62/1": [
|
||||
{
|
||||
"1": "BBmX+KwLR5HGlVNbvlC+dO8Jv9fPthHiTfGpUzi2JJADX5az6GxBAFn02QKHwLcZHyh+lh9faf6rf38/nPYF7/M=",
|
||||
"2": 4939,
|
||||
"3": 2,
|
||||
"4": 25,
|
||||
"5": "Home",
|
||||
"254": 3
|
||||
}
|
||||
],
|
||||
"0/62/4": [
|
||||
"FTABAQAkAgE3AyYUyakYCSYVj6gLsxgmBPIA5y8kBQA3BiYUyakYCSYVj6gLsxgkBwEkCAEwCUEEgYwxrTB+tyiEGfrRwjlXTG34MiQtJXbg5Qqd0ohdRW7MfwYY7vZiX/0h9hI8MqUralFaVPcnghAP0MSJm1YrqTcKNQEpARgkAmAwBBS3BS9aJzt+p6i28Nj+trB2Uu+vdzAFFLcFL1onO36nqLbw2P62sHZS7693GDALQPnIJqOtiZpRoUcwAo5GzvuP5SeVloEfg6jDfAMYWb+Sm6X4b9FLaO9IVlUmABOKG5Ay+6ayHN5KRUFmoo4TrxIY",
|
||||
"FTABAQAkAgE3AycUQhmZbaIbYjokFQIYJgRWZLcqJAUANwYnFEIZmW2iG2I6JBUCGCQHASQIATAJQQT2AlKGW/kOMjqayzeO0md523/fuhrhGEUU91uQpTiKo0I7wcPpKnmrwfQNPX6g0kEQl+VGaXa3e22lzfu5Tzp0Nwo1ASkBGCQCYDAEFOOMk13ScMKuT2hlaydi1yEJnhTqMAUU44yTXdJwwq5PaGVrJ2LXIQmeFOoYMAtAv2jJd1qd5miXbYesH1XrJ+vgyY0hzGuZ78N6Jw4Cb1oN1sLSpA+PNM0u7+hsEqcSvvn2eSV8EaRR+hg5YQjHDxg=",
|
||||
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEGZf4rAtHkcaVU1u+UL507wm/18+2EeJN8alTOLYkkANflrPobEEAWfTZAofAtxkfKH6WH19p/qt/fz+c9gXv8zcKNQEpARgkAmAwBBT0+qfdyShnG+4Pq01pwOnrxdhHRjAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQPVrsFnfFplsQGV5m5EUua+rmo9hAr+OP1bvaifdLqiEIn3uXLTLoKmVUkPImRL2Fb+xcMEAqR2p7RM6ZlFCR20Y"
|
||||
],
|
||||
"0/62/5": 3,
|
||||
"0/62/65533": 2,
|
||||
"0/62/65528": [1, 3, 5, 8, 14],
|
||||
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11, 12, 13],
|
||||
"0/62/65531": [65532, 0, 2, 3, 1, 4, 5, 65533, 65528, 65529, 65531],
|
||||
"0/63/65532": 0,
|
||||
"0/63/65533": 2,
|
||||
"0/63/0": [],
|
||||
"0/63/1": [],
|
||||
"0/63/2": 0,
|
||||
"0/63/3": 3,
|
||||
"0/63/65528": [2, 5],
|
||||
"0/63/65529": [0, 1, 3, 4],
|
||||
"0/63/65531": [65532, 65533, 0, 1, 2, 3, 65528, 65529, 65531],
|
||||
"0/42/65532": 0,
|
||||
"0/42/0": [],
|
||||
"0/42/65533": 1,
|
||||
"0/42/1": true,
|
||||
"0/42/2": 1,
|
||||
"0/42/3": null,
|
||||
"0/42/65528": [],
|
||||
"0/42/65529": [0],
|
||||
"0/42/65531": [65532, 0, 65533, 1, 2, 3, 65528, 65529, 65531],
|
||||
"1/29/0": [
|
||||
{
|
||||
"0": 24,
|
||||
"1": 1
|
||||
},
|
||||
{
|
||||
"0": 17,
|
||||
"1": 1
|
||||
},
|
||||
{
|
||||
"0": 1296,
|
||||
"1": 1
|
||||
},
|
||||
{
|
||||
"0": 1293,
|
||||
"1": 2
|
||||
}
|
||||
],
|
||||
"1/29/1": [29, 47, 156, 144, 145, 152, 159],
|
||||
"1/29/2": [],
|
||||
"1/29/3": [],
|
||||
"1/29/65532": 0,
|
||||
"1/29/65533": 3,
|
||||
"1/29/4": [],
|
||||
"1/29/65528": [],
|
||||
"1/29/65529": [],
|
||||
"1/29/65531": [0, 1, 2, 3, 65532, 65533, 4, 65528, 65529, 65531],
|
||||
"1/47/65532": 7,
|
||||
"1/47/65533": 3,
|
||||
"1/47/0": 0,
|
||||
"1/47/1": 0,
|
||||
"1/47/2": "Main",
|
||||
"1/47/31": [],
|
||||
"1/47/5": 0,
|
||||
"1/47/11": 48000,
|
||||
"1/47/12": 180,
|
||||
"1/47/13": 7200,
|
||||
"1/47/18": [],
|
||||
"1/47/24": 100000,
|
||||
"1/47/27": 1800,
|
||||
"1/47/29": null,
|
||||
"1/47/30": null,
|
||||
"1/47/65528": [],
|
||||
"1/47/65529": [],
|
||||
"1/47/65531": [
|
||||
65532, 65533, 0, 1, 2, 31, 5, 11, 12, 13, 18, 24, 27, 29, 30, 65528,
|
||||
65529, 65531
|
||||
],
|
||||
"1/156/65532": 0,
|
||||
"1/156/65533": 1,
|
||||
"1/156/65528": [],
|
||||
"1/156/65529": [],
|
||||
"1/156/65531": [65532, 65533, 65528, 65529, 65531],
|
||||
"1/144/65532": 0,
|
||||
"1/144/0": 0,
|
||||
"1/144/1": 0,
|
||||
"1/144/2": null,
|
||||
"1/144/8": 0,
|
||||
"1/144/65533": 1,
|
||||
"1/144/4": 0,
|
||||
"1/144/5": 0,
|
||||
"1/144/65528": [],
|
||||
"1/144/65529": [],
|
||||
"1/144/65531": [65532, 0, 1, 2, 8, 65533, 4, 5, 65528, 65529, 65531],
|
||||
"1/145/65532": 0,
|
||||
"1/145/0": null,
|
||||
"1/145/65533": 1,
|
||||
"1/145/65528": [],
|
||||
"1/145/65529": [],
|
||||
"1/145/65531": [65532, 0, 65533, 65528, 65529, 65531],
|
||||
"1/152/65532": 1,
|
||||
"1/152/0": 5,
|
||||
"1/152/1": false,
|
||||
"1/152/2": 1,
|
||||
"1/152/3": 0,
|
||||
"1/152/4": 0,
|
||||
"1/152/65533": 4,
|
||||
"1/152/5": null,
|
||||
"1/152/7": 0,
|
||||
"1/152/65528": [],
|
||||
"1/152/65529": [0, 1],
|
||||
"1/152/65531": [65532, 0, 1, 2, 3, 4, 65533, 5, 7, 65528, 65529, 65531],
|
||||
"1/159/65532": 0,
|
||||
"1/159/0": null,
|
||||
"1/159/65533": 2,
|
||||
"1/159/1": 0,
|
||||
"1/159/65528": [1],
|
||||
"1/159/65529": [0],
|
||||
"1/159/65531": [65532, 0, 65533, 1, 65528, 65529, 65531]
|
||||
},
|
||||
"attribute_subscriptions": []
|
||||
}
|
@@ -1251,6 +1251,539 @@
|
||||
'state': '189.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_appliance_energy_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'offline',
|
||||
'online',
|
||||
'fault',
|
||||
'power_adjust_active',
|
||||
'paused',
|
||||
]),
|
||||
}),
|
||||
'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.mock_battery_storage_appliance_energy_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Appliance energy state',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'esa_state',
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-ESAState-152-2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_appliance_energy_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Mock Battery Storage Appliance energy state',
|
||||
'options': list([
|
||||
'offline',
|
||||
'online',
|
||||
'fault',
|
||||
'power_adjust_active',
|
||||
'paused',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_appliance_energy_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'online',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_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.mock_battery_storage_battery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-PowerSource-47-12',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Mock Battery Storage Battery',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_battery',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '90',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_battery_voltage-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.mock_battery_storage_battery_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery voltage',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'battery_voltage',
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-PowerSourceBatVoltage-47-11',
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_battery_voltage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'voltage',
|
||||
'friendly_name': 'Mock Battery Storage Battery voltage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_battery_voltage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '48.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_current-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.mock_battery_storage_current',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Current',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-ElectricalPowerMeasurementActiveCurrent-144-5',
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_current-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'current',
|
||||
'friendly_name': 'Mock Battery Storage Current',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_current',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_energy_optimization_opt_out-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'no_opt_out',
|
||||
'local_opt_out',
|
||||
'grid_opt_out',
|
||||
'opt_out',
|
||||
]),
|
||||
}),
|
||||
'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.mock_battery_storage_energy_optimization_opt_out',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Energy optimization opt-out',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'esa_opt_out_state',
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-ESAOptOutState-152-7',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_energy_optimization_opt_out-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Mock Battery Storage Energy optimization opt-out',
|
||||
'options': list([
|
||||
'no_opt_out',
|
||||
'local_opt_out',
|
||||
'grid_opt_out',
|
||||
'opt_out',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_energy_optimization_opt_out',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'no_opt_out',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_power-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.mock_battery_storage_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-ElectricalPowerMeasurementWatt-144-8',
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Mock Battery Storage Power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_time_remaining-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.mock_battery_storage_time_remaining',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Time remaining',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'battery_time_remaining',
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-PowerSourceBatTimeRemaining-47-13',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_time_remaining-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Mock Battery Storage Time remaining',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_time_remaining',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '120.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_time_to_full_charge-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.mock_battery_storage_time_to_full_charge',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Time to full charge',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'battery_time_to_full_charge',
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-PowerSourceBatTimeToFullCharge-47-27',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_time_to_full_charge-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Mock Battery Storage Time to full charge',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_time_to_full_charge',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '30.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_voltage-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.mock_battery_storage_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Voltage',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-ElectricalPowerMeasurementVoltage-144-4',
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[battery_storage][sensor.mock_battery_storage_voltage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'voltage',
|
||||
'friendly_name': 'Mock Battery Storage Voltage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_battery_storage_voltage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[cooktop][sensor.mock_cooktop_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -4174,6 +4707,68 @@
|
||||
'state': '32.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[silabs_evse_charging][sensor.evse_energy_optimization_opt_out-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'no_opt_out',
|
||||
'local_opt_out',
|
||||
'grid_opt_out',
|
||||
'opt_out',
|
||||
]),
|
||||
}),
|
||||
'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.evse_energy_optimization_opt_out',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Energy optimization opt-out',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'esa_opt_out_state',
|
||||
'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-ESAOptOutState-152-7',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[silabs_evse_charging][sensor.evse_energy_optimization_opt_out-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'evse Energy optimization opt-out',
|
||||
'options': list([
|
||||
'no_opt_out',
|
||||
'local_opt_out',
|
||||
'grid_opt_out',
|
||||
'opt_out',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.evse_energy_optimization_opt_out',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'no_opt_out',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[silabs_evse_charging][sensor.evse_fault_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -4918,6 +5513,68 @@
|
||||
'state': '0.1',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[silabs_water_heater][sensor.water_heater_energy_optimization_opt_out-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'no_opt_out',
|
||||
'local_opt_out',
|
||||
'grid_opt_out',
|
||||
'opt_out',
|
||||
]),
|
||||
}),
|
||||
'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.water_heater_energy_optimization_opt_out',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Energy optimization opt-out',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'esa_opt_out_state',
|
||||
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ESAOptOutState-152-7',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[silabs_water_heater][sensor.water_heater_energy_optimization_opt_out-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Water Heater Energy optimization opt-out',
|
||||
'options': list([
|
||||
'no_opt_out',
|
||||
'local_opt_out',
|
||||
'grid_opt_out',
|
||||
'opt_out',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.water_heater_energy_optimization_opt_out',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'no_opt_out',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[silabs_water_heater][sensor.water_heater_hot_water_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@@ -524,6 +524,18 @@ async def test_water_heater(
|
||||
assert state
|
||||
assert state.state == "offline"
|
||||
|
||||
# DeviceEnergyManagement -> OptOutState attribute
|
||||
state = hass.states.get("sensor.water_heater_energy_optimization_opt_out")
|
||||
assert state
|
||||
assert state.state == "no_opt_out"
|
||||
|
||||
set_node_attribute(matter_node, 2, 152, 7, 3)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.water_heater_energy_optimization_opt_out")
|
||||
assert state
|
||||
assert state.state == "opt_out"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["pump"])
|
||||
async def test_pump(
|
||||
|
@@ -30,6 +30,7 @@ from tests.common import MockConfigEntry, assert_setup_component, mock_restore_c
|
||||
TEST_OBJECT_ID = "test_template_panel"
|
||||
TEST_ENTITY_ID = f"alarm_control_panel.{TEST_OBJECT_ID}"
|
||||
TEST_STATE_ENTITY_ID = "alarm_control_panel.test"
|
||||
TEST_SWITCH = "switch.test_state"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -110,6 +111,14 @@ TEMPLATE_ALARM_CONFIG = {
|
||||
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
|
||||
}
|
||||
|
||||
TEST_STATE_TRIGGER = {
|
||||
"triggers": {"trigger": "state", "entity_id": [TEST_STATE_ENTITY_ID, TEST_SWITCH]},
|
||||
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
|
||||
"actions": [
|
||||
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_legacy_format(
|
||||
hass: HomeAssistant, count: int, panel_config: dict[str, Any]
|
||||
@@ -146,6 +155,24 @@ async def async_setup_modern_format(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def async_setup_trigger_format(
|
||||
hass: HomeAssistant, count: int, panel_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Do setup of alarm control panel integration via trigger format."""
|
||||
config = {"template": {"alarm_control_panel": panel_config, **TEST_STATE_TRIGGER}}
|
||||
|
||||
with assert_setup_component(count, template.DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
template.DOMAIN,
|
||||
config,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_panel(
|
||||
hass: HomeAssistant,
|
||||
@@ -158,6 +185,8 @@ async def setup_panel(
|
||||
await async_setup_legacy_format(hass, count, panel_config)
|
||||
elif style == ConfigurationStyle.MODERN:
|
||||
await async_setup_modern_format(hass, count, panel_config)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(hass, count, panel_config)
|
||||
|
||||
|
||||
async def async_setup_state_panel(
|
||||
@@ -188,6 +217,16 @@ async def async_setup_state_panel(
|
||||
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
|
||||
},
|
||||
)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass,
|
||||
count,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"state": state_template,
|
||||
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -228,6 +267,17 @@ async def setup_base_panel(
|
||||
**panel_config,
|
||||
},
|
||||
)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
extra = {"state": state_template} if state_template else {}
|
||||
await async_setup_trigger_format(
|
||||
hass,
|
||||
count,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
**extra,
|
||||
**panel_config,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -264,13 +314,25 @@ async def setup_single_attribute_state_panel(
|
||||
**extra,
|
||||
},
|
||||
)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass,
|
||||
count,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
|
||||
"state": state_template,
|
||||
**extra,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_panel")
|
||||
async def test_template_state_text(hass: HomeAssistant) -> None:
|
||||
@@ -301,56 +363,72 @@ async def test_template_state_text(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize("count", [1])
|
||||
@pytest.mark.parametrize(
|
||||
("state_template", "expected"),
|
||||
("state_template", "expected", "trigger_expected"),
|
||||
[
|
||||
("{{ 'disarmed' }}", AlarmControlPanelState.DISARMED),
|
||||
("{{ 'armed_home' }}", AlarmControlPanelState.ARMED_HOME),
|
||||
("{{ 'armed_away' }}", AlarmControlPanelState.ARMED_AWAY),
|
||||
("{{ 'armed_night' }}", AlarmControlPanelState.ARMED_NIGHT),
|
||||
("{{ 'armed_vacation' }}", AlarmControlPanelState.ARMED_VACATION),
|
||||
("{{ 'armed_custom_bypass' }}", AlarmControlPanelState.ARMED_CUSTOM_BYPASS),
|
||||
("{{ 'pending' }}", AlarmControlPanelState.PENDING),
|
||||
("{{ 'arming' }}", AlarmControlPanelState.ARMING),
|
||||
("{{ 'disarming' }}", AlarmControlPanelState.DISARMING),
|
||||
("{{ 'triggered' }}", AlarmControlPanelState.TRIGGERED),
|
||||
("{{ x - 1 }}", STATE_UNKNOWN),
|
||||
("{{ 'disarmed' }}", AlarmControlPanelState.DISARMED, None),
|
||||
("{{ 'armed_home' }}", AlarmControlPanelState.ARMED_HOME, None),
|
||||
("{{ 'armed_away' }}", AlarmControlPanelState.ARMED_AWAY, None),
|
||||
("{{ 'armed_night' }}", AlarmControlPanelState.ARMED_NIGHT, None),
|
||||
("{{ 'armed_vacation' }}", AlarmControlPanelState.ARMED_VACATION, None),
|
||||
(
|
||||
"{{ 'armed_custom_bypass' }}",
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||
None,
|
||||
),
|
||||
("{{ 'pending' }}", AlarmControlPanelState.PENDING, None),
|
||||
("{{ 'arming' }}", AlarmControlPanelState.ARMING, None),
|
||||
("{{ 'disarming' }}", AlarmControlPanelState.DISARMING, None),
|
||||
("{{ 'triggered' }}", AlarmControlPanelState.TRIGGERED, None),
|
||||
("{{ x - 1 }}", STATE_UNKNOWN, STATE_UNAVAILABLE),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_panel")
|
||||
async def test_state_template_states(hass: HomeAssistant, expected: str) -> None:
|
||||
async def test_state_template_states(
|
||||
hass: HomeAssistant, expected: str, trigger_expected: str, style: ConfigurationStyle
|
||||
) -> None:
|
||||
"""Test the state template."""
|
||||
|
||||
# Force a trigger
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
|
||||
if trigger_expected and style == ConfigurationStyle.TRIGGER:
|
||||
expected = trigger_expected
|
||||
|
||||
assert state.state == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template", "attribute_template"),
|
||||
("count", "state_template", "attribute_template", "attribute"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
"{{ 'disarmed' }}",
|
||||
"{% if states.switch.test_state.state %}mdi:check{% endif %}",
|
||||
"icon",
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "attribute"),
|
||||
("style", "initial_state"),
|
||||
[
|
||||
(ConfigurationStyle.MODERN, "icon"),
|
||||
(ConfigurationStyle.MODERN, ""),
|
||||
(ConfigurationStyle.TRIGGER, None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_single_attribute_state_panel")
|
||||
async def test_icon_template(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
async def test_icon_template(hass: HomeAssistant, initial_state: str) -> None:
|
||||
"""Test icon template."""
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.attributes.get("icon") in ("", None)
|
||||
assert state.attributes.get("icon") == initial_state
|
||||
|
||||
hass.states.async_set("switch.test_state", STATE_ON)
|
||||
hass.states.async_set(TEST_SWITCH, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
@@ -358,30 +436,30 @@ async def test_icon_template(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template", "attribute_template"),
|
||||
("count", "state_template", "attribute_template", "attribute"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
"{{ 'disarmed' }}",
|
||||
"{% if states.switch.test_state.state %}local/panel.png{% endif %}",
|
||||
"picture",
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "attribute"),
|
||||
("style", "initial_state"),
|
||||
[
|
||||
(ConfigurationStyle.MODERN, "picture"),
|
||||
(ConfigurationStyle.MODERN, ""),
|
||||
(ConfigurationStyle.TRIGGER, None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_single_attribute_state_panel")
|
||||
async def test_picture_template(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
async def test_picture_template(hass: HomeAssistant, initial_state: str) -> None:
|
||||
"""Test icon template."""
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.attributes.get("entity_picture") in ("", None)
|
||||
assert state.attributes.get("entity_picture") == initial_state
|
||||
|
||||
hass.states.async_set("switch.test_state", STATE_ON)
|
||||
hass.states.async_set(TEST_SWITCH, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
@@ -425,7 +503,8 @@ async def test_setup_config_entry(
|
||||
|
||||
@pytest.mark.parametrize(("count", "state_template"), [(1, None)])
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"panel_config", [OPTIMISTIC_TEMPLATE_ALARM_CONFIG, EMPTY_ACTIONS]
|
||||
@@ -459,7 +538,8 @@ async def test_optimistic_states(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize("count", [0])
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("panel_config", "state_template", "msg"),
|
||||
@@ -538,11 +618,15 @@ async def test_legacy_template_syntax_error(
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, TEST_ENTITY_ID),
|
||||
(ConfigurationStyle.MODERN, "alarm_control_panel.template_alarm_panel"),
|
||||
(ConfigurationStyle.TRIGGER, "alarm_control_panel.unnamed_device"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_single_attribute_state_panel")
|
||||
async def test_name(hass: HomeAssistant, test_entity_id: str) -> None:
|
||||
"""Test the accessibility of the name attribute."""
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, "disarmed")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.attributes.get("friendly_name") == "Template Alarm Panel"
|
||||
@@ -552,7 +636,8 @@ async def test_name(hass: HomeAssistant, test_entity_id: str) -> None:
|
||||
("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"service",
|
||||
@@ -615,6 +700,21 @@ async def test_actions(
|
||||
],
|
||||
ConfigurationStyle.MODERN,
|
||||
),
|
||||
(
|
||||
[
|
||||
{
|
||||
"name": "test_template_alarm_control_panel_01",
|
||||
"state": "{{ true }}",
|
||||
**UNIQUE_ID_CONFIG,
|
||||
},
|
||||
{
|
||||
"name": "test_template_alarm_control_panel_02",
|
||||
"state": "{{ false }}",
|
||||
**UNIQUE_ID_CONFIG,
|
||||
},
|
||||
],
|
||||
ConfigurationStyle.TRIGGER,
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_panel")
|
||||
@@ -669,7 +769,8 @@ async def test_nested_unique_id(
|
||||
|
||||
@pytest.mark.parametrize(("count", "state_template"), [(1, "disarmed")])
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("panel_config", "code_format", "code_arm_required"),
|
||||
@@ -714,7 +815,8 @@ async def test_code_config(hass: HomeAssistant, code_format, code_arm_required)
|
||||
("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("restored_state", "initial_state"),
|
||||
|
@@ -25,7 +25,19 @@ from tests.common import assert_setup_component
|
||||
|
||||
TEST_OBJECT_ID = "test_template_lock"
|
||||
TEST_ENTITY_ID = f"lock.{TEST_OBJECT_ID}"
|
||||
TEST_STATE_ENTITY_ID = "switch.test_state"
|
||||
TEST_STATE_ENTITY_ID = "sensor.test_state"
|
||||
TEST_AVAILABILITY_ENTITY_ID = "availability_state.state"
|
||||
|
||||
TEST_STATE_TRIGGER = {
|
||||
"trigger": {
|
||||
"trigger": "state",
|
||||
"entity_id": [TEST_STATE_ENTITY_ID, TEST_AVAILABILITY_ENTITY_ID],
|
||||
},
|
||||
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
|
||||
"action": [
|
||||
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
|
||||
],
|
||||
}
|
||||
|
||||
LOCK_ACTION = {
|
||||
"lock": {
|
||||
@@ -113,6 +125,29 @@ async def async_setup_modern_format(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def async_setup_trigger_format(
|
||||
hass: HomeAssistant, count: int, lock_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Do setup of lock integration via trigger format."""
|
||||
config = {
|
||||
"template": {
|
||||
"lock": {"name": TEST_OBJECT_ID, **lock_config},
|
||||
**TEST_STATE_TRIGGER,
|
||||
}
|
||||
}
|
||||
|
||||
with assert_setup_component(count, template.DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
template.DOMAIN,
|
||||
config,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_lock(
|
||||
hass: HomeAssistant,
|
||||
@@ -125,6 +160,8 @@ async def setup_lock(
|
||||
await async_setup_legacy_format(hass, count, lock_config)
|
||||
elif style == ConfigurationStyle.MODERN:
|
||||
await async_setup_modern_format(hass, count, lock_config)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(hass, count, lock_config)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -148,6 +185,12 @@ async def setup_base_lock(
|
||||
count,
|
||||
{"state": state_template, **extra_config},
|
||||
)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass,
|
||||
count,
|
||||
{"state": state_template, **extra_config},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -176,6 +219,15 @@ async def setup_state_lock(
|
||||
"state": state_template,
|
||||
},
|
||||
)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass,
|
||||
count,
|
||||
{
|
||||
**OPTIMISTIC_LOCK,
|
||||
"state": state_template,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -199,6 +251,12 @@ async def setup_state_lock_with_extra_config(
|
||||
count,
|
||||
{**OPTIMISTIC_LOCK, "state": state_template, **extra_config},
|
||||
)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass,
|
||||
count,
|
||||
{**OPTIMISTIC_LOCK, "state": state_template, **extra_config},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -228,13 +286,20 @@ async def setup_state_lock_with_attribute(
|
||||
count,
|
||||
{**OPTIMISTIC_LOCK, "state": state_template, **extra},
|
||||
)
|
||||
elif style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass,
|
||||
count,
|
||||
{**OPTIMISTIC_LOCK, "state": state_template, **extra},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template"), [(1, "{{ states.switch.test_state.state }}")]
|
||||
("count", "state_template"), [(1, "{{ states.sensor.test_state.state }}")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock")
|
||||
async def test_template_state(hass: HomeAssistant) -> None:
|
||||
@@ -260,10 +325,11 @@ async def test_template_state(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template", "extra_config"),
|
||||
[(1, "{{ states.switch.test_state.state }}", {"optimistic": True, **OPEN_ACTION})],
|
||||
[(1, "{{ states.sensor.test_state.state }}", {"optimistic": True, **OPEN_ACTION})],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_extra_config")
|
||||
async def test_open_lock_optimistic(
|
||||
@@ -293,18 +359,24 @@ async def test_open_lock_optimistic(
|
||||
|
||||
@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")])
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock")
|
||||
async def test_template_state_boolean_on(hass: HomeAssistant) -> None:
|
||||
"""Test the setting of the state with boolean on."""
|
||||
# Ensure the trigger executes for trigger configurations
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == LockState.LOCKED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 2 }}")])
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock")
|
||||
async def test_template_state_boolean_off(hass: HomeAssistant) -> None:
|
||||
@@ -315,7 +387,8 @@ async def test_template_state_boolean_off(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize("count", [0])
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("state_template", "extra_config"),
|
||||
@@ -326,7 +399,7 @@ async def test_template_state_boolean_off(hass: HomeAssistant) -> None:
|
||||
(
|
||||
"{{ 1==1 }}",
|
||||
{
|
||||
"not_value_template": "{{ states.switch.test_state.state }}",
|
||||
"not_value_template": "{{ states.sensor.test_state.state }}",
|
||||
**OPTIMISTIC_LOCK,
|
||||
},
|
||||
),
|
||||
@@ -345,6 +418,7 @@ async def test_template_syntax_error(hass: HomeAssistant) -> None:
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "code_format_template"),
|
||||
(ConfigurationStyle.MODERN, "code_format"),
|
||||
(ConfigurationStyle.TRIGGER, "code_format"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_attribute")
|
||||
@@ -355,7 +429,8 @@ async def test_template_code_template_syntax_error(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 + 1 }}")])
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock")
|
||||
async def test_template_static(hass: HomeAssistant) -> None:
|
||||
@@ -371,7 +446,8 @@ async def test_template_static(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize("count", [1])
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("state_template", "expected"),
|
||||
@@ -384,26 +460,33 @@ async def test_template_static(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.usefixtures("setup_state_lock")
|
||||
async def test_state_template(hass: HomeAssistant, expected: str) -> None:
|
||||
"""Test state and value_template template."""
|
||||
# Ensure the trigger executes for trigger configurations
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1==1 }}")])
|
||||
@pytest.mark.parametrize(
|
||||
"attribute_template",
|
||||
["{% if states.switch.test_state.state %}/local/switch.png{% endif %}"],
|
||||
("count", "state_template", "attribute"), [(1, "{{ 1==1 }}", "picture")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "attribute"),
|
||||
"attribute_template",
|
||||
["{% if states.sensor.test_state.state %}/local/switch.png{% endif %}"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "initial_state"),
|
||||
[
|
||||
(ConfigurationStyle.MODERN, "picture"),
|
||||
(ConfigurationStyle.MODERN, ""),
|
||||
(ConfigurationStyle.TRIGGER, None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_attribute")
|
||||
async def test_picture_template(hass: HomeAssistant) -> None:
|
||||
async def test_picture_template(hass: HomeAssistant, initial_state: str) -> None:
|
||||
"""Test entity_picture template."""
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.attributes.get("entity_picture") in ("", None)
|
||||
assert state.attributes.get("entity_picture") == initial_state
|
||||
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
@@ -412,22 +495,25 @@ async def test_picture_template(hass: HomeAssistant) -> None:
|
||||
assert state.attributes["entity_picture"] == "/local/switch.png"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1==1 }}")])
|
||||
@pytest.mark.parametrize(
|
||||
"attribute_template",
|
||||
["{% if states.switch.test_state.state %}mdi:eye{% endif %}"],
|
||||
("count", "state_template", "attribute"), [(1, "{{ 1==1 }}", "icon")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "attribute"),
|
||||
"attribute_template",
|
||||
["{% if states.sensor.test_state.state %}mdi:eye{% endif %}"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "initial_state"),
|
||||
[
|
||||
(ConfigurationStyle.MODERN, "icon"),
|
||||
(ConfigurationStyle.MODERN, ""),
|
||||
(ConfigurationStyle.TRIGGER, None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_attribute")
|
||||
async def test_icon_template(hass: HomeAssistant) -> None:
|
||||
async def test_icon_template(hass: HomeAssistant, initial_state: str) -> None:
|
||||
"""Test entity_picture template."""
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.attributes.get("icon") in ("", None)
|
||||
assert state.attributes.get("icon") == initial_state
|
||||
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
@@ -437,10 +523,11 @@ async def test_icon_template(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template"), [(1, "{{ states.switch.test_state.state }}")]
|
||||
("count", "state_template"), [(1, "{{ states.sensor.test_state.state }}")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock")
|
||||
async def test_lock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
|
||||
@@ -464,10 +551,11 @@ async def test_lock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> Non
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template"), [(1, "{{ states.switch.test_state.state }}")]
|
||||
("count", "state_template"), [(1, "{{ states.sensor.test_state.state }}")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock")
|
||||
async def test_unlock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
|
||||
@@ -492,10 +580,11 @@ async def test_unlock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> N
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template", "extra_config"),
|
||||
[(1, "{{ states.switch.test_state.state }}", OPEN_ACTION)],
|
||||
[(1, "{{ states.sensor.test_state.state }}", OPEN_ACTION)],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_extra_config")
|
||||
async def test_open_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
|
||||
@@ -523,7 +612,7 @@ async def test_open_action(hass: HomeAssistant, calls: list[ServiceCall]) -> Non
|
||||
[
|
||||
(
|
||||
1,
|
||||
"{{ states.switch.test_state.state }}",
|
||||
"{{ states.sensor.test_state.state }}",
|
||||
"{{ '.+' }}",
|
||||
)
|
||||
],
|
||||
@@ -533,6 +622,7 @@ async def test_open_action(hass: HomeAssistant, calls: list[ServiceCall]) -> Non
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "code_format_template"),
|
||||
(ConfigurationStyle.MODERN, "code_format"),
|
||||
(ConfigurationStyle.TRIGGER, "code_format"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_attribute")
|
||||
@@ -564,7 +654,7 @@ async def test_lock_action_with_code(
|
||||
[
|
||||
(
|
||||
1,
|
||||
"{{ states.switch.test_state.state }}",
|
||||
"{{ states.sensor.test_state.state }}",
|
||||
"{{ '.+' }}",
|
||||
)
|
||||
],
|
||||
@@ -574,6 +664,7 @@ async def test_lock_action_with_code(
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "code_format_template"),
|
||||
(ConfigurationStyle.MODERN, "code_format"),
|
||||
(ConfigurationStyle.TRIGGER, "code_format"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_attribute")
|
||||
@@ -616,6 +707,7 @@ async def test_unlock_action_with_code(
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "code_format_template"),
|
||||
(ConfigurationStyle.MODERN, "code_format"),
|
||||
(ConfigurationStyle.TRIGGER, "code_format"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
@@ -630,6 +722,10 @@ async def test_lock_actions_fail_with_invalid_code(
|
||||
hass: HomeAssistant, calls: list[ServiceCall], test_action
|
||||
) -> None:
|
||||
"""Test invalid lock codes."""
|
||||
# Ensure trigger entities updated
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
lock.DOMAIN,
|
||||
test_action,
|
||||
@@ -656,17 +752,23 @@ async def test_lock_actions_fail_with_invalid_code(
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "attribute"),
|
||||
("style", "attribute", "expected"),
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "code_format_template"),
|
||||
(ConfigurationStyle.MODERN, "code_format"),
|
||||
(ConfigurationStyle.LEGACY, "code_format_template", 0),
|
||||
(ConfigurationStyle.MODERN, "code_format", 0),
|
||||
(ConfigurationStyle.TRIGGER, "code_format", 2),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_attribute")
|
||||
async def test_lock_actions_dont_execute_with_code_template_rendering_error(
|
||||
hass: HomeAssistant, calls: list[ServiceCall]
|
||||
hass: HomeAssistant, calls: list[ServiceCall], expected: int
|
||||
) -> None:
|
||||
"""Test lock code format rendering fails block lock/unlock actions."""
|
||||
|
||||
# Ensure trigger entities updated
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
lock.DOMAIN,
|
||||
lock.SERVICE_LOCK,
|
||||
@@ -679,7 +781,10 @@ async def test_lock_actions_dont_execute_with_code_template_rendering_error(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 0
|
||||
# Trigger expects calls here because trigger based entities don't
|
||||
# allow template exception resolutions into code_format property so
|
||||
# the actions will fire using the previous code_format.
|
||||
assert len(calls) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -687,7 +792,7 @@ async def test_lock_actions_dont_execute_with_code_template_rendering_error(
|
||||
[
|
||||
(
|
||||
1,
|
||||
"{{ states.switch.test_state.state }}",
|
||||
"{{ states.sensor.test_state.state }}",
|
||||
"{{ None }}",
|
||||
)
|
||||
],
|
||||
@@ -697,6 +802,7 @@ async def test_lock_actions_dont_execute_with_code_template_rendering_error(
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "code_format_template"),
|
||||
(ConfigurationStyle.MODERN, "code_format"),
|
||||
(ConfigurationStyle.TRIGGER, "code_format"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK])
|
||||
@@ -729,7 +835,7 @@ async def test_actions_with_none_as_codeformat_ignores_code(
|
||||
[
|
||||
(
|
||||
1,
|
||||
"{{ states.switch.test_state.state }}",
|
||||
"{{ states.sensor.test_state.state }}",
|
||||
"[12]{1",
|
||||
)
|
||||
],
|
||||
@@ -739,6 +845,7 @@ async def test_actions_with_none_as_codeformat_ignores_code(
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "code_format_template"),
|
||||
(ConfigurationStyle.MODERN, "code_format"),
|
||||
(ConfigurationStyle.TRIGGER, "code_format"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK])
|
||||
@@ -774,10 +881,11 @@ async def test_actions_with_invalid_regexp_as_codeformat_never_execute(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "state_template"), [(1, "{{ states.input_select.test_state.state }}")]
|
||||
("count", "state_template"), [(1, "{{ states.sensor.test_state.state }}")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
|
||||
"style",
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"test_state", [LockState.UNLOCKING, LockState.LOCKING, LockState.JAMMED]
|
||||
@@ -785,7 +893,7 @@ async def test_actions_with_invalid_regexp_as_codeformat_never_execute(
|
||||
@pytest.mark.usefixtures("setup_state_lock")
|
||||
async def test_lock_state(hass: HomeAssistant, test_state) -> None:
|
||||
"""Test value template."""
|
||||
hass.states.async_set("input_select.test_state", test_state)
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, test_state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
@@ -797,7 +905,7 @@ async def test_lock_state(hass: HomeAssistant, test_state) -> None:
|
||||
[
|
||||
(
|
||||
1,
|
||||
"{{ states('switch.test_state') }}",
|
||||
"{{ states('sensor.test_state') }}",
|
||||
"{{ is_state('availability_state.state', 'on') }}",
|
||||
)
|
||||
],
|
||||
@@ -807,20 +915,21 @@ async def test_lock_state(hass: HomeAssistant, test_state) -> None:
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "availability_template"),
|
||||
(ConfigurationStyle.MODERN, "availability"),
|
||||
(ConfigurationStyle.TRIGGER, "availability"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_attribute")
|
||||
async def test_available_template_with_entities(hass: HomeAssistant) -> None:
|
||||
"""Test availability templates with values from other entities."""
|
||||
# When template returns true..
|
||||
hass.states.async_set("availability_state.state", STATE_ON)
|
||||
hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Device State should not be unavailable
|
||||
assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE
|
||||
|
||||
# When Availability template returns false
|
||||
hass.states.async_set("availability_state.state", STATE_OFF)
|
||||
hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# device state should be unavailable
|
||||
@@ -842,15 +951,20 @@ async def test_available_template_with_entities(hass: HomeAssistant) -> None:
|
||||
[
|
||||
(ConfigurationStyle.LEGACY, "availability_template"),
|
||||
(ConfigurationStyle.MODERN, "availability"),
|
||||
(ConfigurationStyle.TRIGGER, "availability"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_lock_with_attribute")
|
||||
async def test_invalid_availability_template_keeps_component_available(
|
||||
hass: HomeAssistant, caplog_setup_text
|
||||
hass: HomeAssistant, caplog_setup_text, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test that an invalid availability keeps the device available."""
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE
|
||||
assert ("UndefinedError: 'x' is undefined") in caplog_setup_text
|
||||
err = "'x' is undefined"
|
||||
assert err in caplog_setup_text or err in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)])
|
||||
|
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
|
@@ -48,6 +48,7 @@ async def _setup_legacy_component(hass: HomeAssistant, params: dict[str, Any]) -
|
||||
)
|
||||
async def test_basic_trend_setup_from_yaml(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
states: list[str],
|
||||
inverted: bool,
|
||||
expected_state: str,
|
||||
@@ -72,6 +73,43 @@ async def test_basic_trend_setup_from_yaml(
|
||||
assert (sensor_state := hass.states.get("binary_sensor.test_trend_sensor"))
|
||||
assert sensor_state.state == expected_state
|
||||
|
||||
# Verify that entity without unique_id in YAML is not in the registry
|
||||
entity_entry = entity_registry.async_get("binary_sensor.test_trend_sensor")
|
||||
assert entity_entry is None
|
||||
|
||||
|
||||
async def test_trend_setup_from_yaml_with_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test trend setup from YAML with unique_id."""
|
||||
await _setup_legacy_component(
|
||||
hass,
|
||||
{
|
||||
"friendly_name": "Test state with ID",
|
||||
"entity_id": "sensor.cpu_temp",
|
||||
"unique_id": "my_unique_trend_sensor",
|
||||
"max_samples": 2.0,
|
||||
"min_gradient": 0.0,
|
||||
"sample_duration": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
# Set some states to ensure the sensor works
|
||||
hass.states.async_set("sensor.cpu_temp", "1")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.cpu_temp", "2")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that the sensor exists and has the correct state
|
||||
assert (sensor_state := hass.states.get("binary_sensor.test_trend_sensor"))
|
||||
assert sensor_state.state == STATE_ON
|
||||
|
||||
# Check that the entity is registered with the correct unique_id
|
||||
entity_entry = entity_registry.async_get("binary_sensor.test_trend_sensor")
|
||||
assert entity_entry is not None
|
||||
assert entity_entry.unique_id == "my_unique_trend_sensor"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("states", "inverted", "expected_state"),
|
||||
|
Reference in New Issue
Block a user