Compare commits

...

36 Commits

Author SHA1 Message Date
farmio 73967ec3b8 override decorator 2026-06-23 22:25:28 +02:00
farmio 7e7c846c78 Remove unused logger 2026-06-23 22:24:47 +02:00
farmio 7ff7c45b18 improve typing 2026-06-23 22:24:47 +02:00
farmio 4481e90ee6 fix flaky tests 2026-06-23 22:24:47 +02:00
farmio 7670dcb227 store raw payload as hex string
this helps FE work with payloads >7byte
2026-06-23 22:24:47 +02:00
farmio db0f9d1f54 tests 2026-06-23 22:24:47 +02:00
farmio f1535bf8a4 Schema fixes 2026-06-23 22:24:47 +02:00
farmio c460100fa1 Validation 2026-06-23 22:24:46 +02:00
farmio 53e2a9341f Fix loading raw value 2026-06-23 22:24:46 +02:00
farmio 0e713d549c Rely on store validation 2026-06-23 22:24:46 +02:00
farmio a068574fe2 Add KNX button to UI configured entities 2026-06-23 22:24:46 +02:00
J. Nick Koston f57418ed60 Bump govee-ble to 1.4.0 (#174553) 2026-06-23 20:27:33 +02:00
Abílio Costa 8d1bf68045 Enable mypy explicit-override check (#171853)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-06-23 18:20:08 +01:00
Raphael Hehl c5bfad9bfe Remove UniFi LED (unifiled) integration (#168232)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-23 17:45:31 +02:00
Abílio Costa 679b0ac2aa Add agent instruction to prevent divider comments (#174531) 2026-06-23 16:41:43 +01:00
epenet 22a583c83f Migrate climate entity attributes to StrEnum (#174528) 2026-06-23 16:48:38 +02:00
epenet d966f71832 Migrate select entity attributes to StrEnum (#174536) 2026-06-23 16:44:47 +02:00
Manu 08569420f6 Remove Eliqonline integration (#174538) 2026-06-23 17:10:21 +03:00
Ronald van der Meer c1bcbca520 Add filter remaining days sensor to Duco (#174316) 2026-06-23 15:16:42 +02:00
Simone Chemelli c73c647162 Improve docstring for async_get_entity_id() method (#174532) 2026-06-23 14:50:34 +02:00
Markus Tuominen b2f1c38b6f Bump ouman-eh-800-api to 1.0.0 (#174458) 2026-06-23 14:45:19 +02:00
Erik Montnemery e8824bedf5 Add additional sun triggers (#174485) 2026-06-23 14:23:29 +02:00
Ajinkya Gokhale 27b107f4a5 Update energieleser to silver quality scale (#174535) 2026-06-23 14:23:06 +02:00
Petro31 7536e8647f Fix entities listed in template blueprints (#171861)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-23 13:35:20 +02:00
Simone Chemelli 75c6058396 Add "Drop in" select to Alexa Devices (#174336) 2026-06-23 10:47:44 +02:00
Matthias Alphart 77533e5af5 Rename "Advanced options" in KNX strings (#174523) 2026-06-23 10:27:11 +02:00
LG-ThinQ-Integration 31a1e7c5e1 Bump thinqconnect 1.0.13 (#174510)
Co-authored-by: YunseonPark-LGE <yunseon.park@lge.com>
2026-06-23 09:55:33 +02:00
renovate[bot] 20f4e7306b Update pytest-unordered to 0.8.0 (#174515) 2026-06-23 09:15:35 +02:00
Mick Vleeshouwer 5c1ac08c92 Catch connection errors when executing Overkiz commands (#174453) 2026-06-23 08:51:36 +02:00
Franck Nijhof 5631c68069 Reword trigger descriptions for presence and detection entities (#174467) 2026-06-23 08:51:14 +02:00
Franck Nijhof 8648611278 Clarify the media player play media action name (#174480) 2026-06-23 08:49:04 +02:00
Franck Nijhof 971d15be1e Improve trigger wording for lawn mower and vacuum entities (#174477)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-06-23 08:48:45 +02:00
Åke Strandberg b6f2429ff3 Bump pyaqvify to 0.0.12 (#174516) 2026-06-23 08:35:13 +02:00
Jacob Hurwitz c676e2a806 Bump python-dropbox-api to 0.1.4 (#174512) 2026-06-23 07:22:21 +02:00
Robert Resch a27ea536db Enable aw check requirements on each deps PR (#174481) 2026-06-23 02:14:03 +02:00
Erwin Douna 22e25d9ce2 MELCloud Home exand sensor state behavior (#174495) 2026-06-23 00:44:14 +02:00
221 changed files with 6556 additions and 4645 deletions
+1
View File
@@ -53,3 +53,4 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
@@ -8,15 +8,11 @@ name: Check requirements (deterministic)
# yamllint disable-line rule:truthy
on:
# Auto-trigger on PRs that touch tracked requirement files is disabled
# for now while we iterate — testing the workflow_run handoff to the
# agentic stage is hard with an auto-trigger. Re-enable once the chain
# has been validated end-to-end.
# pull_request:
# types: [opened, synchronize, reopened]
# paths:
# - "**/requirements*.txt"
# - "homeassistant/package_constraints.txt"
pull_request:
types: [opened, synchronize, reopened]
paths:
- "requirements*.txt"
- "homeassistant/package_constraints.txt"
workflow_dispatch:
inputs:
pull_request_number:
+1
View File
@@ -42,3 +42,4 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
Generated
-1
View File
@@ -1900,7 +1900,6 @@ CLAUDE.md @home-assistant/core
/tests/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifi_discovery/ @RaHehl
/tests/components/unifi_discovery/ @RaHehl
/homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @RaHehl
/tests/components/unifiprotect/ @RaHehl
/homeassistant/components/upb/ @gwww
-1
View File
@@ -7,7 +7,6 @@
"unifi_access",
"unifi_direct",
"unifi_discovery",
"unifiled",
"unifiprotect"
]
}
@@ -16,6 +16,7 @@ PLATFORMS = [
Platform.EVENT,
Platform.MEDIA_PLAYER,
Platform.NOTIFY,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TODO,
@@ -5,6 +5,16 @@
"default": "mdi:chat-processing"
}
},
"select": {
"dropin": {
"default": "mdi:account-multiple-plus",
"state": {
"all": "mdi:account-multiple-plus",
"home": "mdi:home-plus",
"off": "mdi:phone-off"
}
}
},
"sensor": {
"voc_index": {
"default": "mdi:molecule"
@@ -84,6 +84,7 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
entity_description: AmazonNotifyEntityDescription
@property
@override
def available(self) -> bool:
"""Return if entity is available."""
return (
@@ -0,0 +1,105 @@
"""Support for select entities."""
from dataclasses import dataclass
from typing import TYPE_CHECKING, Final, override
from aioamazondevices.structures import AmazonDropInStatus
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry, AmazonDevice, alexa_api_call
from .entity import AmazonEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AmazonSelectEntityDescription(SelectEntityDescription):
"""Alexa Devices select entity description."""
method: str
SELECTS: Final = (
AmazonSelectEntityDescription(
key="dropin",
translation_key="dropin",
entity_category=EntityCategory.CONFIG,
method="set_dropin_status",
# API values: "All", "Home", "Off"
options=[status.value.lower() for status in AmazonDropInStatus],
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up select entities based on a config entry."""
coordinator = entry.runtime_data
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
select_entities = [
AmazonSelectEntity(coordinator, serial_num, select_desc)
for select_desc in SELECTS
for serial_num in new_devices
if select_desc.key
in coordinator.data[serial_num].communication_settings
]
async_add_entities(select_entities)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonSelectEntity(AmazonEntity, SelectEntity):
"""Representation of a select entity for the default Alexa device."""
entity_description: AmazonSelectEntityDescription
@property
@override
def options(self) -> list[str]:
"""Return a list of available options."""
if TYPE_CHECKING:
assert self.entity_description.options is not None
return self.entity_description.options
@property
def _device(self) -> AmazonDevice:
"""Return the device."""
return self.coordinator.data[self._serial_num]
@override
async def async_added_to_hass(self) -> None:
"""Restore last known option."""
await super().async_added_to_hass()
self._attr_current_option = self._device.communication_settings[
self.entity_description.key
].lower()
@override
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
method = getattr(self.coordinator.api, self.entity_description.method)
if TYPE_CHECKING:
assert method is not None
async with alexa_api_call(self.coordinator):
await method(self.device, AmazonDropInStatus(option.capitalize()))
self._attr_current_option = option
self.async_write_ha_state()
@@ -78,6 +78,16 @@
"name": "Speak"
}
},
"select": {
"dropin": {
"name": "Drop In",
"state": {
"all": "Allow drop in from anyone",
"home": "Allow drop in from household members",
"off": "Don't allow drop in"
}
}
},
"sensor": {
"alarm": {
"name": "Next alarm"
@@ -140,6 +150,9 @@
"invalid_sound_value": {
"message": "Invalid sound {sound} specified"
},
"select_option_not_found": {
"message": "Selected option not found: {option}"
},
"unknown_exception": {
"message": "Unknown error occurred: {error}"
}
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyaqvify"],
"quality_scale": "platinum",
"requirements": ["pyaqvify==0.0.11"]
"requirements": ["pyaqvify==0.0.12"]
}
@@ -174,6 +174,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
"""Set the fan mode."""
await self.coordinator.async_set_fan_mode(self._ac_index, self.data, fan_mode)
@override
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set the swing mode."""
await self.coordinator.async_set_swing_mode(
+46 -32
View File
@@ -84,7 +84,9 @@ from .const import ( # noqa: F401
SWING_OFF,
SWING_ON,
SWING_VERTICAL,
ClimateEntityCapabilityAttribute,
ClimateEntityFeature,
ClimateEntityStateAttribute,
HVACAction,
HVACMode,
)
@@ -242,16 +244,16 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_entity_component_unrecorded_attributes = frozenset(
{
ATTR_HVAC_MODES,
ATTR_FAN_MODES,
ATTR_SWING_MODES,
ATTR_MIN_TEMP,
ATTR_MAX_TEMP,
ATTR_MIN_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_TARGET_HUMIDITY_STEP,
ATTR_TARGET_TEMP_STEP,
ATTR_PRESET_MODES,
ClimateEntityCapabilityAttribute.HVAC_MODES,
ClimateEntityCapabilityAttribute.FAN_MODES,
ClimateEntityCapabilityAttribute.SWING_MODES,
ClimateEntityCapabilityAttribute.MIN_TEMP,
ClimateEntityCapabilityAttribute.MAX_TEMP,
ClimateEntityCapabilityAttribute.MIN_HUMIDITY,
ClimateEntityCapabilityAttribute.MAX_HUMIDITY,
ClimateEntityCapabilityAttribute.TARGET_HUMIDITY_STEP,
ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP,
ClimateEntityCapabilityAttribute.PRESET_MODES,
}
)
@@ -315,32 +317,42 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
hass = self.hass
data: dict[str, Any] = {
ATTR_HVAC_MODES: self.hvac_modes,
ATTR_MIN_TEMP: show_temp(hass, self.min_temp, temperature_unit, precision),
ATTR_MAX_TEMP: show_temp(hass, self.max_temp, temperature_unit, precision),
ClimateEntityCapabilityAttribute.HVAC_MODES: self.hvac_modes,
ClimateEntityCapabilityAttribute.MIN_TEMP: show_temp(
hass, self.min_temp, temperature_unit, precision
),
ClimateEntityCapabilityAttribute.MAX_TEMP: show_temp(
hass, self.max_temp, temperature_unit, precision
),
}
if target_temperature_step := self.target_temperature_step:
data[ATTR_TARGET_TEMP_STEP] = target_temperature_step
data[ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP] = (
target_temperature_step
)
if ClimateEntityFeature.TARGET_HUMIDITY in supported_features:
data[ATTR_MIN_HUMIDITY] = self.min_humidity
data[ATTR_MAX_HUMIDITY] = self.max_humidity
data[ClimateEntityCapabilityAttribute.MIN_HUMIDITY] = self.min_humidity
data[ClimateEntityCapabilityAttribute.MAX_HUMIDITY] = self.max_humidity
if self.target_humidity_step is not None:
data[ATTR_TARGET_HUMIDITY_STEP] = self.target_humidity_step
data[ClimateEntityCapabilityAttribute.TARGET_HUMIDITY_STEP] = (
self.target_humidity_step
)
if ClimateEntityFeature.FAN_MODE in supported_features:
data[ATTR_FAN_MODES] = self.fan_modes
data[ClimateEntityCapabilityAttribute.FAN_MODES] = self.fan_modes
if ClimateEntityFeature.PRESET_MODE in supported_features:
data[ATTR_PRESET_MODES] = self.preset_modes
data[ClimateEntityCapabilityAttribute.PRESET_MODES] = self.preset_modes
if ClimateEntityFeature.SWING_MODE in supported_features:
data[ATTR_SWING_MODES] = self.swing_modes
data[ClimateEntityCapabilityAttribute.SWING_MODES] = self.swing_modes
if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
data[ATTR_SWING_HORIZONTAL_MODES] = self.swing_horizontal_modes
data[ClimateEntityCapabilityAttribute.SWING_HORIZONTAL_MODES] = (
self.swing_horizontal_modes
)
return data
@@ -355,13 +367,13 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
hass = self.hass
data: dict[str, str | float | None] = {
ATTR_CURRENT_TEMPERATURE: show_temp(
ClimateEntityStateAttribute.CURRENT_TEMPERATURE: show_temp(
hass, self.current_temperature, temperature_unit, precision
),
}
if ClimateEntityFeature.TARGET_TEMPERATURE in supported_features:
data[ATTR_TEMPERATURE] = show_temp(
data[ClimateEntityStateAttribute.TEMPERATURE] = show_temp(
hass,
self.target_temperature,
temperature_unit,
@@ -369,33 +381,35 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
)
if ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in supported_features:
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
data[ClimateEntityStateAttribute.TARGET_TEMP_HIGH] = show_temp(
hass, self.target_temperature_high, temperature_unit, precision
)
data[ATTR_TARGET_TEMP_LOW] = show_temp(
data[ClimateEntityStateAttribute.TARGET_TEMP_LOW] = show_temp(
hass, self.target_temperature_low, temperature_unit, precision
)
if (current_humidity := self.current_humidity) is not None:
data[ATTR_CURRENT_HUMIDITY] = current_humidity
data[ClimateEntityStateAttribute.CURRENT_HUMIDITY] = current_humidity
if ClimateEntityFeature.TARGET_HUMIDITY in supported_features:
data[ATTR_HUMIDITY] = self.target_humidity
data[ClimateEntityStateAttribute.HUMIDITY] = self.target_humidity
if ClimateEntityFeature.FAN_MODE in supported_features:
data[ATTR_FAN_MODE] = self.fan_mode
data[ClimateEntityStateAttribute.FAN_MODE] = self.fan_mode
if hvac_action := self.hvac_action:
data[ATTR_HVAC_ACTION] = hvac_action
data[ClimateEntityStateAttribute.HVAC_ACTION] = hvac_action
if ClimateEntityFeature.PRESET_MODE in supported_features:
data[ATTR_PRESET_MODE] = self.preset_mode
data[ClimateEntityStateAttribute.PRESET_MODE] = self.preset_mode
if ClimateEntityFeature.SWING_MODE in supported_features:
data[ATTR_SWING_MODE] = self.swing_mode
data[ClimateEntityStateAttribute.SWING_MODE] = self.swing_mode
if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
data[ATTR_SWING_HORIZONTAL_MODE] = self.swing_horizontal_mode
data[ClimateEntityStateAttribute.SWING_HORIZONTAL_MODE] = (
self.swing_horizontal_mode
)
return data
+32
View File
@@ -137,6 +137,38 @@ SERVICE_SET_SWING_HORIZONTAL_MODE = "set_swing_horizontal_mode"
SERVICE_SET_TEMPERATURE = "set_temperature"
class ClimateEntityCapabilityAttribute(StrEnum):
"""Capability attributes for climate entities."""
HVAC_MODES = "hvac_modes"
MIN_TEMP = "min_temp"
MAX_TEMP = "max_temp"
TARGET_TEMP_STEP = "target_temp_step"
MIN_HUMIDITY = "min_humidity"
MAX_HUMIDITY = "max_humidity"
TARGET_HUMIDITY_STEP = "target_humidity_step"
FAN_MODES = "fan_modes"
PRESET_MODES = "preset_modes"
SWING_MODES = "swing_modes"
SWING_HORIZONTAL_MODES = "swing_horizontal_modes"
class ClimateEntityStateAttribute(StrEnum):
"""State attributes for climate entities."""
CURRENT_TEMPERATURE = "current_temperature"
TEMPERATURE = "temperature"
TARGET_TEMP_HIGH = "target_temp_high"
TARGET_TEMP_LOW = "target_temp_low"
CURRENT_HUMIDITY = "current_humidity"
HUMIDITY = "humidity"
FAN_MODE = "fan_mode"
HVAC_ACTION = "hvac_action"
PRESET_MODE = "preset_mode"
SWING_MODE = "swing_mode"
SWING_HORIZONTAL_MODE = "swing_horizontal_mode"
class ClimateEntityFeature(IntFlag):
"""Supported features of the climate entity."""
@@ -2,7 +2,7 @@
"title": "Doorbell",
"triggers": {
"rang": {
"description": "Triggers after one or more doorbells rang.",
"description": "Triggers when one or more doorbells ring.",
"name": "Doorbell rang"
}
}
@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["python-dropbox-api==0.1.3"]
"requirements": ["python-dropbox-api==0.1.4"]
}
@@ -1,5 +1,6 @@
"""Data update coordinator for the Duco integration."""
from contextlib import suppress
from dataclasses import dataclass
import logging
from typing import cast, override
@@ -32,6 +33,7 @@ class DucoData:
nodes: dict[int, Node]
node_actions: NodeListActionItemList
rssi_wifi: int | None
time_filter_remain: int | None
class DucoCoordinator(DataUpdateCoordinator[DucoData]):
@@ -39,6 +41,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
config_entry: DucoConfigEntry
board_info: BoardInfo
_supports_time_filter_remain: bool
def __init__(
self,
@@ -55,6 +58,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
update_interval=SCAN_INTERVAL,
)
self.client = client
self._supports_time_filter_remain = True
@override
async def _async_setup(self) -> None:
@@ -126,8 +130,18 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
else:
rssi_wifi = lan_info.rssi_wifi
# Heat recovery info only backs the optional filter timer sensor, so
# failures on this supplemental endpoint should not make the primary
# node entities unavailable.
time_filter_remain = None
if self._supports_time_filter_remain:
with suppress(DucoError):
time_filter_remain = await self.client.async_get_time_filter_remaining()
self._supports_time_filter_remain = time_filter_remain is not None
return DucoData(
nodes={node.node_id: node for node in nodes},
node_actions=node_actions,
rssi_wifi=rssi_wifi,
time_filter_remain=time_filter_remain,
)
+3
View File
@@ -1,6 +1,9 @@
{
"entity": {
"sensor": {
"filter_remaining": {
"default": "mdi:air-filter"
},
"iaq_co2": {
"default": "mdi:molecule-co2"
},
+54 -25
View File
@@ -19,6 +19,7 @@ from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
@@ -46,6 +47,7 @@ class DucoSensorEntityDescription(SensorEntityDescription):
class DucoBoxSensorEntityDescription(SensorEntityDescription):
"""Duco sensor entity description for box-level diagnostic data."""
supported_fn: Callable[[DucoCoordinator], bool] = lambda _: True
value_fn: Callable[[DucoCoordinator], int | float | None]
@@ -137,6 +139,17 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
)
BOX_SENSOR_DESCRIPTIONS: tuple[DucoBoxSensorEntityDescription, ...] = (
DucoBoxSensorEntityDescription(
key="filter_remaining",
translation_key="filter_remaining",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.DAYS,
suggested_display_precision=0,
supported_fn=lambda coordinator: (
coordinator.data.time_filter_remain is not None
),
value_fn=lambda coordinator: coordinator.data.time_filter_remain,
),
DucoBoxSensorEntityDescription(
key="rssi_wifi",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
@@ -157,10 +170,13 @@ async def async_setup_entry(
"""Set up Duco sensor entities."""
coordinator = entry.runtime_data
# Track the node IDs for which entities have already been created, so we
# can detect both newly added and stale (deregistered) nodes on every
# Track the node IDs for which node entities have already been created, so
# we can detect both newly added and stale (deregistered) nodes on every
# coordinator update.
known_nodes: set[int] = set()
# Track optional box-level sensors separately so they can still be added
# later if their capability probe transiently failed during initial setup.
known_box_sensors: set[tuple[int, str]] = set()
@callback
def _async_add_new_entities() -> None:
@@ -189,34 +205,47 @@ async def async_setup_entry(
remove_config_entry_id=entry.entry_id,
)
known_nodes.difference_update(stale_node_ids)
known_box_sensors.difference_update(
{
description_key
for description_key in known_box_sensors
if description_key[0] in stale_node_ids
}
)
new_entities: list[SensorEntity] = []
for node in coordinator.data.nodes.values():
if node.node_id in known_nodes:
continue
if node.general.node_type == NodeType.UNKNOWN:
# Do not add the node to known_nodes so that it is re-evaluated
# on every coordinator update. This allows entities to be
# created automatically once a firmware update or library
# update adds support for the device type.
_LOGGER.debug(
"Duco node %s (%s) has an unsupported device type and will be "
"retried on subsequent coordinator updates",
node.node_id,
node.general.name,
if node.node_id not in known_nodes:
if node.general.node_type == NodeType.UNKNOWN:
# Do not add the node to known_nodes so that it is re-evaluated
# on every coordinator update. This allows entities to be
# created automatically once a firmware update or library
# update adds support for the device type.
_LOGGER.debug(
"Duco node %s (%s) has an unsupported device type and will be "
"retried on subsequent coordinator updates",
node.node_id,
node.general.name,
)
continue
known_nodes.add(node.node_id)
new_entities.extend(
DucoSensorEntity(coordinator, node, description)
for description in SENSOR_DESCRIPTIONS
if node.general.node_type in description.node_types
)
if node.general.node_type != NodeType.BOX:
continue
known_nodes.add(node.node_id)
new_entities.extend(
DucoSensorEntity(coordinator, node, description)
for description in SENSOR_DESCRIPTIONS
if node.general.node_type in description.node_types
)
new_entities.extend(
DucoBoxSensorEntity(coordinator, node, description)
for description in BOX_SENSOR_DESCRIPTIONS
if node.general.node_type == NodeType.BOX
)
for description in BOX_SENSOR_DESCRIPTIONS:
description_key = (node.node_id, description.key)
if description_key in known_box_sensors:
continue
if not description.supported_fn(coordinator):
continue
known_box_sensors.add(description_key)
new_entities.append(DucoBoxSensorEntity(coordinator, node, description))
if new_entities:
async_add_entities(new_entities)
@@ -73,6 +73,9 @@
}
},
"sensor": {
"filter_remaining": {
"name": "Filter remaining"
},
"iaq_co2": {
"name": "CO2 air quality index"
},
@@ -1 +0,0 @@
"""The eliqonline component."""
@@ -1,9 +0,0 @@
{
"domain": "eliqonline",
"name": "Eliqonline",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/eliqonline",
"iot_class": "cloud_polling",
"quality_scale": "legacy",
"requirements": ["eliqonline==1.2.2"]
}
@@ -1,85 +0,0 @@
"""Monitors home energy use for the ELIQ Online service."""
from datetime import timedelta
import logging
import eliqonline
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
CONF_CHANNEL_ID = "channel_id"
DEFAULT_NAME = "ELIQ Online"
SCAN_INTERVAL = timedelta(seconds=60)
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Required(CONF_CHANNEL_ID): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the ELIQ Online sensor."""
access_token = config.get(CONF_ACCESS_TOKEN)
name = config.get(CONF_NAME, DEFAULT_NAME)
channel_id = config.get(CONF_CHANNEL_ID)
session = async_get_clientsession(hass)
api = eliqonline.API(session=session, access_token=access_token)
try:
_LOGGER.debug("Probing for access to ELIQ Online API")
await api.get_data_now(channelid=channel_id)
except OSError as error:
_LOGGER.error("Could not access the ELIQ Online API: %s", error)
return
async_add_entities([EliqSensor(api, channel_id, name)], True)
class EliqSensor(SensorEntity):
"""Implementation of an ELIQ Online sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, api, channel_id, name):
"""Initialize the sensor."""
self._attr_name = name
self._api = api
self._channel_id = channel_id
async def async_update(self) -> None:
"""Get the latest data."""
try:
response = await self._api.get_data_now(channelid=self._channel_id)
self._attr_native_value = int(response["power"])
_LOGGER.debug("Updated power from server %d W", self.native_value)
except KeyError:
_LOGGER.warning("Invalid response from ELIQ Online API")
except (OSError, TimeoutError) as error:
_LOGGER.warning("Could not connect to the ELIQ Online API: %s", error)
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/energieleser",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["energieleser==0.1.4"],
"zeroconf": [
{
@@ -28,8 +28,10 @@ rules:
status: exempt
comment: Integration has no custom actions.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
docs-configuration-parameters:
status: exempt
comment: Integration has no options flow or post-install configuration parameters.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
@@ -144,5 +144,5 @@
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["govee-ble==1.2.0"]
"requirements": ["govee-ble==1.4.0"]
}
@@ -1,5 +1,7 @@
"""Select platform for Qube Heat Pump."""
from typing import override
from homeassistant.components.select import SelectEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -41,15 +43,18 @@ class QubeSGReadySelect(QubeEntity, SelectEntity):
self._attr_unique_id = f"{entry.entry_id}-sg_ready_mode"
@property
@override
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.coordinator.data.sg_ready_mode is not None
@property
@override
def current_option(self) -> str | None:
"""Return the current SG Ready mode."""
return self.coordinator.data.sg_ready_mode
@override
async def async_select_option(self, option: str) -> None:
"""Set the SG Ready mode."""
try:
+84 -14
View File
@@ -1,19 +1,24 @@
"""Support for KNX button entities."""
from typing import override
from typing import Any, override
from xknx.devices import RawValue as XknxRawValue
from xknx.devices import ExposeSensor as XknxExposeSensor, RawValue as XknxRawValue
from homeassistant import config_entries
from homeassistant.components.button import ButtonEntity
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_PAYLOAD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.typing import ConfigType
from .const import CONF_PAYLOAD_LENGTH, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .const import CONF_PAYLOAD_LENGTH, CONF_VALUE, DOMAIN, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .storage.const import CONF_DATA, CONF_ENTITY, CONF_GA_SEND
from .storage.util import ConfigExtractor
async def async_setup_entry(
@@ -21,27 +26,60 @@ async def async_setup_entry(
config_entry: config_entries.ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the KNX binary sensor platform."""
"""Set up button(s) for KNX platform."""
knx_module = hass.data[KNX_MODULE_KEY]
config: list[ConfigType] = knx_module.config_yaml[Platform.BUTTON]
platform = async_get_current_platform()
knx_module.config_store.add_platform(
platform=Platform.BUTTON,
controller=KnxUiEntityPlatformController(
knx_module=knx_module,
entity_platform=platform,
entity_class=KnxUiButton,
),
)
async_add_entities(KNXButton(knx_module, entity_config) for entity_config in config)
entities: list[KnxYamlEntity | KnxUiEntity] = []
if yaml_platform_config := knx_module.config_yaml.get(Platform.BUTTON):
entities.extend(
KnxYamlButton(knx_module, entity_config)
for entity_config in yaml_platform_config
)
if ui_config := knx_module.config_store.data["entities"].get(Platform.BUTTON):
entities.extend(
KnxUiButton(knx_module, unique_id, config)
for unique_id, config in ui_config.items()
)
if entities:
async_add_entities(entities)
class KNXButton(KnxYamlEntity, ButtonEntity):
class _KnxButton(ButtonEntity):
"""Representation of a KNX button."""
_device: XknxRawValue | XknxExposeSensor
_payload: Any
@override
async def async_press(self) -> None:
"""Press the button."""
await self._device.set(self._payload)
class KnxYamlButton(_KnxButton, KnxYamlEntity):
"""Representation of a KNX button configured via YAML."""
_device: XknxRawValue
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX button."""
# dpt-value to payload conversion is done in schema validation for yaml config
self._payload = config[CONF_PAYLOAD]
self._device = XknxRawValue(
xknx=knx_module.xknx,
name=config[CONF_NAME],
payload_length=config[CONF_PAYLOAD_LENGTH],
group_address=config[KNX_ADDRESS],
)
self._payload = config[CONF_PAYLOAD]
super().__init__(
knx_module=knx_module,
unique_id=f"{self._device.remote_value.group_address}_{self._payload}",
@@ -49,7 +87,39 @@ class KNXButton(KnxYamlEntity, ButtonEntity):
entity_category=config.get(CONF_ENTITY_CATEGORY),
)
@override
async def async_press(self) -> None:
"""Press the button."""
await self._device.set(self._payload)
class KnxUiButton(_KnxButton, KnxUiEntity):
"""Representation of a KNX button configured via the UI."""
_device: XknxRawValue | XknxExposeSensor
def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
) -> None:
"""Initialize a KNX button."""
knx_conf = ConfigExtractor(config[DOMAIN])
button_data = knx_conf.get(CONF_DATA)
if CONF_PAYLOAD in button_data and CONF_PAYLOAD_LENGTH in button_data:
self._payload = int(button_data[CONF_PAYLOAD], 16)
self._device = XknxRawValue(
xknx=knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
payload_length=button_data[CONF_PAYLOAD_LENGTH],
group_address=knx_conf.get_write(CONF_GA_SEND),
)
else:
dpt_string = knx_conf.get_dpt(CONF_GA_SEND)
self._payload = button_data[CONF_VALUE]
self._device = XknxExposeSensor(
xknx=knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
value_type=dpt_string,
group_address=knx_conf.get_write(CONF_GA_SEND),
respond_to_read=False,
)
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
+2
View File
@@ -26,6 +26,7 @@ KNX_ADDRESS: Final = "address"
CONF_INVERT: Final = "invert"
CONF_KNX_EXPOSE: Final = "expose"
CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address"
CONF_VALUE: Final = "value"
##
# Connection constants
@@ -178,6 +179,7 @@ SUPPORTED_PLATFORMS_YAML: Final = {
SUPPORTED_PLATFORMS_UI: Final = {
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.DATE,
+42 -4
View File
@@ -2,9 +2,9 @@
from collections.abc import Mapping
from functools import cache
from typing import Literal, TypedDict
from typing import Literal, NotRequired, TypedDict, cast
from xknx.dpt import DPTBase, DPTComplex, DPTEnum, DPTNumeric
from xknx.dpt import DPTBase, DPTComplex, DPTComplexFieldSchema, DPTEnum, DPTNumeric
from xknx.dpt.dpt_16 import DPTString
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
@@ -24,15 +24,28 @@ class DPTInfo(TypedDict):
sensor_device_class: SensorDeviceClass | None
sensor_state_class: SensorStateClass | None
payload_length: int
# numeric specific
min: NotRequired[float]
max: NotRequired[float]
step: NotRequired[float]
# enum specific
options: NotRequired[list[str]]
# complex specific
schema: NotRequired[list[DPTComplexFieldSchema]]
@cache
def get_supported_dpts() -> Mapping[str, DPTInfo]:
"""Return a mapping of supported DPTs with HA specific attributes."""
dpts = {}
dpts: dict[str, DPTInfo] = {}
for dpt_class in DPTBase.dpt_class_tree():
dpt_number_str = dpt_class.dpt_number_str()
ha_dpt_class = _ha_dpt_class(dpt_class)
dpts[dpt_number_str] = DPTInfo(
info = DPTInfo(
dpt_class=ha_dpt_class,
main=dpt_class.dpt_main_number, # type: ignore[typeddict-item] # checked in xknx unit tests
sub=dpt_class.dpt_sub_number,
@@ -40,7 +53,15 @@ def get_supported_dpts() -> Mapping[str, DPTInfo]:
unit=_sensor_unit_overrides.get(dpt_number_str, dpt_class.unit),
sensor_device_class=_sensor_device_classes.get(dpt_number_str),
sensor_state_class=_get_sensor_state_class(ha_dpt_class, dpt_number_str),
payload_length=dpt_class.payload_length,
)
if ha_dpt_class == "numeric":
_add_numeric_details(info, cast(type[DPTNumeric], dpt_class))
elif ha_dpt_class == "enum":
_add_enum_details(info, cast(type[DPTEnum], dpt_class))
elif ha_dpt_class == "complex":
_add_complex_details(info, cast(type[DPTComplex], dpt_class))
dpts[dpt_number_str] = info
return dpts
@@ -57,6 +78,23 @@ def _ha_dpt_class(dpt_cls: type[DPTBase]) -> HaDptClass:
raise ValueError("Unsupported DPT class")
def _add_numeric_details(dpt_info: DPTInfo, dpt_cls: type[DPTNumeric]) -> None:
"""Add numeric specific details to the DPTInfo."""
dpt_info["min"] = dpt_cls.value_min
dpt_info["max"] = dpt_cls.value_max
dpt_info["step"] = dpt_cls.resolution
def _add_enum_details(dpt_info: DPTInfo, dpt_cls: type[DPTEnum]) -> None:
"""Add enum specific details to the DPTInfo."""
dpt_info["options"] = [o.name.lower() for o in dpt_cls.get_valid_values()]
def _add_complex_details(dpt_info: DPTInfo, dpt_cls: type[DPTComplex]) -> None:
"""Add complex specific details to the DPTInfo."""
dpt_info["schema"] = dpt_cls.get_dict_schema()
_sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"7.011": SensorDeviceClass.DISTANCE,
"7.012": SensorDeviceClass.CURRENT,
+6 -4
View File
@@ -57,6 +57,7 @@ from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
CONF_SYNC_STATE,
CONF_VALUE,
KNX_ADDRESS,
ClimateConf,
ColorTempModes,
@@ -98,9 +99,12 @@ def _max_payload_value(payload_length: int) -> int:
def button_payload_sub_validator(entity_config: OrderedDict) -> OrderedDict:
"""Validate a button entity payload configuration."""
"""Validate a button entity payload configuration.
Returns raw payload and length from value and type (DPT), if given.
"""
if _type := entity_config.get(CONF_TYPE):
_payload = entity_config[ButtonSchema.CONF_VALUE]
_payload = entity_config[CONF_VALUE]
if (transcoder := DPTBase.parse_transcoder(_type)) is None:
raise vol.Invalid(f"'type: {_type}' is not a valid sensor type.")
entity_config[CONF_PAYLOAD_LENGTH] = transcoder.payload_length
@@ -234,8 +238,6 @@ class ButtonSchema(KNXPlatformSchema):
PLATFORM = Platform.BUTTON
CONF_VALUE = "value"
payload_or_value_msg = f"Please use only one of `{CONF_PAYLOAD}` or `{CONF_VALUE}`"
length_or_type_msg = (
f"Please use only one of `{CONF_PAYLOAD_LENGTH}` or `{CONF_TYPE}`"
@@ -19,6 +19,9 @@ CONF_GA_TIME: Final = "ga_time"
CONF_GA_STEP: Final = "ga_step"
# Button
CONF_GA_SEND: Final = "ga_send"
# Climate
CONF_GA_TEMPERATURE_CURRENT: Final = "ga_temperature_current"
CONF_GA_HUMIDITY_CURRENT: Final = "ga_humidity_current"
@@ -3,7 +3,8 @@
from enum import StrEnum, unique
import voluptuous as vol
from xknx.dpt import DPTNumeric
from xknx.dpt import DPTBase, DPTBinary, DPTNumeric
from xknx.exceptions import ConversionError
from homeassistant.components.climate import HVACMode
from homeassistant.components.number import (
@@ -36,9 +37,11 @@ from ..const import (
CONF_CONTEXT_TIMEOUT,
CONF_IGNORE_INTERNAL_STATE,
CONF_INVERT,
CONF_PAYLOAD_LENGTH,
CONF_RESET_AFTER,
CONF_RESPOND_TO_READ,
CONF_SYNC_STATE,
CONF_VALUE,
DOMAIN,
SUPPORTED_PLATFORMS_UI,
ClimateConf,
@@ -92,6 +95,7 @@ from .const import (
CONF_GA_RED_SWITCH,
CONF_GA_SATURATION,
CONF_GA_SCENE,
CONF_GA_SEND,
CONF_GA_SENSOR,
CONF_GA_SETPOINT_SHIFT,
CONF_GA_SPEED,
@@ -115,6 +119,7 @@ from .knx_selector import (
GASelector,
GroupSelect,
GroupSelectOption,
KnxPayloadSelector,
KNXSectionFlat,
SyncStateSelector,
)
@@ -169,6 +174,55 @@ BINARY_SENSOR_KNX_SCHEMA = vol.Schema(
},
)
def _button_data_sub_validator(config: dict) -> dict:
"""Validate data matching configured DPT."""
dpt = config[CONF_GA_SEND].get(CONF_DPT)
transcoder = None
if dpt:
transcoder = DPTBase.parse_transcoder(dpt)
assert transcoder is not None # already checked by GASelector
if CONF_VALUE in config[CONF_DATA]:
try:
transcoder.to_knx(config[CONF_DATA][CONF_VALUE])
except ConversionError as ex:
raise vol.Invalid(
f"Value invalid for DPT {transcoder.dpt_number_str()}",
path=([CONF_DATA]),
) from ex
elif CONF_PAYLOAD_LENGTH in config[CONF_DATA]:
length = config[CONF_DATA][CONF_PAYLOAD_LENGTH]
if length != transcoder.payload_length or (
length != 0 and transcoder.payload_type is DPTBinary
):
raise vol.Invalid(
f"Payload length invalid for DPT {transcoder.dpt_number_str()}",
path=([CONF_DATA]),
)
return config
# without DPT only raw allowed -> payload + payload_length (checked by KnxPayloadSelector)
if CONF_PAYLOAD_LENGTH in config[CONF_DATA]:
return config
raise vol.Invalid("Invalid configuration for button entity")
BUTTON_KNX_SCHEMA = AllSerializeFirst(
vol.Schema(
{
vol.Required(CONF_GA_SEND): GASelector(
state=False,
write_required=True,
passive=False,
dpt=["numeric", "enum", "complex", "string"],
dpt_required=False, # for raw payload support
),
vol.Required(CONF_DATA): KnxPayloadSelector(ga_path=CONF_GA_SEND),
},
),
_button_data_sub_validator,
)
COVER_KNX_SCHEMA = AllSerializeFirst(
vol.Schema(
{
@@ -741,6 +795,7 @@ SENSOR_KNX_SCHEMA = AllSerializeFirst(
KNX_SCHEMA_FOR_PLATFORM = {
Platform.BINARY_SENSOR: BINARY_SENSOR_KNX_SCHEMA,
Platform.BUTTON: BUTTON_KNX_SCHEMA,
Platform.CLIMATE: CLIMATE_KNX_SCHEMA,
Platform.COVER: COVER_KNX_SCHEMA,
Platform.DATE: DATE_KNX_SCHEMA,
@@ -6,6 +6,9 @@ from typing import Any, override
import voluptuous as vol
from homeassistant.const import CONF_PAYLOAD
from ..const import CONF_PAYLOAD_LENGTH, CONF_VALUE
from ..dpt import HaDptClass, get_supported_dpts
from ..validation import ga_validator, maybe_ga_validator, sync_state_validator
from .const import CONF_DPT, CONF_GA_PASSIVE, CONF_GA_STATE, CONF_GA_WRITE
@@ -159,7 +162,11 @@ class GroupSelect(KNXSelectorBase):
class GASelector(KNXSelectorBase):
"""Selector for a KNX group address structure."""
"""Selector for a KNX group address structure.
`dpt_required` optional dpt only apply to dpt-class lists, enums are always required.
`valid_dpt` is used in frontend to filter dropdown menu - no validation is done.
"""
selector_type = "knx_group_address"
@@ -171,6 +178,7 @@ class GASelector(KNXSelectorBase):
write_required: bool = False,
state_required: bool = False,
dpt: type[Enum] | list[HaDptClass] | None = None,
dpt_required: bool = True,
valid_dpt: str | Iterable[str] | None = None,
) -> None:
"""Initialize the group address selector."""
@@ -180,7 +188,7 @@ class GASelector(KNXSelectorBase):
self.write_required = write_required
self.state_required = state_required
self.dpt = dpt
# valid_dpt is used in frontend to filter dropdown menu - no validation is done
self.dpt_required = dpt_required
self.valid_dpt = (valid_dpt,) if isinstance(valid_dpt, str) else valid_dpt
self.schema = self.build_schema()
@@ -196,6 +204,7 @@ class GASelector(KNXSelectorBase):
}
if self.dpt is not None:
if isinstance(self.dpt, list):
# optional / required is not passed to FE - only validated in BE
options["dptClasses"] = self.dpt
else:
options["dptSelect"] = [
@@ -267,7 +276,8 @@ class GASelector(KNXSelectorBase):
"""Add DPT validator to the schema."""
if self.dpt is not None:
if isinstance(self.dpt, list):
schema[vol.Required(CONF_DPT)] = vol.In(get_supported_dpts())
marker = vol.Required if self.dpt_required else vol.Optional
schema[marker(CONF_DPT)] = vol.In(get_supported_dpts())
else:
schema[vol.Required(CONF_DPT)] = vol.In(
{item.value for item in self.dpt}
@@ -300,3 +310,64 @@ class SyncStateSelector(KNXSelectorBase):
if not self.allow_false and not data:
raise vol.Invalid(f"Sync state cannot be {data}")
return self.schema(data)
class KnxPayloadSelector(KNXSelectorBase):
"""Selector for KNX payload configuration.
Raw payloads are stored as hex strings.
"""
schema = vol.Any(
{
vol.Required(CONF_VALUE): object,
},
{
vol.Required(CONF_PAYLOAD): str,
vol.Required(CONF_PAYLOAD_LENGTH): vol.All(int, vol.Range(min=0, max=14)),
},
)
selector_type = "knx_payload"
def __init__(self, ga_path: str) -> None:
"""Initialize the KNX payload selector."""
self.ga_path = ga_path
@override
def serialize(self) -> dict[str, Any]:
"""Serialize the selector to a dictionary."""
return {
"type": self.selector_type,
"ga_path": self.ga_path,
}
@override
def __call__(self, data: Any) -> Any:
"""Validate the passed data."""
validated = self.schema(data)
if CONF_PAYLOAD in validated and CONF_PAYLOAD_LENGTH in validated:
payload = validated[CONF_PAYLOAD]
payload_length = validated[CONF_PAYLOAD_LENGTH]
try:
int_payload = int(payload, 16)
except ValueError as ex:
raise vol.Invalid(f"Invalid payload format: {payload}") from ex
validated[CONF_PAYLOAD] = hex(int_payload) # prepends "0x" if not present
if int_payload < 0:
raise vol.Invalid(f"Payload cannot be negative: {payload}")
if payload_length == 0:
# DPT 1,2,3 is marked length 0, has 6 bit size
if int_payload > 63:
raise vol.Invalid(
f"Payload exceeds DPT 1,2,3 limit of 0x3f (63): {payload}"
)
else:
max_payload = (1 << (payload_length * 8)) - 1
if int_payload > max_payload:
raise vol.Invalid(
f"Payload {payload} exceeds possible maximum for "
f"length {payload_length}: {hex(max_payload)}"
)
# CONF_VALUE branch needs subvalidator as we don't have the DPT available here
return validated
+27 -1
View File
@@ -453,6 +453,19 @@
}
}
},
"button": {
"description": "Entity for sending predefined values.",
"knx": {
"data": {
"description": "The value sent when the button is pressed. The format of the value depends on the DPT of the configured address.",
"label": "Data"
},
"ga_send": {
"description": "Group address the value is sent to.",
"label": "Address"
}
}
},
"climate": {
"description": "The KNX climate platform is used as an interface to heating actuators, HVAC gateways, etc.",
"knx": {
@@ -991,7 +1004,7 @@
"label": "[%key:component::knx::config_panel::entities::create::_::knx::respond_to_read::label%]"
},
"section_advanced_options": {
"title": "Advanced options"
"title": "Additional settings"
},
"show_raw_values": "Show raw values",
"title": "Add exposure",
@@ -1014,6 +1027,19 @@
"project": {
"description": "Inspect imported group addresses",
"title": "Project"
},
"selectors": {
"knx-payload-selector": {
"dpt_missing": "No DPT selected Typed mode not available",
"mode": {
"label": "Payload format",
"raw": "Raw payload",
"typed": "Typed value"
},
"raw_length": "Payload length",
"raw_length_description": "Length of the raw payload in bytes. For DPT 1, 2 and 3 use `0`.",
"raw_payload": "Raw payload"
}
}
},
"device_automation": {
@@ -96,7 +96,7 @@
"title": "Lawn mower",
"triggers": {
"errored": {
"description": "Triggers after one or more lawn mowers encounter an error.",
"description": "Triggers when one or more lawn mowers encounter an error.",
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
@@ -108,7 +108,7 @@
"name": "Lawn mower encountered an error"
},
"paused_mowing": {
"description": "Triggers after one or more lawn mowers pause mowing.",
"description": "Triggers when one or more lawn mowers pause mowing.",
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
@@ -120,7 +120,7 @@
"name": "Lawn mower paused mowing"
},
"returned_to_dock": {
"description": "Triggers after one or more lawn mowers have returned to dock.",
"description": "Triggers when one or more lawn mowers have returned to dock.",
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
@@ -132,7 +132,7 @@
"name": "Lawn mower returned to dock"
},
"started_mowing": {
"description": "Triggers after one or more lawn mowers start mowing.",
"description": "Triggers when one or more lawn mowers start mowing.",
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
@@ -144,7 +144,7 @@
"name": "Lawn mower started mowing"
},
"started_returning": {
"description": "Triggers after one or more lawn mowers start returning to dock.",
"description": "Triggers when one or more lawn mowers start returning to dock.",
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["thinqconnect"],
"requirements": ["thinqconnect==1.0.12"]
"requirements": ["thinqconnect==1.0.13"]
}
@@ -370,7 +370,7 @@
"name": "Media"
}
},
"name": "Play media"
"name": "Play specified media"
},
"repeat_set": {
"description": "Sets the repeat mode of a media player.",
@@ -11,7 +11,10 @@
}
},
"holiday_mode": {
"default": "mdi:beach"
"default": "mdi:beach",
"state": {
"off": "mdi:home"
}
},
"overheat_protection": {
"default": "mdi:fire",
@@ -20,7 +23,10 @@
}
},
"standby": {
"default": "mdi:power-sleep"
"default": "mdi:power-sleep",
"state": {
"off": "mdi:power"
}
}
},
"sensor": {
@@ -2,7 +2,7 @@
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from typing import Any, override
from aiomelcloudhome import ATAUnit, ATWUnit, MELCloudHome
from aiomelcloudhome.exceptions import (
@@ -215,15 +215,18 @@ class ATASwitch(MelCloudHomeATAUnitEntity, SwitchEntity):
self._attr_unique_id = f"{unit.id}_{entity_description.key}"
@property
@override
def available(self) -> bool:
"""Return if the entity is available."""
return super().available and self.entity_description.available_fn(self.unit)
@property
@override
def is_on(self) -> bool | None:
"""Return the state of the switch."""
return self.entity_description.is_on_fn(self.unit)
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable the protection."""
await _perform_action(
@@ -231,6 +234,7 @@ class ATASwitch(MelCloudHomeATAUnitEntity, SwitchEntity):
self.entity_description.turn_on_fn(self.coordinator.client, self.unit),
)
@override
async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable the protection."""
await _perform_action(
@@ -256,15 +260,18 @@ class ATWSwitch(MelCloudHomeATWUnitEntity, SwitchEntity):
self._attr_unique_id = f"{unit.id}_{entity_description.key}"
@property
@override
def available(self) -> bool:
"""Return if the entity is available."""
return super().available and self.entity_description.available_fn(self.unit)
@property
@override
def is_on(self) -> bool | None:
"""Return the state of the switch."""
return self.entity_description.is_on_fn(self.unit)
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable the protection."""
await _perform_action(
@@ -272,6 +279,7 @@ class ATWSwitch(MelCloudHomeATWUnitEntity, SwitchEntity):
self.entity_description.turn_on_fn(self.coordinator.client, self.unit),
)
@override
async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable the protection."""
await _perform_action(
@@ -51,7 +51,7 @@
"title": "Moisture",
"triggers": {
"changed": {
"description": "Triggers after one or more moisture content values change.",
"description": "Triggers when one or more moisture content values change.",
"fields": {
"threshold": {
"name": "[%key:component::moisture::common::trigger_threshold_name%]"
@@ -60,7 +60,7 @@
"name": "Moisture content changed"
},
"cleared": {
"description": "Triggers after one or more moisture sensors stop detecting moisture.",
"description": "Triggers when one or more moisture sensors stop detecting moisture.",
"fields": {
"behavior": {
"name": "[%key:component::moisture::common::trigger_behavior_name%]"
@@ -72,7 +72,7 @@
"name": "Moisture cleared"
},
"crossed_threshold": {
"description": "Triggers after one or more moisture content values cross a threshold.",
"description": "Triggers when one or more moisture content values cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::moisture::common::trigger_behavior_name%]"
@@ -87,7 +87,7 @@
"name": "Moisture content crossed threshold"
},
"detected": {
"description": "Triggers after one or more moisture sensors start detecting moisture.",
"description": "Triggers when one or more moisture sensors start detecting moisture.",
"fields": {
"behavior": {
"name": "[%key:component::moisture::common::trigger_behavior_name%]"
+2 -2
View File
@@ -34,7 +34,7 @@
"title": "Motion",
"triggers": {
"cleared": {
"description": "Triggers after one or more motion sensors stop detecting motion.",
"description": "Triggers when one or more motion sensors stop detecting motion.",
"fields": {
"behavior": {
"name": "[%key:component::motion::common::trigger_behavior_name%]"
@@ -46,7 +46,7 @@
"name": "Motion cleared"
},
"detected": {
"description": "Triggers after one or more motion sensors start detecting motion.",
"description": "Triggers when one or more motion sensors start detecting motion.",
"fields": {
"behavior": {
"name": "[%key:component::motion::common::trigger_behavior_name%]"
@@ -83,6 +83,7 @@ class NFAndroidTVNotifyEntity(NotifyEntity):
self.entry = entry
self.client = entry.runtime_data
@override
def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message via notify.send_message action."""
try:
@@ -34,7 +34,7 @@
"title": "Occupancy",
"triggers": {
"cleared": {
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
"description": "Triggers when one or more occupancy sensors stop detecting occupancy.",
"fields": {
"behavior": {
"name": "[%key:component::occupancy::common::trigger_behavior_name%]"
@@ -46,7 +46,7 @@
"name": "Occupancy cleared"
},
"detected": {
"description": "Triggers after one or more occupancy sensors start detecting occupancy.",
"description": "Triggers when one or more occupancy sensors start detecting occupancy.",
"fields": {
"behavior": {
"name": "[%key:component::occupancy::common::trigger_behavior_name%]"
+2 -1
View File
@@ -2,7 +2,7 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from typing import Any, override
from openevsehttp.__main__ import OpenEVSE
@@ -91,6 +91,7 @@ class OpenEVSEButton(CoordinatorEntity[OpenEVSEDataUpdateCoordinator], ButtonEnt
}
self._attr_device_info[ATTR_SERIAL_NUMBER] = unique_id
@override
async def async_press(self) -> None:
"""Press the button."""
with openevse_exception_handler(0.0):
@@ -5,7 +5,7 @@ from typing import Any, override
from ouman_eh_800_api import (
EnumControlOumanEndpoint,
IntControlOumanEndpoint,
FloatControlOumanEndpoint,
L1BaseEndpoints,
L1RoomSensor,
L2BaseEndpoints,
@@ -57,7 +57,7 @@ class OumanEh800ClimateEntityDescription(
operation_mode_endpoint: EnumControlOumanEndpoint
current_temperature_endpoint: NumberOumanEndpoint
target_temperature_endpoint: IntControlOumanEndpoint
target_temperature_endpoint: FloatControlOumanEndpoint
valve_position_endpoint: NumberOumanEndpoint
@@ -104,7 +104,7 @@ class OumanEh800ClimateEntity(OumanEh800Entity, ClimateEntity):
_attr_name = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = 1
_attr_target_temperature_step = 0.1
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_preset_modes = list(_PRESET_TO_OPERATION_MODE)
_attr_supported_features = (
@@ -187,7 +187,7 @@ class OumanEh800ClimateEntity(OumanEh800Entity, ClimateEntity):
await self.async_set_hvac_mode(hvac_mode)
await self.coordinator.async_set_endpoint_value(
self.entity_description.target_temperature_endpoint,
int(kwargs[ATTR_TEMPERATURE]),
kwargs[ATTR_TEMPERATURE],
)
@override
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["ouman-eh-800-api==0.5.0"]
"requirements": ["ouman-eh-800-api==1.0.0"]
}
@@ -2,6 +2,7 @@
from typing import Any
from aiohttp import ClientConnectorError, ServerDisconnectedError
from pyoverkiz.enums import OverkizCommand
from pyoverkiz.exceptions import BaseOverkizError
from pyoverkiz.models import Action, Command, Device, StateDefinition
@@ -63,6 +64,12 @@ class OverkizExecutor:
# Catch Overkiz exceptions to support `continue_on_error` functionality
except BaseOverkizError as exception:
raise HomeAssistantError(exception) from exception
except (
TimeoutError,
ClientConnectorError,
ServerDisconnectedError,
) as exception:
raise HomeAssistantError("Failed to connect") from exception
# ExecutionRegisteredEvent doesn't contain the
# device_url, thus we need to register it here
@@ -96,6 +103,12 @@ class OverkizExecutor:
# Catch Overkiz exceptions to support `continue_on_error` functionality
except BaseOverkizError as exception:
raise HomeAssistantError(exception) from exception
except (
TimeoutError,
ClientConnectorError,
ServerDisconnectedError,
) as exception:
raise HomeAssistantError("Failed to connect") from exception
self.coordinator.executions[exec_id] = {
"device_url": self.device.device_url,
+5 -2
View File
@@ -25,6 +25,7 @@ from .const import (
SERVICE_SELECT_LAST,
SERVICE_SELECT_NEXT,
SERVICE_SELECT_PREVIOUS,
SelectEntityCapabilityAttribute,
)
_LOGGER = logging.getLogger(__name__)
@@ -121,7 +122,9 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Representation of a Select entity."""
_entity_component_unrecorded_attributes = frozenset({ATTR_OPTIONS})
_entity_component_unrecorded_attributes = frozenset(
{SelectEntityCapabilityAttribute.OPTIONS}
)
entity_description: SelectEntityDescription
_attr_current_option: str | None = None
@@ -133,7 +136,7 @@ class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
return {
ATTR_OPTIONS: self.options,
SelectEntityCapabilityAttribute.OPTIONS: self.options,
}
@property
+9
View File
@@ -1,7 +1,16 @@
"""Provides the constants needed for the component."""
from enum import StrEnum
DOMAIN = "select"
class SelectEntityCapabilityAttribute(StrEnum):
"""Capability attributes for select entities."""
OPTIONS = "options"
ATTR_CYCLE = "cycle"
ATTR_OPTIONS = "options"
+26
View File
@@ -35,5 +35,31 @@
"default": "mdi:theme-light-dark"
}
}
},
"triggers": {
"dawn": {
"trigger": "mdi:weather-sunset-up"
},
"dusk": {
"trigger": "mdi:weather-sunset-down"
},
"elevation_changed": {
"trigger": "mdi:sun-angle"
},
"elevation_crossed_threshold": {
"trigger": "mdi:sun-angle"
},
"solar_midnight": {
"trigger": "mdi:weather-night"
},
"solar_noon": {
"trigger": "mdi:weather-sunny"
},
"sunrise": {
"trigger": "mdi:weather-sunset-up"
},
"sunset": {
"trigger": "mdi:weather-sunset-down"
}
}
}
+75 -1
View File
@@ -1,4 +1,10 @@
{
"common": {
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type",
"twilight_type_description": "The phase of twilight.",
"twilight_type_name": "Twilight type"
},
"config": {
"step": {
"user": {
@@ -36,5 +42,73 @@
}
}
},
"title": "Sun"
"selector": {
"twilight_type": {
"options": {
"astronomical": "Astronomical",
"civil": "Civil",
"nautical": "Nautical"
}
}
},
"title": "Sun",
"triggers": {
"dawn": {
"description": "Triggers at dawn, when civil, nautical, or astronomical twilight begins.",
"fields": {
"type": {
"description": "[%key:component::sun::common::twilight_type_description%]",
"name": "[%key:component::sun::common::twilight_type_name%]"
}
},
"name": "Dawn"
},
"dusk": {
"description": "Triggers at dusk, when civil, nautical, or astronomical twilight ends.",
"fields": {
"type": {
"description": "[%key:component::sun::common::twilight_type_description%]",
"name": "[%key:component::sun::common::twilight_type_name%]"
}
},
"name": "Dusk"
},
"elevation_changed": {
"description": "Triggers whenever the sun's elevation changes, optionally limited to a threshold or range you set.",
"fields": {
"threshold": {
"name": "[%key:component::sun::common::trigger_threshold_name%]"
}
},
"name": "Sun elevation changed"
},
"elevation_crossed_threshold": {
"description": "Triggers when the sun's elevation crosses a threshold you set.",
"fields": {
"for": {
"name": "[%key:component::sun::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::sun::common::trigger_threshold_name%]"
}
},
"name": "Sun elevation crossed threshold"
},
"solar_midnight": {
"description": "Triggers when the sun reaches its lowest point.",
"name": "Solar midnight"
},
"solar_noon": {
"description": "Triggers when the sun reaches its highest point.",
"name": "Solar noon"
},
"sunrise": {
"description": "Triggers when the sun rises.",
"name": "Sunrise"
},
"sunset": {
"description": "Triggers when the sun sets.",
"name": "Sunset"
}
}
}
+309 -41
View File
@@ -1,61 +1,329 @@
"""Offer sun based automation rules."""
"""Provides triggers for the sun."""
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Any, cast, override
import astral
import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_EVENT,
CONF_FOR,
CONF_OFFSET,
CONF_PLATFORM,
CONF_OPTIONS,
CONF_TYPE,
DEGREE,
EVENT_CORE_CONFIG_UPDATE,
SUN_EVENT_SUNRISE,
SUN_EVENT_SUNSET,
)
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.automation import (
DomainSpec,
move_top_level_schema_fields_to_options,
)
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.selector import (
NumericThresholdMode,
NumericThresholdSelector,
NumericThresholdSelectorConfig,
)
from homeassistant.helpers.sun import (
get_astral_event_next,
get_astral_observer,
get_observer_astral_event_next,
)
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerBase,
Trigger,
TriggerActionRunner,
TriggerConfig,
TriggerNotTriggeredReporter,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
from .const import DOMAIN, STATE_ATTR_ELEVATION
# Names of solar events supported by the astral.sun module
_SUN_EVENT_SOLAR_NOON = "noon"
_SUN_EVENT_SOLAR_MIDNIGHT = "midnight"
_SUN_EVENT_DAWN = "dawn"
_SUN_EVENT_DUSK = "dusk"
_TWILIGHT_CIVIL = "civil"
_TWILIGHT_NAUTICAL = "nautical"
_TWILIGHT_ASTRONOMICAL = "astronomical"
# Sun depression below the horizon for each twilight phase, as defined by astral.
_TWILIGHT_DEPRESSIONS = {
_TWILIGHT_CIVIL: astral.Depression.CIVIL,
_TWILIGHT_NAUTICAL: astral.Depression.NAUTICAL,
_TWILIGHT_ASTRONOMICAL: astral.Depression.ASTRONOMICAL,
}
# The sun is a singleton, so the elevation triggers always target sun.sun
# instead of asking the user to pick an entity.
_SUN_ENTITY_ID = f"{DOMAIN}.{DOMAIN}"
_ELEVATION_DOMAIN_SPECS = {DOMAIN: DomainSpec(value_source=STATE_ATTR_ELEVATION)}
_ELEVATION_CHANGED_TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "sun",
vol.Required(CONF_EVENT): cv.sun_event,
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
vol.Required(CONF_OPTIONS, default=dict): {
vol.Required("threshold"): NumericThresholdSelector(
NumericThresholdSelectorConfig(mode=NumericThresholdMode.CHANGED)
),
}
}
)
# Unlike the generic numerical triggers there is no behavior option: a behavior
# (each/first/all) is only meaningful across multiple targeted entities.
_ELEVATION_CROSSED_TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_OPTIONS, default=dict): {
vol.Required("threshold"): NumericThresholdSelector(
NumericThresholdSelectorConfig(mode=NumericThresholdMode.CROSSED)
),
vol.Optional(CONF_FOR): cv.positive_time_period,
}
}
)
async def async_attach_trigger(
hass: HomeAssistant,
config: ConfigType,
action: TriggerActionType,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Listen for events based on configuration."""
trigger_data = trigger_info["trigger_data"]
event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET)
description = event
if offset:
description = f"{description} with offset"
job = HassJob(action)
class SunElevationTrigger(EntityNumericalStateTriggerBase):
"""Trigger for the sun's elevation."""
@callback
def call_action() -> None:
"""Call action with right context."""
hass.async_run_hass_job(
job,
{
"trigger": {
**trigger_data,
"platform": "sun",
"event": event,
"offset": offset,
"description": description,
}
},
_domain_specs = _ELEVATION_DOMAIN_SPECS
_valid_unit = DEGREE
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the trigger, targeting the singleton sun entity."""
super().__init__(
hass,
TriggerConfig(
key=config.key,
target={ATTR_ENTITY_ID: [_SUN_ENTITY_ID]},
options=config.options,
),
)
if event == SUN_EVENT_SUNRISE:
return async_track_sunrise(hass, call_action, offset)
return async_track_sunset(hass, call_action, offset)
class SunElevationChangedTrigger(
SunElevationTrigger, EntityNumericalStateChangedTriggerBase
):
"""Trigger for changes to the sun's elevation."""
_schema = _ELEVATION_CHANGED_TRIGGER_SCHEMA
class SunElevationCrossedTrigger(
SunElevationTrigger, EntityNumericalStateCrossedThresholdTriggerBase
):
"""Trigger for the sun's elevation crossing a threshold."""
_schema = _ELEVATION_CROSSED_TRIGGER_SCHEMA
_EVENT_TRIGGER_SCHEMA = vol.Schema({vol.Required(CONF_OPTIONS, default=dict): {}})
class SunEventTrigger(Trigger):
"""Trigger that fires at a solar event time."""
_event: str
_schema: vol.Schema = _EVENT_TRIGGER_SCHEMA
@override
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return cast(ConfigType, cls._schema(config))
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the trigger."""
super().__init__(hass, config)
self._options = config.options or {}
def _get_next_event(self, utc_point_in_time: datetime) -> datetime:
"""Return the next time this solar event occurs."""
return get_astral_event_next(self._hass, self._event, utc_point_in_time)
def _action_payload(self) -> dict[str, Any]:
"""Return extra trigger payload passed to the action."""
return {}
@override
async def async_attach_runner(
self,
run_action: TriggerActionRunner,
did_not_trigger: TriggerNotTriggeredReporter | None = None,
) -> CALLBACK_TYPE:
"""Attach the trigger to an action runner."""
unsubs: dict[str, CALLBACK_TYPE | None] = {"event": None, "config": None}
@callback
def schedule_next_event() -> None:
unsubs["event"] = async_track_point_in_utc_time(
self._hass, handle_event, self._get_next_event(dt_util.utcnow())
)
@callback
def handle_event(_now: datetime) -> None:
unsubs["event"] = None
schedule_next_event()
run_action(self._action_payload(), f"sun event {self._event}")
@callback
def handle_config(_event: Event) -> None:
if unsubs["event"]:
unsubs["event"]()
schedule_next_event()
unsubs["config"] = self._hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, handle_config
)
schedule_next_event()
@callback
def async_remove() -> None:
for unsub in unsubs.values():
if unsub:
unsub()
return async_remove
class SunriseTrigger(SunEventTrigger):
"""Trigger that fires at sunrise."""
_event = SUN_EVENT_SUNRISE
class SunsetTrigger(SunEventTrigger):
"""Trigger that fires at sunset."""
_event = SUN_EVENT_SUNSET
class SolarNoonTrigger(SunEventTrigger):
"""Trigger that fires at solar noon."""
_event = _SUN_EVENT_SOLAR_NOON
class SolarMidnightTrigger(SunEventTrigger):
"""Trigger that fires at solar midnight."""
_event = _SUN_EVENT_SOLAR_MIDNIGHT
_DAWN_DUSK_TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_OPTIONS, default=dict): {
vol.Optional(CONF_TYPE, default=_TWILIGHT_CIVIL): vol.In(
_TWILIGHT_DEPRESSIONS
),
}
}
)
class SunDawnDuskTrigger(SunEventTrigger):
"""Trigger that fires at dawn or dusk for a configurable twilight phase."""
_schema = _DAWN_DUSK_TRIGGER_SCHEMA
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the trigger."""
super().__init__(hass, config)
self._twilight: str = self._options[CONF_TYPE]
self._depression = _TWILIGHT_DEPRESSIONS[self._twilight]
@override
def _get_next_event(self, utc_point_in_time: datetime) -> datetime:
return get_observer_astral_event_next(
get_astral_observer(self._hass),
self._event,
utc_point_in_time,
depression=self._depression,
)
@override
def _action_payload(self) -> dict[str, Any]:
return {CONF_TYPE: self._twilight}
class DawnTrigger(SunDawnDuskTrigger):
"""Trigger that fires at dawn."""
_event = _SUN_EVENT_DAWN
class DuskTrigger(SunDawnDuskTrigger):
"""Trigger that fires at dusk."""
_event = _SUN_EVENT_DUSK
_LEGACY_OPTIONS_SCHEMA_DICT: dict[vol.Marker, Any] = {
vol.Required(CONF_EVENT): cv.sun_event,
vol.Optional(CONF_OFFSET, default=timedelta(0)): cv.time_period,
}
class LegacySunTrigger(SunEventTrigger):
"""Backwards compatible trigger for the legacy ``platform: sun`` config."""
_schema = vol.Schema({vol.Required(CONF_OPTIONS): _LEGACY_OPTIONS_SCHEMA_DICT})
@override
@classmethod
async def async_validate_complete_config(
cls, hass: HomeAssistant, complete_config: ConfigType
) -> ConfigType:
"""Validate complete config, migrating the legacy top-level fields."""
complete_config = move_top_level_schema_fields_to_options(
complete_config, _LEGACY_OPTIONS_SCHEMA_DICT
)
return await super().async_validate_complete_config(hass, complete_config)
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the trigger."""
super().__init__(hass, config)
self._event = self._options[CONF_EVENT]
self._offset: timedelta = self._options[CONF_OFFSET]
@override
def _get_next_event(self, utc_point_in_time: datetime) -> datetime:
return get_astral_event_next(
self._hass, self._event, utc_point_in_time, self._offset
)
@override
def _action_payload(self) -> dict[str, Any]:
return {"event": self._event, "offset": self._offset}
TRIGGERS: dict[str, type[Trigger]] = {
"_": LegacySunTrigger,
"sunrise": SunriseTrigger,
"sunset": SunsetTrigger,
"solar_noon": SolarNoonTrigger,
"solar_midnight": SolarMidnightTrigger,
"dawn": DawnTrigger,
"dusk": DuskTrigger,
"elevation_changed": SunElevationChangedTrigger,
"elevation_crossed_threshold": SunElevationCrossedTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for the sun."""
return TRIGGERS
@@ -0,0 +1,64 @@
.type: &twilight_type
required: true
default: civil
selector:
select:
translation_key: twilight_type
options:
- civil
- nautical
- astronomical
.for: &trigger_for
required: true
default: "00:00:00"
selector:
duration:
.elevation_threshold_entity: &trigger_elevation_threshold_entity
- domain: input_number
unit_of_measurement: "°"
- domain: number
unit_of_measurement: "°"
- domain: sensor
unit_of_measurement: "°"
.elevation_threshold_number: &trigger_elevation_threshold_number
min: -90
max: 90
mode: box
unit_of_measurement: "°"
sunrise: {}
sunset: {}
solar_noon: {}
solar_midnight: {}
dawn:
fields:
type: *twilight_type
dusk:
fields:
type: *twilight_type
elevation_changed:
fields:
threshold:
required: true
selector:
numeric_threshold:
entity: *trigger_elevation_threshold_entity
mode: changed
number: *trigger_elevation_threshold_number
elevation_crossed_threshold:
fields:
for: *trigger_for
threshold:
required: true
selector:
numeric_threshold:
entity: *trigger_elevation_threshold_entity
mode: crossed
number: *trigger_elevation_threshold_number
+18 -3
View File
@@ -289,19 +289,35 @@ async def _async_resolve_template_config(
config = blueprint_inputs.async_substitute()
platforms = [platform for platform in PLATFORMS if platform in config]
platform_config: list[ConfigType] | ConfigType
if len(platforms) > 1:
raise vol.Invalid("more than one platform defined per blueprint")
if len(platforms) == 1:
platform = platforms.pop()
for prop in (CONF_NAME, CONF_UNIQUE_ID):
if prop in config:
config[platform][prop] = config.pop(prop)
platform_config = config[platform]
if isinstance(platform_config, dict):
platform_config[prop] = config.pop(prop)
continue
if len(platform_config) > 1:
raise vol.Invalid(
f"more than one {platform} entity defined in blueprint"
)
platform_config[0][prop] = config.pop(prop)
# State based template entities remove CONF_VARIABLES because they pass
# blueprint inputs to the template entities. Trigger based template entities
# retain CONF_VARIABLES because the variables are always executed between
# the trigger and action.
if CONF_TRIGGERS not in config and CONF_VARIABLES in config:
_merge_section_variables(config[platform], config.pop(CONF_VARIABLES))
section_variables = config.pop(CONF_VARIABLES)
platform_config = config[platform]
if isinstance(platform_config, dict):
platform_config = [platform_config]
for entity_config in platform_config:
_merge_section_variables(entity_config, section_variables)
raw_config = dict(config)
@@ -313,7 +329,6 @@ async def _async_resolve_template_config(
# variables at the entity level should be merged
# together at the entity level.
section_variables = config.pop(CONF_VARIABLES)
platform_config: list[ConfigType] | ConfigType
platforms = [platform for platform in PLATFORMS if platform in config]
for platform in platforms:
platform_config = config[platform]
@@ -1 +0,0 @@
"""Unifi LED Lights integration."""
@@ -1,97 +0,0 @@
"""Support for Unifi Led lights."""
import logging
from typing import Any, override
from unifiled import unifiled
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
LightEntity,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
# Validation of the user's configuration
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=20443): vol.All(cv.port, cv.string),
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Unifi LED platform."""
# Assign configuration variables.
# The configuration check takes care they are present.
host = config[CONF_HOST]
port = config[CONF_PORT]
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
api = unifiled(host, port, username=username, password=password)
# Verify that passed in configuration works
if not api.getloginstate():
_LOGGER.error("Could not connect to unifiled controller")
return
add_entities(UnifiLedLight(light, api) for light in api.getlights())
class UnifiLedLight(LightEntity):
"""Representation of an unifiled Light."""
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
def __init__(self, light: dict[str, Any], api: unifiled) -> None:
"""Init Unifi LED Light."""
self._api = api
self._light = light
self._attr_name = light["name"]
self._light_id = light["id"]
self._attr_unique_id = light["id"]
self._attr_is_on = light["status"]["output"]
self._attr_available = light["isOnline"]
self._attr_brightness = self._api.convertfrom100to255(light["status"]["led"])
@override
def turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on."""
self._api.setdevicebrightness(
self._light_id,
str(self._api.convertfrom255to100(kwargs.get(ATTR_BRIGHTNESS, 255))),
)
self._api.setdeviceoutput(self._light_id, 1)
@override
def turn_off(self, **kwargs: Any) -> None:
"""Instruct the light to turn off."""
self._api.setdeviceoutput(self._light_id, 0)
def update(self) -> None:
"""Update the light states."""
self._attr_is_on = self._api.getlightstate(self._light_id)
self._attr_brightness = self._api.convertfrom100to255(
self._api.getlightbrightness(self._light_id)
)
self._attr_available = self._api.getlightavailable(self._light_id)
@@ -1,10 +0,0 @@
{
"domain": "unifiled",
"name": "UniFi LED",
"codeowners": ["@florisvdk"],
"documentation": "https://www.home-assistant.io/integrations/unifiled",
"iot_class": "local_polling",
"loggers": ["unifiled"],
"quality_scale": "legacy",
"requirements": ["unifiled==0.11"]
}
@@ -381,6 +381,7 @@ class ProtectDeviceSmartDetectEventEntity(
entity_description: ProtectEventEntityDescription
@override
async def async_added_to_hass(self) -> None:
"""Subscribe to public smart-detect events for this camera."""
await super().async_added_to_hass()
+7 -7
View File
@@ -191,7 +191,7 @@
"title": "Vacuum",
"triggers": {
"errored": {
"description": "Triggers after one or more vacuums encounter an error.",
"description": "Triggers when one or more vacuum cleaners encounter an error.",
"fields": {
"behavior": {
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
@@ -200,10 +200,10 @@
"name": "[%key:component::vacuum::common::trigger_for_name%]"
}
},
"name": "Vacuum encountered an error"
"name": "Vacuum cleaner encountered an error"
},
"paused_cleaning": {
"description": "Triggers after one or more vacuums pause cleaning.",
"description": "Triggers when one or more vacuum cleaners pause cleaning.",
"fields": {
"behavior": {
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
@@ -215,7 +215,7 @@
"name": "Vacuum cleaner paused cleaning"
},
"returned_to_dock": {
"description": "Triggers after one or more vacuums have returned to dock.",
"description": "Triggers when one or more vacuum cleaners have returned to dock.",
"fields": {
"behavior": {
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
@@ -224,10 +224,10 @@
"name": "[%key:component::vacuum::common::trigger_for_name%]"
}
},
"name": "Vacuum returned to dock"
"name": "Vacuum cleaner returned to dock"
},
"started_cleaning": {
"description": "Triggers after one or more vacuums start cleaning.",
"description": "Triggers when one or more vacuum cleaners start cleaning.",
"fields": {
"behavior": {
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
@@ -239,7 +239,7 @@
"name": "Vacuum cleaner started cleaning"
},
"started_returning": {
"description": "Triggers after one or more vacuums start returning to dock.",
"description": "Triggers when one or more vacuum cleaners start returning to dock.",
"fields": {
"behavior": {
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
+3 -1
View File
@@ -1,7 +1,7 @@
"""Vistapool Select entities."""
from dataclasses import dataclass
from typing import Any
from typing import Any, override
from aioaquarite import AquariteError
@@ -111,6 +111,7 @@ class VistapoolSelect(VistapoolEntity, SelectEntity):
self._attr_translation_placeholders = description.translation_placeholders
@property
@override
def current_option(self) -> str | None:
"""Return the option that maps to the current API value."""
index = _to_index(
@@ -121,6 +122,7 @@ class VistapoolSelect(VistapoolEntity, SelectEntity):
return None
return options[index]
@override
async def async_select_option(self, option: str) -> None:
"""Send the index of the chosen option to the controller."""
assert self.entity_description.options is not None
@@ -368,6 +368,7 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator):
self._supported_capabilities: list[str] = []
@override
async def _async_update_data(self) -> CoordinatorData:
"""Fetch data and trigger location update on engine-off."""
previous_state = self.get_api_field("engineStatus")
@@ -467,6 +468,7 @@ class VolvoFastIntervalCoordinator(VolvoBaseCoordinator):
api.async_get_window_states,
]
@override
async def _async_update_data(self) -> CoordinatorData:
"""Fetch data and trigger location update on lock."""
previous_state = self.get_api_field("centralLock")
@@ -48,6 +48,7 @@ class WolfLinkCoordinator(DataUpdateCoordinator[dict[int, tuple[int, str]]]):
self._system_share_id = device.system_share_id
self._refetch_parameters = False
@override
async def _async_setup(self) -> None:
"""Fetch parameters once during initial setup."""
try:
-12
View File
@@ -1759,12 +1759,6 @@
}
}
},
"eliqonline": {
"name": "Eliqonline",
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling"
},
"elkm1": {
"name": "Elk-M1 Control",
"integration_type": "hub",
@@ -7599,12 +7593,6 @@
"iot_class": "local_polling",
"name": "UniFi AP"
},
"unifiled": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "UniFi LED"
},
"unifiprotect": {
"integration_type": "hub",
"config_flow": true,
+5 -1
View File
@@ -1155,7 +1155,11 @@ class EntityRegistry(BaseRegistry):
def async_get_entity_id(
self, domain: str, platform: str, unique_id: str
) -> str | None:
"""Check if an entity_id is currently registered."""
"""Check if an entity_id is currently registered.
domain: entity platform domain (e.g. light, sensor)
platform: integration domain (e.g. hue, zwave)
"""
return self.entities.get_entity_id((domain, platform, unique_id))
@callback
Generated
+1 -1
View File
@@ -17,7 +17,7 @@ no_implicit_optional = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_ignores = true
enable_error_code = deprecated, ignore-without-code, redundant-self, truthy-iterable
enable_error_code = deprecated, explicit-override, ignore-without-code, redundant-self, truthy-iterable
disable_error_code = annotation-unchecked, import-not-found, import-untyped
extra_checks = false
check_untyped_defs = true
@@ -9,6 +9,7 @@ Material Design Icons set.
"""
import re
from typing import override
from astroid import nodes
from pylint.checkers import BaseChecker
@@ -51,6 +52,7 @@ class MdiIconsChecker(BaseChecker):
_in_integration: bool
_checked_icons_json: set[str]
@override
def open(self) -> None:
"""Initialize per-run state."""
self._checked_icons_json = set()
+5 -11
View File
@@ -909,9 +909,6 @@ elevenlabs==2.51.0
# homeassistant.components.elgato
elgato==5.1.2
# homeassistant.components.eliqonline
eliqonline==1.2.2
# homeassistant.components.elkm1
elkm1-lib==2.2.15
@@ -1168,7 +1165,7 @@ goslide-api==0.7.4
gotailwind==0.4.0
# homeassistant.components.govee_ble
govee-ble==1.2.0
govee-ble==1.4.0
# homeassistant.components.govee_light_local
govee-local-api==2.4.0
@@ -1809,7 +1806,7 @@ oru==0.1.11
orvibo==1.1.2
# homeassistant.components.ouman_eh_800
ouman-eh-800-api==0.5.0
ouman-eh-800-api==1.0.0
# homeassistant.components.ourgroceries
ourgroceries==1.5.4
@@ -2036,7 +2033,7 @@ pyanglianwater==3.2.2
pyaprilaire==0.9.1
# homeassistant.components.aqvify
pyaqvify==0.0.11
pyaqvify==0.0.12
# homeassistant.components.atag
pyatag==0.3.5.3
@@ -2645,7 +2642,7 @@ python-clementine-remote==1.0.1
python-digitalocean==1.13.2
# homeassistant.components.dropbox
python-dropbox-api==0.1.3
python-dropbox-api==0.1.4
# homeassistant.components.duco
python-duco-connectivity==0.7.1
@@ -3192,7 +3189,7 @@ thermopro-ble==1.1.4
thingspeak==1.0.0
# homeassistant.components.lg_thinq
thinqconnect==1.0.12
thinqconnect==1.0.13
# homeassistant.components.tilt_ble
tilt-ble==1.0.1
@@ -3272,9 +3269,6 @@ unifi-discovery==1.5.0
# homeassistant.components.unifi_direct
unifi_ap==0.0.2
# homeassistant.components.unifiled
unifiled==0.11
# homeassistant.components.homeassistant_hardware
universal-silabs-flasher==1.1.0
+1 -1
View File
@@ -30,7 +30,7 @@ pytest-github-actions-annotate-failures==0.4.0
pytest-socket==0.8.0
pytest-sugar==1.1.1
pytest-timeout==2.4.0
pytest-unordered==0.7.0
pytest-unordered==0.8.0
pytest-picked==0.5.1
pytest-xdist==3.8.0
pytest==9.0.3
+1
View File
@@ -54,6 +54,7 @@ GENERAL_SETTINGS: Final[dict[str, str]] = {
"enable_error_code": ", ".join( # noqa: FLY002
[
"deprecated",
"explicit-override",
"ignore-without-code",
"redundant-self",
"truthy-iterable",
-4
View File
@@ -305,7 +305,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"egardia",
"eight_sleep",
"electrasmart",
"eliqonline",
"elkm1",
"elmax",
"elv",
@@ -963,7 +962,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"uk_transport",
"ukraine_alarm",
"unifi_direct",
"unifiled",
"universal",
"upb",
"upc_connect",
@@ -1265,7 +1263,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"eight_sleep",
"electrasmart",
"elevenlabs",
"eliqonline",
"elkm1",
"elmax",
"elv",
@@ -1955,7 +1952,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"uk_transport",
"ukraine_alarm",
"unifi_direct",
"unifiled",
"universal",
"upb",
"upc_connect",
-1
View File
@@ -185,7 +185,6 @@ EXCEPTIONS = {
"crownstone-core", # https://github.com/crownstone/crownstone-lib-python-core/pull/6
"crownstone-sse", # https://github.com/crownstone/crownstone-lib-python-sse/pull/2
"crownstone-uart", # https://github.com/crownstone/crownstone-lib-python-uart/pull/12
"eliqonline", # https://github.com/molobrakos/eliqonline/pull/17
"imutils", # https://github.com/PyImageSearch/imutils/pull/292
"iso4217", # Public domain
"kiwiki-client", # https://github.com/c7h/kiwiki_client/pull/6
@@ -6,15 +6,15 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30,
'min_temp': 16,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -49,20 +49,20 @@
# name: test_climate_entities[climate.living_room_living_room-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 50.0,
'current_temperature': 22.0,
<ClimateEntityStateAttribute.CURRENT_HUMIDITY: 'current_humidity'>: 50.0,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.0,
'friendly_name': 'Living Room',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30,
'min_temp': 16,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
'supported_features': <ClimateEntityFeature: 385>,
'temperature': 24.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 24.0,
}),
'context': <ANY>,
'entity_id': 'climate.living_room_living_room',
@@ -79,21 +79,21 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30.0,
'min_temp': 16.0,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -128,27 +128,27 @@
# name: test_climate_entities[climate.test_system-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 50.0,
'current_temperature': 22.0,
'fan_mode': 'low',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_HUMIDITY: 'current_humidity'>: 50.0,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.0,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'low',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'friendly_name': 'Test System',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30.0,
'min_temp': 16.0,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
'supported_features': <ClimateEntityFeature: 393>,
'temperature': 24.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 24.0,
}),
'context': <ANY>,
'entity_id': 'climate.test_system',
@@ -21,17 +21,17 @@
# name: test_climate_myauto_main[climate.myauto]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': None,
'fan_mode': 'auto',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: None,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'auto',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'low',
'medium',
'high',
'auto',
]),
'friendly_name': 'myauto',
'hvac_action': <HVACAction.COOLING: 'cooling'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.COOLING: 'cooling'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
@@ -39,18 +39,18 @@
<HVACMode.DRY: 'dry'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 32,
'min_temp': 16,
'preset_mode': 'MyAuto',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 32,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'MyAuto',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'MyZone',
'MyTemp',
'MyAuto',
]),
'supported_features': <ClimateEntityFeature: 410>,
'target_temp_high': 24,
'target_temp_low': 20,
'target_temp_step': 1,
<ClimateEntityStateAttribute.TARGET_TEMP_HIGH: 'target_temp_high'>: 24,
<ClimateEntityStateAttribute.TARGET_TEMP_LOW: 'target_temp_low'>: 20,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
}),
'context': <ANY>,
'entity_id': 'climate.myauto',
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'1',
'8',
'30',
@@ -49,7 +49,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient CO2 automatic baseline duration',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'1',
'8',
'30',
@@ -73,7 +73,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'cloud',
'local',
]),
@@ -112,7 +112,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient Configuration source',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'cloud',
'local',
]),
@@ -132,7 +132,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'ugm3',
'us_aqi',
]),
@@ -171,7 +171,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient Display PM standard',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'ugm3',
'us_aqi',
]),
@@ -191,7 +191,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'c',
'f',
]),
@@ -230,7 +230,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient Display temperature unit',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'c',
'f',
]),
@@ -250,7 +250,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'off',
'co2',
'pm',
@@ -290,7 +290,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient LED bar mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'off',
'co2',
'pm',
@@ -311,7 +311,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'12',
'60',
'120',
@@ -353,7 +353,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient NOx index learning offset',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'12',
'60',
'120',
@@ -376,7 +376,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'12',
'60',
'120',
@@ -418,7 +418,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient VOC index learning offset',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'12',
'60',
'120',
@@ -441,7 +441,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'1',
'8',
'30',
@@ -484,7 +484,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient CO2 automatic baseline duration',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'1',
'8',
'30',
@@ -508,7 +508,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'cloud',
'local',
]),
@@ -547,7 +547,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient Configuration source',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'cloud',
'local',
]),
@@ -567,7 +567,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'12',
'60',
'120',
@@ -609,7 +609,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient NOx index learning offset',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'12',
'60',
'120',
@@ -632,7 +632,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'12',
'60',
'120',
@@ -674,7 +674,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient VOC index learning offset',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'12',
'60',
'120',
@@ -6,12 +6,12 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 35.0,
'min_temp': 5.0,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 5.0,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'home',
'away',
'boost',
@@ -50,23 +50,23 @@
# name: test_climate_entities[climate.test_thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 45.0,
'current_temperature': 22.0,
<ClimateEntityStateAttribute.CURRENT_HUMIDITY: 'current_humidity'>: 45.0,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.0,
'friendly_name': 'Test Thermostat',
'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.IDLE: 'idle'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 35.0,
'min_temp': 5.0,
'preset_mode': 'home',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 5.0,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'home',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'home',
'away',
'boost',
]),
'supported_features': <ClimateEntityFeature: 17>,
'temperature': 22.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.0,
}),
'context': <ANY>,
'entity_id': 'climate.test_thermostat',
@@ -6,19 +6,19 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'low',
'high',
'auto',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30.0,
'min_temp': 16.0,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'on',
'off',
]),
@@ -56,21 +56,21 @@
# name: test_climate_entities[None][climate.living_room-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'low',
'high',
'auto',
]),
'friendly_name': 'living room',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30.0,
'min_temp': 16.0,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
'supported_features': <ClimateEntityFeature: 425>,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'on',
'off',
]),
@@ -90,19 +90,19 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'low',
'high',
'auto',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30.0,
'min_temp': 16.0,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'on',
'off',
]),
@@ -140,29 +140,29 @@
# name: test_climate_entities[climate_data0][climate.living_room-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 45.0,
'current_temperature': 22.5,
'fan_mode': 'high',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_HUMIDITY: 'current_humidity'>: 45.0,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.5,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'high',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'low',
'high',
'auto',
]),
'friendly_name': 'living room',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30.0,
'min_temp': 16.0,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
'supported_features': <ClimateEntityFeature: 425>,
'swing_mode': 'off',
'swing_modes': list([
<ClimateEntityStateAttribute.SWING_MODE: 'swing_mode'>: 'off',
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'on',
'off',
]),
'temperature': 22.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.0,
}),
'context': <ANY>,
'entity_id': 'climate.living_room',
@@ -0,0 +1,62 @@
# serializer version: 1
# name: test_all_entities[select.echo_test_drop_in-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'all',
'home',
'off',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.echo_test_drop_in',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Drop In',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Drop In',
'platform': 'alexa_devices',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'dropin',
'unique_id': 'echo_test_serial_number-dropin',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[select.echo_test_drop_in-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Echo Test Drop In',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'all',
'home',
'off',
]),
}),
'context': <ANY>,
'entity_id': 'select.echo_test_drop_in',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'all',
})
# ---
@@ -0,0 +1,98 @@
"""Tests for the Alexa Devices select platform."""
from copy import deepcopy
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN
from tests.common import MockConfigEntry, snapshot_platform
ENTITY_ID = "select.echo_test_drop_in"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.SELECT]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_select_dropin_option(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test selecting a drop-in option."""
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(ENTITY_ID))
assert state.state == "all"
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: "home"},
blocking=True,
)
assert mock_amazon_devices_client.set_dropin_status.call_count == 1
assert (state := hass.states.get(ENTITY_ID))
assert state.state == "home"
device_data = deepcopy(TEST_DEVICE_1)
device_data.communication_settings = {
"announcements": "ON",
"communications": "ON",
"dropin": "Off",
}
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_DEVICE_1_SN: device_data
}
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: "off"},
blocking=True,
)
assert mock_amazon_devices_client.set_dropin_status.call_count == 2
assert (state := hass.states.get(ENTITY_ID))
assert state.state == "off"
async def test_offline_device(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test offline device handling."""
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
].online = False
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(ENTITY_ID))
assert state.state == STATE_UNAVAILABLE
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'off',
'level1',
'level2',
@@ -47,7 +47,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'My water heater Hot Water+ level',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'off',
'level1',
'level2',
@@ -6,13 +6,13 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 40.0,
'min_temp': 10.0,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 40.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 10.0,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'ready',
'rest',
]),
@@ -50,22 +50,22 @@
# name: test_climate[climate.fakespa-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 10.0,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 10.0,
'friendly_name': 'FakeSpa',
'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.IDLE: 'idle'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 40.0,
'min_temp': 10.0,
'preset_mode': 'ready',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 40.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 10.0,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'ready',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'ready',
'rest',
]),
'supported_features': <ClimateEntityFeature: 401>,
'temperature': 40.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 40.0,
}),
'context': <ANY>,
'entity_id': 'climate.fakespa',
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'low',
'high',
]),
@@ -45,7 +45,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'FakeSpa Temperature range',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'low',
'high',
]),
@@ -6,20 +6,20 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'med',
'high',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 95,
'min_temp': 45,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 95,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 45,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -54,28 +54,28 @@
# name: test_setup_integration_success[climate.system_1_zone_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 75,
'fan_mode': 'auto',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 75,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'auto',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'med',
'high',
]),
'friendly_name': 'System 1 Zone 1',
'hvac_action': <HVACAction.OFF: 'off'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.OFF: 'off'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 95,
'min_temp': 45,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 95,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 45,
'supported_features': <ClimateEntityFeature: 395>,
'target_temp_high': None,
'target_temp_low': None,
'temperature': 72,
<ClimateEntityStateAttribute.TARGET_TEMP_HIGH: 'target_temp_high'>: None,
<ClimateEntityStateAttribute.TARGET_TEMP_LOW: 'target_temp_low'>: None,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 72,
}),
'context': <ANY>,
'entity_id': 'climate.system_1_zone_1',
@@ -6,14 +6,14 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.AUTO: 'auto'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 20.0,
'min_temp': 8.0,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 20.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 8.0,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'eco',
'none',
]),
@@ -51,23 +51,23 @@
# name: test_celsius_fahrenheit[climate.heating_circuit_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 18.6,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 18.6,
'friendly_name': 'Heating circuit 1',
'hvac_action': <HVACAction.OFF: 'off'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.OFF: 'off'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.AUTO: 'auto'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 20.0,
'min_temp': 8.0,
'preset_mode': 'none',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 20.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 8.0,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'none',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'eco',
'none',
]),
'supported_features': <ClimateEntityFeature: 401>,
'temperature': 18.5,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 18.5,
}),
'context': <ANY>,
'entity_id': 'climate.heating_circuit_1',
@@ -84,14 +84,14 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.AUTO: 'auto'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 20.0,
'min_temp': 8.0,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 20.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 8.0,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'eco',
'none',
]),
@@ -129,23 +129,23 @@
# name: test_climate_entity_properties[climate.heating_circuit_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 18.6,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 18.6,
'friendly_name': 'Heating circuit 1',
'hvac_action': <HVACAction.OFF: 'off'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.OFF: 'off'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.AUTO: 'auto'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 20.0,
'min_temp': 8.0,
'preset_mode': 'none',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 20.0,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 8.0,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'none',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'eco',
'none',
]),
'supported_features': <ClimateEntityFeature: 401>,
'temperature': 18.5,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 18.5,
}),
'context': <ANY>,
'entity_id': 'climate.heating_circuit_1',
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'Speaker A',
'Speaker B',
'Headphones',
@@ -46,7 +46,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Cambridge Audio CXNv2 Audio output',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'Speaker A',
'Speaker B',
'Headphones',
@@ -67,7 +67,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'amplifier',
'receiver',
'off',
@@ -107,7 +107,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Cambridge Audio CXNv2 Control Bus mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'amplifier',
'receiver',
'off',
@@ -128,7 +128,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'bright',
'dim',
'off',
@@ -168,7 +168,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Cambridge Audio CXNv2 Display brightness',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'bright',
'dim',
'off',
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'15',
'30',
'45',
@@ -48,7 +48,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Jar Dimming time',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'15',
'30',
'45',
@@ -6,13 +6,13 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
@@ -20,13 +20,13 @@
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'off',
'on',
]),
'target_temp_step': 1,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -65,13 +65,13 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
@@ -79,13 +79,13 @@
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'off',
'on',
]),
'target_temp_step': 1,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -120,17 +120,17 @@
# name: test_climate_state.2
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 27,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 27,
'error_code': 0,
'fan_mode': 'off',
'fan_modes': list([
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'off',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'friendly_name': 'Midea 0',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
@@ -138,16 +138,16 @@
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'supported_features': <ClimateEntityFeature: 425>,
'swing_mode': 'off',
'swing_modes': list([
<ClimateEntityStateAttribute.SWING_MODE: 'swing_mode'>: 'off',
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'off',
'on',
]),
'target_temp_step': 1,
'temperature': 23,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 23,
}),
'context': <ANY>,
'entity_id': 'climate.midea_0',
@@ -160,17 +160,17 @@
# name: test_climate_state.3
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 26,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 26,
'error_code': 0,
'fan_mode': 'low',
'fan_modes': list([
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'low',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'friendly_name': 'Midea 1',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
@@ -178,16 +178,16 @@
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'supported_features': <ClimateEntityFeature: 425>,
'swing_mode': 'off',
'swing_modes': list([
<ClimateEntityStateAttribute.SWING_MODE: 'swing_mode'>: 'off',
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'off',
'on',
]),
'target_temp_step': 1,
'temperature': 24,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 24,
}),
'context': <ANY>,
'entity_id': 'climate.midea_1',
@@ -204,13 +204,13 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
@@ -218,13 +218,13 @@
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'off',
'on',
]),
'target_temp_step': 1,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -263,13 +263,13 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
@@ -277,13 +277,13 @@
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'off',
'on',
]),
'target_temp_step': 1,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -318,14 +318,14 @@
# name: test_climate_state.6
StateSnapshot({
'attributes': ReadOnlyDict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'friendly_name': 'Midea 0',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
@@ -333,14 +333,14 @@
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'supported_features': <ClimateEntityFeature: 425>,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'off',
'on',
]),
'target_temp_step': 1,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
}),
'context': <ANY>,
'entity_id': 'climate.midea_0',
@@ -353,14 +353,14 @@
# name: test_climate_state.7
StateSnapshot({
'attributes': ReadOnlyDict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'low',
'medium',
'high',
]),
'friendly_name': 'Midea 1',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
@@ -368,14 +368,14 @@
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'supported_features': <ClimateEntityFeature: 425>,
'swing_modes': list([
<ClimateEntityCapabilityAttribute.SWING_MODES: 'swing_modes'>: list([
'off',
'on',
]),
'target_temp_step': 1,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
}),
'context': <ANY>,
'entity_id': 'climate.midea_1',
@@ -6,18 +6,18 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30,
'min_temp': 5,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 5,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'automatic',
'manual',
]),
'target_temp_step': 0.1,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -52,24 +52,24 @@
# name: test_all_entities[climate.climate0-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 22.1,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.1,
'friendly_name': 'Climate0',
'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.IDLE: 'idle'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 30,
'min_temp': 5,
'preset_mode': 'manual',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 5,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'manual',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'automatic',
'manual',
]),
'supported_features': <ClimateEntityFeature: 401>,
'target_temp_step': 0.1,
'temperature': 5.0,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 0.1,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 5.0,
}),
'context': <ANY>,
'entity_id': 'climate.climate0',
@@ -6,21 +6,21 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'off',
'auto',
'low',
'medium',
'high',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'home',
'eco',
'none',
@@ -60,9 +60,9 @@
# name: test_climate_entities_snapshot[climate.nano_color_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 21.0,
'fan_mode': 'auto',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 21.0,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'auto',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'off',
'auto',
'low',
@@ -70,22 +70,22 @@
'high',
]),
'friendly_name': 'Nano Color 2',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
'preset_mode': 'home',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'home',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'home',
'eco',
'none',
'away',
]),
'supported_features': <ClimateEntityFeature: 25>,
'temperature': 22.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.0,
}),
'context': <ANY>,
'entity_id': 'climate.nano_color_2',
@@ -102,13 +102,13 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'home',
'away',
]),
@@ -146,16 +146,16 @@
# name: test_climate_entities_snapshot[climate.r_900-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 21.0,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 21.0,
'friendly_name': 'R 900',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
'preset_mode': 'home',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'home',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'home',
'away',
]),
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'off',
'auto',
'on',
@@ -46,7 +46,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Nano Color 2 Bypass',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'off',
'auto',
'on',
@@ -67,7 +67,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'polish',
'english',
]),
@@ -106,7 +106,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Nano Color 2 Language',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'polish',
'english',
]),
@@ -126,7 +126,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disabled',
'eco',
'hybrid',
@@ -166,7 +166,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'R 900 Operating mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disabled',
'eco',
'hybrid',
@@ -6,19 +6,19 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'on',
'circulate',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 95,
'min_temp': 45,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 95,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 45,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -53,28 +53,28 @@
# name: test_climate_entities[climate.test_controller_residential_thermostat_v2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 45,
'current_temperature': 72,
'fan_mode': 'auto',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_HUMIDITY: 'current_humidity'>: 45,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 72,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'auto',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'auto',
'on',
'circulate',
]),
'friendly_name': 'Test Controller Residential Thermostat V2',
'hvac_action': <HVACAction.OFF: 'off'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.OFF: 'off'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 95,
'min_temp': 45,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 95,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 45,
'supported_features': <ClimateEntityFeature: 395>,
'target_temp_high': None,
'target_temp_low': None,
'temperature': 68,
<ClimateEntityStateAttribute.TARGET_TEMP_HIGH: 'target_temp_high'>: None,
<ClimateEntityStateAttribute.TARGET_TEMP_LOW: 'target_temp_low'>: None,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 68,
}),
'context': <ANY>,
'entity_id': 'climate.test_controller_residential_thermostat_v2',
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'smart',
'auto',
'high',
@@ -15,14 +15,14 @@
'on',
'off',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.COOL: 'cool'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -57,9 +57,9 @@
# name: test_climate_device_with_cooling_support[sensor_payload0][climate.zen_01-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 23.2,
'fan_mode': 'off',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 23.2,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'off',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'smart',
'auto',
'high',
@@ -69,18 +69,18 @@
'off',
]),
'friendly_name': 'Zen-01',
'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.IDLE: 'idle'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.COOL: 'cool'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'offset': 0,
'supported_features': <ClimateEntityFeature: 393>,
'temperature': 22.2,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.2,
}),
'context': <ANY>,
'entity_id': 'climate.zen_01',
@@ -97,7 +97,7 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'smart',
'auto',
'high',
@@ -106,14 +106,14 @@
'on',
'off',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.COOL: 'cool'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -148,9 +148,9 @@
# name: test_climate_device_with_fan_support[sensor_payload0][climate.zen_01-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 23.2,
'fan_mode': 'auto',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 23.2,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'auto',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'smart',
'auto',
'high',
@@ -160,18 +160,18 @@
'off',
]),
'friendly_name': 'Zen-01',
'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.IDLE: 'idle'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.COOL: 'cool'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'offset': 0,
'supported_features': <ClimateEntityFeature: 393>,
'temperature': 22.2,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.2,
}),
'context': <ANY>,
'entity_id': 'climate.zen_01',
@@ -188,7 +188,7 @@
]),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'smart',
'auto',
'high',
@@ -197,15 +197,15 @@
'on',
'off',
]),
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.COOL: 'cool'>,
]),
'max_temp': 35,
'min_temp': 7,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'auto',
'boost',
'comfort',
@@ -248,9 +248,9 @@
# name: test_climate_device_with_preset[sensor_payload0][climate.zen_01-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 23.2,
'fan_mode': 'off',
'fan_modes': list([
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 23.2,
<ClimateEntityStateAttribute.FAN_MODE: 'fan_mode'>: 'off',
<ClimateEntityCapabilityAttribute.FAN_MODES: 'fan_modes'>: list([
'smart',
'auto',
'high',
@@ -260,18 +260,18 @@
'off',
]),
'friendly_name': 'Zen-01',
'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.IDLE: 'idle'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.COOL: 'cool'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'offset': 0,
'preset_mode': 'auto',
'preset_modes': list([
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'auto',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'auto',
'boost',
'comfort',
@@ -281,7 +281,7 @@
'manual',
]),
'supported_features': <ClimateEntityFeature: 409>,
'temperature': 22.2,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.2,
}),
'context': <ANY>,
'entity_id': 'climate.zen_01',
@@ -298,13 +298,13 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -339,19 +339,19 @@
# name: test_climate_device_without_cooling_support[sensor_payload0][climate.thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 22.6,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.6,
'friendly_name': 'Thermostat',
'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.HEATING: 'heating'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'offset': 10,
'supported_features': <ClimateEntityFeature: 385>,
'temperature': 22.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.0,
'valve': 30,
}),
'context': <ANY>,
@@ -369,12 +369,12 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -409,17 +409,17 @@
# name: test_clip_climate_device[config_entry_options0-sensor_payload0][climate.clip_thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 22.6,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.6,
'friendly_name': 'CLIP thermostat',
'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.HEATING: 'heating'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'supported_features': <ClimateEntityFeature: 385>,
'temperature': None,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: None,
'valve': 30,
}),
'context': <ANY>,
@@ -437,13 +437,13 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -478,19 +478,19 @@
# name: test_clip_climate_device[config_entry_options0-sensor_payload0][climate.thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 22.6,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.6,
'friendly_name': 'Thermostat',
'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.HEATING: 'heating'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'offset': 10,
'supported_features': <ClimateEntityFeature: 385>,
'temperature': 22.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.0,
'valve': 30,
}),
'context': <ANY>,
@@ -508,12 +508,12 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -548,19 +548,19 @@
# name: test_simple_climate_device[sensor_payload0][climate.thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 21.0,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 21.0,
'friendly_name': 'thermostat',
'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.HEATING: 'heating'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'locked': True,
'max_temp': 35,
'min_temp': 7,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
'offset': 0,
'supported_features': <ClimateEntityFeature: 385>,
'temperature': 21.0,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 21.0,
'valve': 24,
}),
'context': <ANY>,
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'leftright',
'undirected',
]),
@@ -45,7 +45,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Device Mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'leftright',
'undirected',
]),
@@ -65,7 +65,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'High',
'Medium',
'Low',
@@ -105,7 +105,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Sensitivity',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'High',
'Medium',
'Low',
@@ -126,7 +126,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'far',
'medium',
'near',
@@ -166,7 +166,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Trigger Distance',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'far',
'medium',
'near',
@@ -187,7 +187,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'leftright',
'undirected',
]),
@@ -226,7 +226,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Device Mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'leftright',
'undirected',
]),
@@ -246,7 +246,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'High',
'Medium',
'Low',
@@ -286,7 +286,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Sensitivity',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'High',
'Medium',
'Low',
@@ -307,7 +307,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'far',
'medium',
'near',
@@ -347,7 +347,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Trigger Distance',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'far',
'medium',
'near',
@@ -368,7 +368,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'leftright',
'undirected',
]),
@@ -407,7 +407,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Device Mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'leftright',
'undirected',
]),
@@ -427,7 +427,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'High',
'Medium',
'Low',
@@ -467,7 +467,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Sensitivity',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'High',
'Medium',
'Low',
@@ -488,7 +488,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'far',
'medium',
'near',
@@ -528,7 +528,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Aqara FP1 Trigger Distance',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'far',
'medium',
'near',
@@ -549,7 +549,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'off',
'auto',
'speed_1',
@@ -593,7 +593,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'IKEA Starkvind Fan Mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'off',
'auto',
'speed_1',
@@ -2,16 +2,16 @@
# name: test_climate
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 20,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 20,
'friendly_name': 'Test',
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 24,
'min_temp': 4,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 24,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 4,
'supported_features': <ClimateEntityFeature: 1>,
'target_temp_step': 0.5,
'temperature': 20,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 0.5,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 20,
}),
'context': <ANY>,
'entity_id': 'climate.test_test',
@@ -28,12 +28,12 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 24,
'min_temp': 4,
'target_temp_step': 0.5,
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 24,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 4,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 0.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
+1
View File
@@ -237,6 +237,7 @@ def mock_duco_client(
client.async_get_lan_info.return_value = mock_lan_info
client.async_get_nodes.return_value = mock_nodes
client.async_get_node_actions.return_value = mock_node_actions
client.async_get_time_filter_remaining.return_value = 180
client.async_get_diagnostics.return_value = [
DiagComponent(component="Ventilation", status="Ok")
]
@@ -435,7 +435,7 @@
'state': '90',
})
# ---
# name: test_sensor_entities_state[sensor.living_state_end_time-entry]
# name: test_sensor_entities_state[sensor.living_filter_remaining-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -449,7 +449,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.living_state_end_time',
'entity_id': 'sensor.living_filter_remaining',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -457,33 +457,37 @@
'labels': set({
}),
'name': None,
'object_id_base': 'State end time',
'object_id_base': 'Filter remaining',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'State end time',
'original_name': 'Filter remaining',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'time_state_end',
'unique_id': 'aa:bb:cc:dd:ee:ff_1_time_state_end',
'unit_of_measurement': None,
'translation_key': 'filter_remaining',
'unique_id': 'aa:bb:cc:dd:ee:ff_1_filter_remaining',
'unit_of_measurement': <UnitOfTime.DAYS: 'd'>,
})
# ---
# name: test_sensor_entities_state[sensor.living_state_end_time-state]
# name: test_sensor_entities_state[sensor.living_filter_remaining-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Living State end time',
'device_class': 'duration',
'friendly_name': 'Living Filter remaining',
'unit_of_measurement': <UnitOfTime.DAYS: 'd'>,
}),
'context': <ANY>,
'entity_id': 'sensor.living_state_end_time',
'entity_id': 'sensor.living_filter_remaining',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '180',
})
# ---
# name: test_sensor_entities_state[sensor.living_signal_strength-entry]
@@ -541,6 +545,57 @@
'state': '-60',
})
# ---
# name: test_sensor_entities_state[sensor.living_state_end_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'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.living_state_end_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'State end time',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'State end time',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'time_state_end',
'unique_id': 'aa:bb:cc:dd:ee:ff_1_time_state_end',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_entities_state[sensor.living_state_end_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Living State end time',
}),
'context': <ANY>,
'entity_id': 'sensor.living_state_end_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor_entities_state[sensor.living_target_flow_level-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
+53
View File
@@ -26,6 +26,8 @@ from . import setup_platform_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
FILTER_REMAINING_ENTITY_ID = "sensor.living_filter_remaining"
@pytest.fixture
async def init_integration(
@@ -139,6 +141,57 @@ async def test_lan_info_failures_keep_node_entities_available(
assert state.state == "-60"
async def test_time_filter_remaining_missing_skips_sensor_creation(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
mock_sensor_nodes: list[Node],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the filter timer sensor is not created when unsupported."""
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
mock_duco_client.async_get_time_filter_remaining = AsyncMock(
side_effect=[None, 180]
)
await setup_platform_integration(hass, mock_config_entry, [Platform.SENSOR])
assert hass.states.get(FILTER_REMAINING_ENTITY_ID) is None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert hass.states.get(FILTER_REMAINING_ENTITY_ID) is None
async def test_time_filter_remaining_transient_failure_recovers_sensor_creation(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
mock_sensor_nodes: list[Node],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the filter timer sensor is added once a transient startup failure recovers."""
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
mock_duco_client.async_get_time_filter_remaining = AsyncMock(
side_effect=[DucoError("heat recovery info error"), 180]
)
await setup_platform_integration(hass, mock_config_entry, [Platform.SENSOR])
assert hass.states.get(FILTER_REMAINING_ENTITY_ID) is None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get(FILTER_REMAINING_ENTITY_ID)
assert state is not None
assert state.state == "180"
@pytest.mark.parametrize(
(
"node_id",
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'Map 2',
'1',
]),
@@ -45,7 +45,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'X8 PRO OMNI Active map',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'Map 2',
'1',
]),
@@ -65,7 +65,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'smart',
]),
@@ -104,7 +104,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'X8 PRO OMNI Auto-empty frequency',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'smart',
]),
@@ -124,7 +124,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'mop',
'mop_after_vacuum',
'vacuum',
@@ -165,7 +165,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'X8 PRO OMNI Work mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'mop',
'mop_after_vacuum',
'vacuum',
@@ -187,7 +187,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'Map 2',
'1',
]),
@@ -226,7 +226,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Active map',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'Map 2',
'1',
]),
@@ -246,7 +246,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'smart',
]),
@@ -285,7 +285,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Auto-empty frequency',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'smart',
]),
@@ -305,7 +305,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'low',
'medium',
'high',
@@ -345,7 +345,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Water flow level',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'low',
'medium',
'high',
@@ -366,7 +366,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'Map 2',
'1',
]),
@@ -405,7 +405,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Ozmo 950 Active map',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'Map 2',
'1',
]),
@@ -425,7 +425,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'low',
'medium',
'high',
@@ -466,7 +466,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Ozmo 950 Water flow level',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'low',
'medium',
'high',
@@ -6,18 +6,18 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 32,
'min_temp': 18,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 32,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 18,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'none',
'bio_mode',
'smart_mode',
]),
'target_temp_step': 0.5,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 0.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -52,24 +52,24 @@
# name: test_dynamic_new_devices[climate.mock_aquarium_mock_heater-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 24.2,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 24.2,
'friendly_name': 'Mock Heater',
'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.HEATING: 'heating'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 32,
'min_temp': 18,
'preset_mode': 'none',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 32,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 18,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'none',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'none',
'bio_mode',
'smart_mode',
]),
'supported_features': <ClimateEntityFeature: 401>,
'target_temp_step': 0.5,
'temperature': 25.5,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 0.5,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 25.5,
}),
'context': <ANY>,
'entity_id': 'climate.mock_aquarium_mock_heater',
@@ -86,18 +86,18 @@
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 32,
'min_temp': 18,
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 32,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 18,
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'none',
'bio_mode',
'smart_mode',
]),
'target_temp_step': 0.5,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 0.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -132,24 +132,24 @@
# name: test_setup_heater[climate.mock_aquarium_mock_heater-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 24.2,
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 24.2,
'friendly_name': 'Mock Heater',
'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.HEATING: 'heating'>,
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 32,
'min_temp': 18,
'preset_mode': 'none',
'preset_modes': list([
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 32,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 18,
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'none',
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
'none',
'bio_mode',
'smart_mode',
]),
'supported_features': <ClimateEntityFeature: 401>,
'target_temp_step': 0.5,
'temperature': 25.5,
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 0.5,
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 25.5,
}),
'context': <ANY>,
'entity_id': 'climate.mock_aquarium_mock_heater',
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'manual',
'pulse',
'bio',
@@ -46,7 +46,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock classicVARIO Filter mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'manual',
'pulse',
'bio',
@@ -67,7 +67,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -119,7 +119,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock filter Constant flow speed',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -153,7 +153,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -205,7 +205,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock filter Day speed',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -239,7 +239,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'manual',
'constant_flow',
'pulse',
@@ -280,7 +280,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock filter Filter mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'manual',
'constant_flow',
'pulse',
@@ -302,7 +302,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -354,7 +354,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock filter High pulse speed',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -388,7 +388,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -440,7 +440,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock filter Low pulse speed',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -474,7 +474,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'35',
'37.5',
'40.5',
@@ -526,7 +526,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock filter Manual speed',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'35',
'37.5',
'40.5',
@@ -560,7 +560,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -612,7 +612,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock filter Night speed',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'400',
'440',
'480',
@@ -646,7 +646,7 @@
]),
'area_id': None,
'capabilities': dict({
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'constant',
'daycycle',
]),
@@ -685,7 +685,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock reeflex Operation mode',
'options': list([
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'constant',
'daycycle',
]),

Some files were not shown because too many files have changed in this diff Show More