Compare commits

..

2 Commits

Author SHA1 Message Date
Daniel Hjelseth Høyer
56423f5712 Tibber, handle failed update
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-24 06:35:58 +01:00
Daniel Hjelseth Høyer
e55f89a800 Tibber, handle failed update
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-22 08:25:37 +01:00
109 changed files with 859 additions and 4385 deletions

4
CODEOWNERS generated
View File

@@ -784,8 +784,6 @@ build.json @home-assistant/supervisor
/tests/components/igloohome/ @keithle888
/homeassistant/components/ign_sismologia/ @exxamalte
/tests/components/ign_sismologia/ @exxamalte
/homeassistant/components/illuminance/ @home-assistant/core
/tests/components/illuminance/ @home-assistant/core
/homeassistant/components/image/ @home-assistant/core
/tests/components/image/ @home-assistant/core
/homeassistant/components/image_processing/ @home-assistant/core
@@ -1705,8 +1703,6 @@ build.json @home-assistant/supervisor
/tests/components/tellduslive/ @fredrike
/homeassistant/components/teltonika/ @karlbeecken
/tests/components/teltonika/ @karlbeecken
/homeassistant/components/temperature/ @home-assistant/core
/tests/components/temperature/ @home-assistant/core
/homeassistant/components/template/ @Petro31 @home-assistant/core
/tests/components/template/ @Petro31 @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77

View File

@@ -245,10 +245,8 @@ DEFAULT_INTEGRATIONS = {
"garage_door",
"gate",
"humidity",
"illuminance",
"motion",
"occupancy",
"temperature",
"window",
}
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {

View File

@@ -155,7 +155,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"gate",
"humidifier",
"humidity",
"illuminance",
"input_boolean",
"lawn_mower",
"light",
"lock",
@@ -169,7 +169,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"select",
"siren",
"switch",
"temperature",
"text",
"update",
"vacuum",

View File

@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==3.1.2",
"habluetooth==5.11.1"
"habluetooth==5.10.2"
]
}

View File

@@ -462,10 +462,6 @@
"below": {
"description": "Trigger when the target temperature is below this value.",
"name": "Below"
},
"unit": {
"description": "All values will be converted to this unit when evaluating the trigger.",
"name": "Unit of measurement"
}
},
"name": "Climate-control device target temperature changed"
@@ -485,10 +481,6 @@
"description": "Type of threshold crossing to trigger on.",
"name": "Threshold type"
},
"unit": {
"description": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::description%]",
"name": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::name%]"
},
"upper_limit": {
"description": "Upper threshold limit.",
"name": "Upper threshold"

View File

@@ -2,15 +2,12 @@
import voluptuous as vol
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
from homeassistant.core import HomeAssistant, State
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityNumericalStateChangedTriggerWithUnitBase,
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
EntityNumericalStateTriggerWithUnitBase,
EntityTargetStateTriggerBase,
Trigger,
TriggerConfig,
@@ -19,7 +16,6 @@ from homeassistant.helpers.trigger import (
make_entity_target_state_trigger,
make_entity_transition_trigger,
)
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
@@ -48,33 +44,6 @@ class HVACModeChangedTrigger(EntityTargetStateTriggerBase):
self._to_states = set(self._options[CONF_HVAC_MODE])
class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitBase):
"""Mixin for climate target temperature triggers with unit conversion."""
_base_unit = UnitOfTemperature.CELSIUS
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _get_entity_unit(self, state: State) -> str | None:
"""Get the temperature unit of a climate entity from its state."""
# Climate entities convert temperatures to the system unit via show_temp
return self._hass.config.units.temperature_unit
class ClimateTargetTemperatureChangedTrigger(
_ClimateTargetTemperatureTriggerMixin,
EntityNumericalStateChangedTriggerWithUnitBase,
):
"""Trigger for climate target temperature value changes."""
class ClimateTargetTemperatureCrossedThresholdTrigger(
_ClimateTargetTemperatureTriggerMixin,
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
):
"""Trigger for climate target temperature value crossing a threshold."""
TRIGGERS: dict[str, type[Trigger]] = {
"hvac_mode_changed": HVACModeChangedTrigger,
"started_cooling": make_entity_target_state_trigger(
@@ -84,15 +53,17 @@ TRIGGERS: dict[str, type[Trigger]] = {
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
),
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
),
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
),
"target_temperature_changed": make_entity_numerical_state_changed_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
),
"target_temperature_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
),
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
"target_temperature_crossed_threshold": ClimateTargetTemperatureCrossedThresholdTrigger,
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
"turned_on": make_entity_transition_trigger(
DOMAIN,

View File

@@ -14,29 +14,7 @@
- last
- any
.number_or_entity_humidity: &number_or_entity_humidity
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "%"
- domain: sensor
device_class: humidity
- domain: number
device_class: humidity
translation_key: number_or_entity
.number_or_entity_temperature: &number_or_entity_temperature
.number_or_entity: &number_or_entity
required: false
selector:
choose:
@@ -49,24 +27,12 @@
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "°C"
- "°F"
- domain: sensor
device_class: temperature
- domain: number
device_class: temperature
domain:
- input_number
- number
- sensor
translation_key: number_or_entity
.trigger_unit_temperature: &trigger_unit_temperature
required: false
selector:
select:
options:
- "°C"
- "°F"
.trigger_threshold_type: &trigger_threshold_type
required: true
default: above
@@ -103,29 +69,27 @@ hvac_mode_changed:
target_humidity_changed:
target: *trigger_climate_target
fields:
above: *number_or_entity_humidity
below: *number_or_entity_humidity
above: *number_or_entity
below: *number_or_entity
target_humidity_crossed_threshold:
target: *trigger_climate_target
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_humidity
upper_limit: *number_or_entity_humidity
lower_limit: *number_or_entity
upper_limit: *number_or_entity
target_temperature_changed:
target: *trigger_climate_target
fields:
above: *number_or_entity_temperature
below: *number_or_entity_temperature
unit: *trigger_unit_temperature
above: *number_or_entity
below: *number_or_entity
target_temperature_crossed_threshold:
target: *trigger_climate_target
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_temperature
upper_limit: *number_or_entity_temperature
unit: *trigger_unit_temperature
lower_limit: *number_or_entity
upper_limit: *number_or_entity

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.2.0"]
"requirements": ["evohome-async==1.1.3"]
}

View File

@@ -124,17 +124,15 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
until = dt_util.as_utc(until) if until else None
if operation_mode == STATE_ON:
await self.coordinator.call_client_api(
self._evo_device.set_on(until=until)
)
await self.coordinator.call_client_api(self._evo_device.on(until=until))
else: # STATE_OFF
await self.coordinator.call_client_api(
self._evo_device.set_off(until=until)
self._evo_device.off(until=until)
)
async def async_turn_away_mode_on(self) -> None:
"""Turn away mode on."""
await self.coordinator.call_client_api(self._evo_device.set_off())
await self.coordinator.call_client_api(self._evo_device.off())
async def async_turn_away_mode_off(self) -> None:
"""Turn away mode off."""
@@ -142,8 +140,8 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on."""
await self.coordinator.call_client_api(self._evo_device.set_on())
await self.coordinator.call_client_api(self._evo_device.on())
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off."""
await self.coordinator.call_client_api(self._evo_device.set_off())
await self.coordinator.call_client_api(self._evo_device.off())

View File

@@ -13,7 +13,6 @@ from fritzconnection.core.exceptions import (
FritzSecurityError,
FritzServiceError,
)
from requests.exceptions import ConnectionError
from homeassistant.const import Platform
@@ -69,7 +68,6 @@ BUTTON_TYPE_WOL = "WakeOnLan"
UPTIME_DEVIATION = 5
FRITZ_EXCEPTIONS = (
ConnectionError,
FritzActionError,
FritzActionFailedError,
FritzConnectionException,

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["greenplanet-energy-api==0.1.10"],
"requirements": ["greenplanet-energy-api==0.1.4"],
"single_config_entry": true
}

View File

@@ -1,17 +0,0 @@
"""Integration for illuminance triggers."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
DOMAIN = "illuminance"
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
__all__ = []
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
return True

View File

@@ -1,16 +0,0 @@
{
"triggers": {
"changed": {
"trigger": "mdi:brightness-6"
},
"cleared": {
"trigger": "mdi:brightness-4"
},
"crossed_threshold": {
"trigger": "mdi:brightness-6"
},
"detected": {
"trigger": "mdi:brightness-7"
}
}
}

View File

@@ -1,8 +0,0 @@
{
"domain": "illuminance",
"name": "Illuminance",
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/illuminance",
"integration_type": "system",
"quality_scale": "internal"
}

View File

@@ -1,88 +0,0 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
"trigger_behavior_name": "Behavior"
},
"selector": {
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
},
"trigger_threshold_type": {
"options": {
"above": "Above",
"below": "Below",
"between": "Between",
"outside": "Outside"
}
}
},
"title": "Illuminance",
"triggers": {
"changed": {
"description": "Triggers after one or more illuminance values change.",
"fields": {
"above": {
"description": "Only trigger when illuminance is above this value.",
"name": "Above"
},
"below": {
"description": "Only trigger when illuminance is below this value.",
"name": "Below"
}
},
"name": "Illuminance changed"
},
"cleared": {
"description": "Triggers after one or more light sensors stop detecting light.",
"fields": {
"behavior": {
"description": "[%key:component::illuminance::common::trigger_behavior_description%]",
"name": "[%key:component::illuminance::common::trigger_behavior_name%]"
}
},
"name": "Light cleared"
},
"crossed_threshold": {
"description": "Triggers after one or more illuminance values cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::illuminance::common::trigger_behavior_description%]",
"name": "[%key:component::illuminance::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "The lower limit of the threshold.",
"name": "Lower limit"
},
"threshold_type": {
"description": "The type of threshold to use.",
"name": "Threshold type"
},
"upper_limit": {
"description": "The upper limit of the threshold.",
"name": "Upper limit"
}
},
"name": "Illuminance crossed threshold"
},
"detected": {
"description": "Triggers after one or more light sensors start detecting light.",
"fields": {
"behavior": {
"description": "[%key:component::illuminance::common::trigger_behavior_description%]",
"name": "[%key:component::illuminance::common::trigger_behavior_name%]"
}
},
"name": "Light detected"
}
}
}

View File

@@ -1,46 +0,0 @@
"""Provides triggers for illuminance."""
from __future__ import annotations
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import LIGHT_LUX, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.trigger import (
Trigger,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
make_entity_target_state_trigger,
)
ILLUMINANCE_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.ILLUMINANCE),
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.ILLUMINANCE),
}
TRIGGERS: dict[str, type[Trigger]] = {
"detected": make_entity_target_state_trigger(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.LIGHT)},
STATE_ON,
),
"cleared": make_entity_target_state_trigger(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.LIGHT)},
STATE_OFF,
),
"changed": make_entity_numerical_state_changed_trigger(
ILLUMINANCE_DOMAIN_SPECS, valid_unit=LIGHT_LUX
),
"crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
ILLUMINANCE_DOMAIN_SPECS, valid_unit=LIGHT_LUX
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for illuminance."""
return TRIGGERS

View File

@@ -1,79 +0,0 @@
.trigger_common_fields: &trigger_common_fields
behavior: &trigger_behavior
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
.number_or_entity: &number_or_entity
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "lx"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "lx"
- domain: sensor
device_class: illuminance
- domain: number
device_class: illuminance
translation_key: number_or_entity
.trigger_threshold_type: &trigger_threshold_type
required: true
default: above
selector:
select:
options:
- above
- below
- between
- outside
translation_key: trigger_threshold_type
.trigger_binary_target: &trigger_binary_target
entity:
- domain: binary_sensor
device_class: light
.trigger_numerical_target: &trigger_numerical_target
entity:
- domain: number
device_class: illuminance
- domain: sensor
device_class: illuminance
detected:
fields: *trigger_common_fields
target: *trigger_binary_target
cleared:
fields: *trigger_common_fields
target: *trigger_binary_target
changed:
target: *trigger_numerical_target
fields:
above: *number_or_entity
below: *number_or_entity
crossed_threshold:
target: *trigger_numerical_target
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity
upper_limit: *number_or_entity

View File

@@ -2,9 +2,22 @@
from __future__ import annotations
from homeassistant.const import Platform
from aioimmich import Immich
from aioimmich.const import CONNECT_ERRORS
from aioimmich.exceptions import ImmichUnauthorizedError
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_PORT,
CONF_SSL,
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
@@ -25,7 +38,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ImmichConfigEntry) -> bool:
"""Set up Immich from a config entry."""
coordinator = ImmichDataUpdateCoordinator(hass, entry)
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
immich = Immich(
session,
entry.data[CONF_API_KEY],
entry.data[CONF_HOST],
entry.data[CONF_PORT],
entry.data[CONF_SSL],
"home-assistant",
)
try:
user_info = await immich.users.async_get_my_user()
except ImmichUnauthorizedError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_error",
) from err
except CONNECT_ERRORS as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
coordinator = ImmichDataUpdateCoordinator(hass, entry, immich, user_info.is_admin)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

View File

@@ -16,19 +16,11 @@ from aioimmich.server.models import (
ImmichServerVersionCheck,
)
from awesomeversion import AwesomeVersion
from yarl import URL
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_PORT,
CONF_SSL,
CONF_VERIFY_SSL,
)
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -54,49 +46,24 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
config_entry: ImmichConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: ImmichConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, api: Immich, is_admin: bool
) -> None:
"""Initialize the data update coordinator."""
self.api = Immich(
async_get_clientsession(hass, config_entry.data[CONF_VERIFY_SSL]),
config_entry.data[CONF_API_KEY],
config_entry.data[CONF_HOST],
config_entry.data[CONF_PORT],
config_entry.data[CONF_SSL],
"home-assistant",
)
self.is_admin = False
self.configuration_url = str(
URL.build(
scheme="https" if config_entry.data[CONF_SSL] else "http",
host=config_entry.data[CONF_HOST],
port=config_entry.data[CONF_PORT],
)
self.api = api
self.is_admin = is_admin
self.configuration_url = (
f"{'https' if entry.data[CONF_SSL] else 'http'}://"
f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}"
)
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
config_entry=entry,
name=DOMAIN,
update_interval=timedelta(seconds=60),
)
async def _async_setup(self) -> None:
"""Handle setup of the coordinator."""
try:
user_info = await self.api.users.async_get_my_user()
except ImmichUnauthorizedError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_error",
) from err
except CONNECT_ERRORS as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
self.is_admin = user_info.is_admin
async def _async_update_data(self) -> ImmichData:
"""Update data via internal method."""
try:

View File

@@ -20,5 +20,13 @@
"turn_on": {
"service": "mdi:toggle-switch"
}
},
"triggers": {
"turned_off": {
"trigger": "mdi:toggle-switch-off"
},
"turned_on": {
"trigger": "mdi:toggle-switch"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted toggles to trigger on.",
"trigger_behavior_name": "Behavior"
},
"entity_component": {
"_": {
"name": "[%key:component::input_boolean::title%]",
@@ -17,6 +21,15 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"reload": {
"description": "Reloads helpers from the YAML-configuration.",
@@ -35,5 +48,27 @@
"name": "[%key:common::action::turn_on%]"
}
},
"title": "Input boolean"
"title": "Input boolean",
"triggers": {
"turned_off": {
"description": "Triggers after one or more toggles turn off.",
"fields": {
"behavior": {
"description": "[%key:component::input_boolean::common::trigger_behavior_description%]",
"name": "[%key:component::input_boolean::common::trigger_behavior_name%]"
}
},
"name": "Toggle turned off"
},
"turned_on": {
"description": "Triggers after one or more toggles turn on.",
"fields": {
"behavior": {
"description": "[%key:component::input_boolean::common::trigger_behavior_description%]",
"name": "[%key:component::input_boolean::common::trigger_behavior_name%]"
}
},
"name": "Toggle turned on"
}
}
}

View File

@@ -0,0 +1,17 @@
"""Provides triggers for input booleans."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
from . import DOMAIN
TRIGGERS: dict[str, type[Trigger]] = {
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for input booleans."""
return TRIGGERS

View File

@@ -0,0 +1,18 @@
.trigger_common: &trigger_common
target:
entity:
domain: input_boolean
fields:
behavior:
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
turned_off: *trigger_common
turned_on: *trigger_common

View File

@@ -114,26 +114,24 @@ class KnxYamlBinarySensor(_KnxBinarySensor, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of KNX binary sensor."""
self._device = XknxBinarySensor(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_state=config[CONF_STATE_ADDRESS],
invert=config[CONF_INVERT],
sync_state=config[CONF_SYNC_STATE],
ignore_internal_state=config[CONF_IGNORE_INTERNAL_STATE],
context_timeout=config.get(CONF_CONTEXT_TIMEOUT),
reset_after=config.get(CONF_RESET_AFTER),
always_callback=True,
)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.remote_value.group_address_state),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=XknxBinarySensor(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_state=config[CONF_STATE_ADDRESS],
invert=config[CONF_INVERT],
sync_state=config[CONF_SYNC_STATE],
ignore_internal_state=config[CONF_IGNORE_INTERNAL_STATE],
context_timeout=config.get(CONF_CONTEXT_TIMEOUT),
reset_after=config.get(CONF_RESET_AFTER),
always_callback=True,
),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_force_update = self._device.ignore_internal_state
self._attr_unique_id = str(self._device.remote_value.group_address_state)
class KnxUiBinarySensor(_KnxBinarySensor, KnxUiEntity):

View File

@@ -35,18 +35,19 @@ class KNXButton(KnxYamlEntity, ButtonEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX button."""
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}",
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
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]
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = (
f"{self._device.remote_value.group_address}_{self._payload}"
)
async def async_press(self) -> None:

View File

@@ -119,7 +119,7 @@ async def async_setup_entry(
async_add_entities(entities)
def _create_climate_yaml(xknx: XKNX, config: ConfigType) -> XknxClimate:
def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
"""Return a KNX Climate device to be used within XKNX."""
climate_mode = XknxClimateMode(
xknx,
@@ -646,17 +646,9 @@ class KnxYamlClimate(_KnxClimate, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of a KNX climate device."""
self._device = _create_climate_yaml(knx_module.xknx, config)
super().__init__(
knx_module=knx_module,
unique_id=(
f"{self._device.temperature.group_address_state}_"
f"{self._device.target_temperature.group_address_state}_"
f"{self._device.target_temperature.group_address}_"
f"{self._device._setpoint_shift.group_address}" # noqa: SLF001
),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=_create_climate(knx_module.xknx, config),
)
default_hvac_mode: HVACMode = config[ClimateConf.DEFAULT_CONTROLLER_MODE]
fan_max_step = config[ClimateConf.FAN_MAX_STEP]
@@ -668,6 +660,14 @@ class KnxYamlClimate(_KnxClimate, KnxYamlEntity):
fan_zero_mode=fan_zero_mode,
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = (
f"{self._device.temperature.group_address_state}_"
f"{self._device.target_temperature.group_address_state}_"
f"{self._device.target_temperature.group_address}_"
f"{self._device._setpoint_shift.group_address}" # noqa: SLF001
)
class KnxUiClimate(_KnxClimate, KnxUiEntity):
"""Representation of a KNX climate device configured from the UI."""

View File

@@ -191,34 +191,36 @@ class KnxYamlCover(_KnxCover, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize the cover."""
self._device = XknxCover(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS),
group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS),
group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS),
group_address_position_state=config.get(
CoverSchema.CONF_POSITION_STATE_ADDRESS
),
group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS),
group_address_angle_state=config.get(CoverSchema.CONF_ANGLE_STATE_ADDRESS),
group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS),
travel_time_down=config[CoverConf.TRAVELLING_TIME_DOWN],
travel_time_up=config[CoverConf.TRAVELLING_TIME_UP],
invert_updown=config[CoverConf.INVERT_UPDOWN],
invert_position=config[CoverConf.INVERT_POSITION],
invert_angle=config[CoverConf.INVERT_ANGLE],
)
super().__init__(
knx_module=knx_module,
unique_id=(
f"{self._device.updown.group_address}_"
f"{self._device.position_target.group_address}"
device=XknxCover(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS),
group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS),
group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS),
group_address_position_state=config.get(
CoverSchema.CONF_POSITION_STATE_ADDRESS
),
group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS),
group_address_angle_state=config.get(
CoverSchema.CONF_ANGLE_STATE_ADDRESS
),
group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS),
travel_time_down=config[CoverConf.TRAVELLING_TIME_DOWN],
travel_time_up=config[CoverConf.TRAVELLING_TIME_UP],
invert_updown=config[CoverConf.INVERT_UPDOWN],
invert_position=config[CoverConf.INVERT_POSITION],
invert_angle=config[CoverConf.INVERT_ANGLE],
),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
)
self.init_base()
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = (
f"{self._device.updown.group_address}_"
f"{self._device.position_target.group_address}"
)
if custom_device_class := config.get(CONF_DEVICE_CLASS):
self._attr_device_class = custom_device_class

View File

@@ -105,21 +105,20 @@ class KnxYamlDate(_KNXDate, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX date."""
self._device = XknxDateDevice(
knx_module.xknx,
name=config[CONF_NAME],
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
sync_state=config[CONF_SYNC_STATE],
)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.remote_value.group_address),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=XknxDateDevice(
knx_module.xknx,
name=config[CONF_NAME],
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
sync_state=config[CONF_SYNC_STATE],
),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
class KnxUiDate(_KNXDate, KnxUiEntity):

View File

@@ -110,21 +110,20 @@ class KnxYamlDateTime(_KNXDateTime, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX datetime."""
self._device = XknxDateTimeDevice(
knx_module.xknx,
name=config[CONF_NAME],
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
sync_state=config[CONF_SYNC_STATE],
)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.remote_value.group_address),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=XknxDateTimeDevice(
knx_module.xknx,
name=config[CONF_NAME],
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
sync_state=config[CONF_SYNC_STATE],
),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
class KnxUiDateTime(_KNXDateTime, KnxUiEntity):

View File

@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any
from xknx.devices import Device as XknxDevice
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, EntityCategory
from homeassistant.const import CONF_ENTITY_CATEGORY, EntityCategory
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import EntityPlatform
@@ -52,11 +52,14 @@ class _KnxEntityBase(Entity):
"""Representation of a KNX entity."""
_attr_should_poll = False
_attr_unique_id: str
_knx_module: KNXModule
_device: XknxDevice
@property
def name(self) -> str:
"""Return the name of the KNX device."""
return self._device.name
@property
def available(self) -> bool:
"""Return True if entity is available."""
@@ -97,23 +100,16 @@ class _KnxEntityBase(Entity):
class KnxYamlEntity(_KnxEntityBase):
"""Representation of a KNX entity configured from YAML."""
def __init__(
self,
knx_module: KNXModule,
unique_id: str,
name: str,
entity_category: EntityCategory | None,
) -> None:
def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None:
"""Initialize the YAML entity."""
self._knx_module = knx_module
self._attr_name = name or None
self._attr_unique_id = unique_id
self._attr_entity_category = entity_category
self._device = device
class KnxUiEntity(_KnxEntityBase):
"""Representation of a KNX UI entity."""
_attr_unique_id: str
_attr_has_entity_name = True
def __init__(
@@ -121,8 +117,6 @@ class KnxUiEntity(_KnxEntityBase):
) -> None:
"""Initialize the UI entity."""
self._knx_module = knx_module
self._attr_name = entity_config[CONF_NAME]
self._attr_unique_id = unique_id
if entity_category := entity_config.get(CONF_ENTITY_CATEGORY):
self._attr_entity_category = EntityCategory(entity_category)

View File

@@ -208,32 +208,35 @@ class KnxYamlFan(_KnxFan, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of KNX fan."""
max_step = config.get(FanConf.MAX_STEP)
self._device = XknxFan(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_speed=config.get(KNX_ADDRESS),
group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS),
group_address_oscillation=config.get(FanSchema.CONF_OSCILLATION_ADDRESS),
group_address_oscillation_state=config.get(
FanSchema.CONF_OSCILLATION_STATE_ADDRESS
),
group_address_switch=config.get(FanSchema.CONF_SWITCH_ADDRESS),
group_address_switch_state=config.get(FanSchema.CONF_SWITCH_STATE_ADDRESS),
max_step=max_step,
sync_state=config.get(CONF_SYNC_STATE, True),
)
super().__init__(
knx_module=knx_module,
unique_id=(
str(self._device.speed.group_address)
if self._device.speed.group_address
else str(self._device.switch.group_address)
device=XknxFan(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_speed=config.get(KNX_ADDRESS),
group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS),
group_address_oscillation=config.get(
FanSchema.CONF_OSCILLATION_ADDRESS
),
group_address_oscillation_state=config.get(
FanSchema.CONF_OSCILLATION_STATE_ADDRESS
),
group_address_switch=config.get(FanSchema.CONF_SWITCH_ADDRESS),
group_address_switch_state=config.get(
FanSchema.CONF_SWITCH_STATE_ADDRESS
),
max_step=max_step,
sync_state=config.get(CONF_SYNC_STATE, True),
),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
)
# FanSpeedMode.STEP if max_step is set
self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
if self._device.speed.group_address:
self._attr_unique_id = str(self._device.speed.group_address)
else:
self._attr_unique_id = str(self._device.switch.group_address)
class KnxUiFan(_KnxFan, KnxUiEntity):

View File

@@ -558,16 +558,15 @@ class KnxYamlLight(_KnxLight, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of KNX light."""
self._device = _create_yaml_light(knx_module.xknx, config)
super().__init__(
knx_module=knx_module,
unique_id=self._device_unique_id(),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=_create_yaml_light(knx_module.xknx, config),
)
self._attr_color_mode = next(iter(self.supported_color_modes))
self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN]
self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN]
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = self._device_unique_id()
def _device_unique_id(self) -> str:
"""Return unique id for this device."""

View File

@@ -46,13 +46,12 @@ class KNXNotify(KnxYamlEntity, NotifyEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX notification."""
self._device = _create_notification_instance(knx_module.xknx, config)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.remote_value.group_address),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=_create_notification_instance(knx_module.xknx, config),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a notification to knx bus."""

View File

@@ -109,19 +109,16 @@ class KnxYamlNumber(_KnxNumber, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX number."""
self._device = NumericValue(
knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
value_type=config[CONF_TYPE],
)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.sensor_value.group_address),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=NumericValue(
knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
value_type=config[CONF_TYPE],
),
)
dpt_string = self._device.sensor_value.dpt_class.dpt_number_str()
dpt_info = get_supported_dpts()[dpt_string]
@@ -134,6 +131,7 @@ class KnxYamlNumber(_KnxNumber, KnxYamlEntity):
dpt_info["sensor_device_class"],
),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_mode = config[CONF_MODE]
self._attr_native_max_value = config.get(
NumberConf.MAX,
@@ -151,6 +149,7 @@ class KnxYamlNumber(_KnxNumber, KnxYamlEntity):
CONF_UNIT_OF_MEASUREMENT,
dpt_info["unit"],
)
self._attr_unique_id = str(self._device.sensor_value.group_address)
self._device.sensor_value.value = max(0, self._attr_native_min_value)

View File

@@ -83,19 +83,18 @@ class KnxYamlScene(_KnxScene, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize KNX scene."""
self._device = XknxScene(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
scene_number=config[SceneSchema.CONF_SCENE_NUMBER],
)
super().__init__(
knx_module=knx_module,
unique_id=(
f"{self._device.scene_value.group_address}_{self._device.scene_number}"
device=XknxScene(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
scene_number=config[SceneSchema.CONF_SCENE_NUMBER],
),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = (
f"{self._device.scene_value.group_address}_{self._device.scene_number}"
)

View File

@@ -199,22 +199,16 @@ class KNXPlatformSchema(ABC):
}
COMMON_ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=""): cv.string,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
class BinarySensorSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX binary sensors."""
PLATFORM = Platform.BINARY_SENSOR
DEFAULT_NAME = "KNX Binary Sensor"
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean,
vol.Optional(CONF_INVERT, default=False): cv.boolean,
@@ -224,6 +218,7 @@ class BinarySensorSchema(KNXPlatformSchema):
),
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_RESET_AFTER): cv.positive_float,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
),
)
@@ -235,6 +230,7 @@ class ButtonSchema(KNXPlatformSchema):
PLATFORM = Platform.BUTTON
CONF_VALUE = "value"
DEFAULT_NAME = "KNX Button"
payload_or_value_msg = f"Please use only one of `{CONF_PAYLOAD}` or `{CONF_VALUE}`"
length_or_type_msg = (
@@ -242,8 +238,9 @@ class ButtonSchema(KNXPlatformSchema):
)
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(KNX_ADDRESS): ga_validator,
vol.Exclusive(
CONF_PAYLOAD, "payload_or_value", msg=payload_or_value_msg
@@ -257,6 +254,7 @@ class ButtonSchema(KNXPlatformSchema):
vol.Exclusive(
CONF_TYPE, "length_or_type", msg=length_or_type_msg
): object,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
),
vol.Any(
@@ -324,6 +322,7 @@ class ClimateSchema(KNXPlatformSchema):
CONF_SWING_HORIZONTAL_ADDRESS = "swing_horizontal_address"
CONF_SWING_HORIZONTAL_STATE_ADDRESS = "swing_horizontal_state_address"
DEFAULT_NAME = "KNX Climate"
DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010"
DEFAULT_SETPOINT_SHIFT_MAX = 6
DEFAULT_SETPOINT_SHIFT_MIN = -6
@@ -332,8 +331,9 @@ class ClimateSchema(KNXPlatformSchema):
DEFAULT_FAN_SPEED_MODE = "percent"
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(
ClimateConf.SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
): vol.All(int, vol.Range(min=0, max=32)),
@@ -399,6 +399,7 @@ class ClimateSchema(KNXPlatformSchema):
): vol.Coerce(HVACMode),
vol.Optional(ClimateConf.MIN_TEMP): vol.Coerce(float),
vol.Optional(ClimateConf.MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_FAN_SPEED_ADDRESS): ga_list_validator,
vol.Optional(CONF_FAN_SPEED_STATE_ADDRESS): ga_list_validator,
vol.Optional(ClimateConf.FAN_MAX_STEP, default=3): cv.byte,
@@ -432,10 +433,12 @@ class CoverSchema(KNXPlatformSchema):
CONF_ANGLE_STATE_ADDRESS = "angle_state_address"
DEFAULT_TRAVEL_TIME = 25
DEFAULT_NAME = "KNX Cover"
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_list_validator,
vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_list_validator,
vol.Optional(CONF_STOP_ADDRESS): ga_list_validator,
@@ -453,6 +456,7 @@ class CoverSchema(KNXPlatformSchema):
vol.Optional(CoverConf.INVERT_POSITION, default=False): cv.boolean,
vol.Optional(CoverConf.INVERT_ANGLE, default=False): cv.boolean,
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
),
vol.Any(
@@ -477,12 +481,16 @@ class DateSchema(KNXPlatformSchema):
PLATFORM = Platform.DATE
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
DEFAULT_NAME = "KNX Date"
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
@@ -492,12 +500,16 @@ class DateTimeSchema(KNXPlatformSchema):
PLATFORM = Platform.DATETIME
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
DEFAULT_NAME = "KNX DateTime"
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
@@ -559,9 +571,12 @@ class FanSchema(KNXPlatformSchema):
CONF_SWITCH_ADDRESS = "switch_address"
CONF_SWITCH_STATE_ADDRESS = "switch_state_address"
DEFAULT_NAME = "KNX Fan"
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_SWITCH_ADDRESS): ga_list_validator,
@@ -569,6 +584,7 @@ class FanSchema(KNXPlatformSchema):
vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator,
vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator,
vol.Optional(FanConf.MAX_STEP): cv.byte,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
}
),
@@ -613,6 +629,7 @@ class LightSchema(KNXPlatformSchema):
CONF_MIN_KELVIN = "min_kelvin"
CONF_MAX_KELVIN = "max_kelvin"
DEFAULT_NAME = "KNX Light"
DEFAULT_COLOR_TEMP_MODE = "absolute"
DEFAULT_MIN_KELVIN = 2700 # 370 mireds
DEFAULT_MAX_KELVIN = 6000 # 166 mireds
@@ -644,8 +661,9 @@ class LightSchema(KNXPlatformSchema):
)
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_BRIGHTNESS_ADDRESS): ga_list_validator,
@@ -695,6 +713,7 @@ class LightSchema(KNXPlatformSchema):
vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
),
vol.Any(
@@ -740,10 +759,14 @@ class NotifySchema(KNXPlatformSchema):
PLATFORM = Platform.NOTIFY
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
DEFAULT_NAME = "KNX Notify"
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator,
vol.Required(KNX_ADDRESS): ga_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
@@ -752,10 +775,12 @@ class NumberSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX numbers."""
PLATFORM = Platform.NUMBER
DEFAULT_NAME = "KNX Number"
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(
NumberMode
@@ -768,6 +793,7 @@ class NumberSchema(KNXPlatformSchema):
vol.Optional(NumberConf.STEP): cv.positive_float,
vol.Optional(CONF_DEVICE_CLASS): NUMBER_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
),
_number_limit_sub_validator,
@@ -781,12 +807,15 @@ class SceneSchema(KNXPlatformSchema):
CONF_SCENE_NUMBER = "scene_number"
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
DEFAULT_NAME = "KNX SCENE"
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Required(SceneConf.SCENE_NUMBER): vol.All(
vol.Coerce(int), vol.Range(min=1, max=64)
),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
@@ -798,10 +827,12 @@ class SelectSchema(KNXPlatformSchema):
CONF_OPTION = "option"
CONF_OPTIONS = "options"
DEFAULT_NAME = "KNX Select"
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Required(CONF_PAYLOAD_LENGTH): vol.All(
@@ -815,6 +846,7 @@ class SelectSchema(KNXPlatformSchema):
],
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
),
select_options_sub_validator,
@@ -829,10 +861,12 @@ class SensorSchema(KNXPlatformSchema):
CONF_ALWAYS_CALLBACK = "always_callback"
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
CONF_SYNC_STATE = CONF_SYNC_STATE
DEFAULT_NAME = "KNX Sensor"
ENTITY_SCHEMA = vol.All(
COMMON_ENTITY_SCHEMA.extend(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean,
vol.Optional(CONF_SENSOR_STATE_CLASS): STATE_CLASSES_SCHEMA,
@@ -840,6 +874,7 @@ class SensorSchema(KNXPlatformSchema):
vol.Required(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
),
_sensor_attribute_sub_validator,
@@ -854,13 +889,16 @@ class SwitchSchema(KNXPlatformSchema):
CONF_INVERT = CONF_INVERT
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
DEFAULT_NAME = "KNX Switch"
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_INVERT, default=False): cv.boolean,
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
@@ -870,13 +908,17 @@ class TextSchema(KNXPlatformSchema):
PLATFORM = Platform.TEXT
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
DEFAULT_NAME = "KNX Text"
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator,
vol.Optional(CONF_MODE, default=TextMode.TEXT): vol.Coerce(TextMode),
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
@@ -886,12 +928,16 @@ class TimeSchema(KNXPlatformSchema):
PLATFORM = Platform.TIME
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
DEFAULT_NAME = "KNX Time"
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
@@ -916,21 +962,27 @@ class WeatherSchema(KNXPlatformSchema):
CONF_KNX_AIR_PRESSURE_ADDRESS = "address_air_pressure"
CONF_KNX_HUMIDITY_ADDRESS = "address_humidity"
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
{
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator,
}
DEFAULT_NAME = "KNX Weather Station"
ENTITY_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
),
)

View File

@@ -65,12 +65,9 @@ class KNXSelect(KnxYamlEntity, SelectEntity, RestoreEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX select."""
self._device = _create_raw_value(knx_module.xknx, config)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.remote_value.group_address),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=_create_raw_value(knx_module.xknx, config),
)
self._option_payloads: dict[str, int] = {
option[SelectSchema.CONF_OPTION]: option[CONF_PAYLOAD]
@@ -78,6 +75,8 @@ class KNXSelect(KnxYamlEntity, SelectEntity, RestoreEntity):
}
self._attr_options = list(self._option_payloads)
self._attr_current_option = None
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
async def async_added_to_hass(self) -> None:
"""Restore last state."""

View File

@@ -202,19 +202,16 @@ class KnxYamlSensor(_KnxSensor, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of a KNX sensor."""
self._device = XknxSensor(
knx_module.xknx,
name=config[CONF_NAME],
group_address_state=config[SensorSchema.CONF_STATE_ADDRESS],
sync_state=config[CONF_SYNC_STATE],
always_callback=True,
value_type=config[CONF_TYPE],
)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.sensor_value.group_address_state),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=XknxSensor(
knx_module.xknx,
name=config[CONF_NAME],
group_address_state=config[SensorSchema.CONF_STATE_ADDRESS],
sync_state=config[CONF_SYNC_STATE],
always_callback=True,
value_type=config[CONF_TYPE],
),
)
dpt_string = self._device.sensor_value.dpt_class.dpt_number_str()
dpt_info = get_supported_dpts()[dpt_string]
@@ -223,6 +220,7 @@ class KnxYamlSensor(_KnxSensor, KnxYamlEntity):
CONF_DEVICE_CLASS,
dpt_info["sensor_device_class"],
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_extra_state_attributes = {}
self._attr_force_update = config[SensorSchema.CONF_ALWAYS_CALLBACK]
self._attr_native_unit_of_measurement = config.get(
@@ -233,6 +231,7 @@ class KnxYamlSensor(_KnxSensor, KnxYamlEntity):
CONF_STATE_CLASS,
dpt_info["sensor_state_class"],
)
self._attr_unique_id = str(self._device.sensor_value.group_address_state)
class KnxUiSensor(_KnxSensor, KnxUiEntity):

View File

@@ -107,21 +107,20 @@ class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of KNX switch."""
self._device = XknxSwitch(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
invert=config[SwitchSchema.CONF_INVERT],
)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.switch.group_address),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=XknxSwitch(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
invert=config[SwitchSchema.CONF_INVERT],
),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_unique_id = str(self._device.switch.group_address)
class KnxUiSwitch(_KnxSwitch, KnxUiEntity):

View File

@@ -112,21 +112,20 @@ class KnxYamlText(_KnxText, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX text."""
self._device = XknxNotification(
knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
value_type=config[CONF_TYPE],
)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.remote_value.group_address),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=XknxNotification(
knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
value_type=config[CONF_TYPE],
),
)
self._attr_mode = config[CONF_MODE]
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
class KnxUiText(_KnxText, KnxUiEntity):

View File

@@ -105,21 +105,20 @@ class KnxYamlTime(_KNXTime, KnxYamlEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX time."""
self._device = XknxTimeDevice(
knx_module.xknx,
name=config[CONF_NAME],
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
sync_state=config[CONF_SYNC_STATE],
)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device.remote_value.group_address),
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=XknxTimeDevice(
knx_module.xknx,
name=config[CONF_NAME],
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
sync_state=config[CONF_SYNC_STATE],
),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
class KnxUiTime(_KNXTime, KnxUiEntity):

View File

@@ -85,13 +85,12 @@ class KNXWeather(KnxYamlEntity, WeatherEntity):
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of a KNX sensor."""
self._device = _create_weather(knx_module.xknx, config)
super().__init__(
knx_module=knx_module,
unique_id=str(self._device._temperature.group_address_state), # noqa: SLF001
name=config[CONF_NAME],
entity_category=config.get(CONF_ENTITY_CATEGORY),
device=_create_weather(knx_module.xknx, config),
)
self._attr_unique_id = str(self._device._temperature.group_address_state) # noqa: SLF001
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
@property
def native_temperature(self) -> float | None:

View File

@@ -617,10 +617,8 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
pyrolytic = 323
descale = 326
evaporate_water = 327
rinse = 333
shabbat_program = 335
yom_tov = 336
hydroclean = 341
drying = 357, 2028
heat_crockery = 358
prove_dough = 359, 2023
@@ -725,7 +723,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
belgian_sponge_cake = 624
goose_unstuffed = 625
rack_of_lamb_with_vegetables = 634
yorkshire_pudding = 635, 2352
yorkshire_pudding = 635
meat_loaf = 636
defrost_meat = 647
defrost_vegetables = 654
@@ -1125,7 +1123,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
wholegrain_rice = 3376
parboiled_rice_steam_cooking = 3380
parboiled_rice_rapid_steam_cooking = 3381
basmati_rice_steam_cooking = 3382, 3383
basmati_rice_steam_cooking = 3383
basmati_rice_rapid_steam_cooking = 3384
jasmine_rice_steam_cooking = 3386
jasmine_rice_rapid_steam_cooking = 3387
@@ -1133,7 +1131,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
huanghuanian_rapid_steam_cooking = 3390
simiao_steam_cooking = 3392
simiao_rapid_steam_cooking = 3393
long_grain_rice_general_steam_cooking = 3394, 3395
long_grain_rice_general_steam_cooking = 3395
long_grain_rice_general_rapid_steam_cooking = 3396
chongming_steam_cooking = 3398
chongming_rapid_steam_cooking = 3399

View File

@@ -560,7 +560,6 @@
"hot_water": "Hot water",
"huanghuanian_rapid_steam_cooking": "Huanghuanian (rapid steam cooking)",
"huanghuanian_steam_cooking": "Huanghuanian (steam cooking)",
"hydroclean": "HydroClean",
"hygiene": "Hygiene",
"intensive": "Intensive",
"intensive_bake": "Intensive bake",

View File

@@ -13,7 +13,6 @@
"documentation": "https://www.home-assistant.io/integrations/opendisplay",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["opendisplay"],
"quality_scale": "silver",
"requirements": ["py-opendisplay==5.5.0"]
}

View File

@@ -13,5 +13,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["oralb_ble"],
"requirements": ["oralb-ble==1.1.0"]
"requirements": ["oralb-ble==1.0.2"]
}

View File

@@ -90,5 +90,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.1.0"]
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.0.2"]
}

View File

@@ -125,14 +125,6 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
),
entity_category=EntityCategory.CONFIG,
),
ProxmoxVMButtonEntityDescription(
key="shutdown",
translation_key="shutdown",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).qemu(vmid).status.shutdown.post()
),
entity_category=EntityCategory.CONFIG,
),
)
CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (

View File

@@ -7,9 +7,6 @@
"reset": {
"default": "mdi:restart"
},
"shutdown": {
"default": "mdi:power"
},
"start": {
"default": "mdi:play"
},

View File

@@ -20,18 +20,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import (
RoborockB01Q10UpdateCoordinator,
RoborockConfigEntry,
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
RoborockWashingMachineUpdateCoordinator,
)
from .entity import (
RoborockCoordinatedEntityA01,
RoborockCoordinatedEntityB01Q10,
RoborockEntity,
RoborockEntityV1,
)
from .entity import RoborockCoordinatedEntityA01, RoborockEntity, RoborockEntityV1
_LOGGER = logging.getLogger(__name__)
@@ -103,14 +97,6 @@ ZEO_BUTTON_DESCRIPTIONS = [
]
Q10_BUTTON_DESCRIPTIONS = [
ButtonEntityDescription(
key="empty_dustbin",
translation_key="empty_dustbin",
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: RoborockConfigEntry,
@@ -153,14 +139,6 @@ async def async_setup_entry(
if isinstance(coordinator, RoborockWashingMachineUpdateCoordinator)
for description in ZEO_BUTTON_DESCRIPTIONS
),
(
RoborockQ10EmptyDustbinButtonEntity(
coordinator,
description,
)
for coordinator in config_entry.runtime_data.b01_q10
for description in Q10_BUTTON_DESCRIPTIONS
),
)
)
@@ -255,37 +233,3 @@ class RoborockButtonEntityA01(RoborockCoordinatedEntityA01, ButtonEntity):
) from err
finally:
await self.coordinator.async_request_refresh()
class RoborockQ10EmptyDustbinButtonEntity(
RoborockCoordinatedEntityB01Q10, ButtonEntity
):
"""A class to define Q10 empty dustbin button entity."""
entity_description: ButtonEntityDescription
coordinator: RoborockB01Q10UpdateCoordinator
def __init__(
self,
coordinator: RoborockB01Q10UpdateCoordinator,
entity_description: ButtonEntityDescription,
) -> None:
"""Create a Q10 empty dustbin button entity."""
self.entity_description = entity_description
super().__init__(
f"{entity_description.key}_{coordinator.duid_slug}",
coordinator,
)
async def async_press(self, **kwargs: Any) -> None:
"""Press the button to empty dustbin."""
try:
await self.coordinator.api.vacuum.empty_dustbin()
except RoborockException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={
"command": "empty_dustbin",
},
) from err

View File

@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==5.0.0",
"python-roborock==4.25.0",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

@@ -84,9 +84,6 @@
}
},
"button": {
"empty_dustbin": {
"name": "Empty dustbin"
},
"pause": {
"name": "Pause"
},

View File

@@ -80,23 +80,23 @@ Q7_STATE_CODE_TO_STATE = {
}
Q10_STATE_CODE_TO_STATE = {
YXDeviceState.SLEEPING: VacuumActivity.IDLE,
YXDeviceState.IDLE: VacuumActivity.IDLE,
YXDeviceState.CLEANING: VacuumActivity.CLEANING,
YXDeviceState.RETURNING_HOME: VacuumActivity.RETURNING,
YXDeviceState.REMOTE_CONTROL_ACTIVE: VacuumActivity.CLEANING,
YXDeviceState.CHARGING: VacuumActivity.DOCKED,
YXDeviceState.PAUSED: VacuumActivity.PAUSED,
YXDeviceState.ERROR: VacuumActivity.ERROR,
YXDeviceState.UPDATING: VacuumActivity.DOCKED,
YXDeviceState.EMPTYING_THE_BIN: VacuumActivity.DOCKED,
YXDeviceState.MAPPING: VacuumActivity.CLEANING,
YXDeviceState.RELOCATING: VacuumActivity.CLEANING,
YXDeviceState.SWEEPING: VacuumActivity.CLEANING,
YXDeviceState.MOPPING: VacuumActivity.CLEANING,
YXDeviceState.SWEEP_AND_MOP: VacuumActivity.CLEANING,
YXDeviceState.TRANSITIONING: VacuumActivity.CLEANING,
YXDeviceState.WAITING_TO_CHARGE: VacuumActivity.DOCKED,
YXDeviceState.SLEEP_STATE: VacuumActivity.IDLE,
YXDeviceState.STANDBY_STATE: VacuumActivity.IDLE,
YXDeviceState.CLEANING_STATE: VacuumActivity.CLEANING,
YXDeviceState.TO_CHARGE_STATE: VacuumActivity.RETURNING,
YXDeviceState.REMOTEING_STATE: VacuumActivity.CLEANING,
YXDeviceState.CHARGING_STATE: VacuumActivity.DOCKED,
YXDeviceState.PAUSE_STATE: VacuumActivity.PAUSED,
YXDeviceState.FAULT_STATE: VacuumActivity.ERROR,
YXDeviceState.UPGRADE_STATE: VacuumActivity.DOCKED,
YXDeviceState.DUSTING: VacuumActivity.DOCKED,
YXDeviceState.CREATING_MAP_STATE: VacuumActivity.CLEANING,
YXDeviceState.RE_LOCATION_STATE: VacuumActivity.CLEANING,
YXDeviceState.ROBOT_SWEEPING: VacuumActivity.CLEANING,
YXDeviceState.ROBOT_MOPING: VacuumActivity.CLEANING,
YXDeviceState.ROBOT_SWEEP_AND_MOPING: VacuumActivity.CLEANING,
YXDeviceState.ROBOT_TRANSITIONING: VacuumActivity.CLEANING,
YXDeviceState.ROBOT_WAIT_CHARGE: VacuumActivity.DOCKED,
}
PARALLEL_UPDATES = 0

View File

@@ -208,16 +208,6 @@ CAPABILITY_TO_SENSORS: dict[
supported_states_attributes=Attribute.SUPPORTED_COOKTOP_OPERATING_STATE,
)
},
Capability.SAMSUNG_CE_CLEAN_STATION_STICK_STATUS: {
Attribute.STATUS: SmartThingsBinarySensorEntityDescription(
key=Attribute.STATUS,
component_translation_key={
"station": "stick_cleaner_status",
},
exists_fn=lambda component, _: component == "station",
is_on_key="attached",
)
},
Capability.SAMSUNG_CE_MICROFIBER_FILTER_STATUS: {
Attribute.STATUS: SmartThingsBinarySensorEntityDescription(
key=Attribute.STATUS,

View File

@@ -85,9 +85,6 @@
"robot_cleaner_dust_bag": {
"name": "Dust bag full"
},
"stick_cleaner_status": {
"name": "Stick cleaner in station"
},
"sub_remote_control": {
"name": "Upper washer remote control"
},

View File

@@ -105,7 +105,6 @@ SENSORS: list[SmSensorEntityDescription] = [
),
]
EXTRA_SENSOR = SmSensorEntityDescription(
key="zigbee_temperature_2",
translation_key="zigbee_temperature",
@@ -116,15 +115,6 @@ EXTRA_SENSOR = SmSensorEntityDescription(
value_fn=lambda x: x.zb_temp2,
)
PSRAM_SENSOR = SmSensorEntityDescription(
key="psram_usage",
translation_key="psram_usage",
device_class=SensorDeviceClass.DATA_SIZE,
native_unit_of_measurement=UnitOfInformation.KILOBYTES,
entity_registry_enabled_default=False,
value_fn=lambda x: x.psram_usage,
)
UPTIME: list[SmSensorEntityDescription] = [
SmSensorEntityDescription(
key="core_uptime",
@@ -166,9 +156,6 @@ async def async_setup_entry(
if coordinator.data.sensors.zb_temp2 is not None:
entities.append(SmSensorEntity(coordinator, EXTRA_SENSOR))
if coordinator.data.info.u_device:
entities.append(SmSensorEntity(coordinator, PSRAM_SENSOR))
async_add_entities(entities)

View File

@@ -104,9 +104,6 @@
"fs_usage": {
"name": "Filesystem usage"
},
"psram_usage": {
"name": "PSRAM usage"
},
"ram_usage": {
"name": "RAM usage"
},

View File

@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["aiotedee"],
"quality_scale": "platinum",
"requirements": ["aiotedee==0.2.27"]
"requirements": ["aiotedee==0.2.25"]
}

View File

@@ -1,17 +0,0 @@
"""Integration for temperature triggers."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
DOMAIN = "temperature"
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
__all__ = []
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
return True

View File

@@ -1,10 +0,0 @@
{
"triggers": {
"changed": {
"trigger": "mdi:thermometer"
},
"crossed_threshold": {
"trigger": "mdi:thermometer"
}
}
}

View File

@@ -1,8 +0,0 @@
{
"domain": "temperature",
"name": "Temperature",
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/temperature",
"integration_type": "system",
"quality_scale": "internal"
}

View File

@@ -1,76 +0,0 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
"trigger_behavior_name": "Behavior"
},
"selector": {
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
},
"trigger_threshold_type": {
"options": {
"above": "Above",
"below": "Below",
"between": "Between",
"outside": "Outside"
}
}
},
"title": "Temperature",
"triggers": {
"changed": {
"description": "Triggers when the temperature changes.",
"fields": {
"above": {
"description": "Only trigger when temperature is above this value.",
"name": "Above"
},
"below": {
"description": "Only trigger when temperature is below this value.",
"name": "Below"
},
"unit": {
"description": "All values will be converted to this unit when evaluating the trigger.",
"name": "Unit of measurement"
}
},
"name": "Temperature changed"
},
"crossed_threshold": {
"description": "Triggers when the temperature crosses a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::temperature::common::trigger_behavior_description%]",
"name": "[%key:component::temperature::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "The lower limit of the threshold.",
"name": "Lower limit"
},
"threshold_type": {
"description": "The type of threshold to use.",
"name": "Threshold type"
},
"unit": {
"description": "[%key:component::temperature::triggers::changed::fields::unit::description%]",
"name": "[%key:component::temperature::triggers::changed::fields::unit::name%]"
},
"upper_limit": {
"description": "The upper limit of the threshold.",
"name": "Upper limit"
}
},
"name": "Temperature crossed threshold"
}
}
}

View File

@@ -1,83 +0,0 @@
"""Provides triggers for temperature."""
from __future__ import annotations
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE as CLIMATE_ATTR_CURRENT_TEMPERATURE,
DOMAIN as CLIMATE_DOMAIN,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.components.water_heater import (
ATTR_CURRENT_TEMPERATURE as WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
DOMAIN as WATER_HEATER_DOMAIN,
)
from homeassistant.components.weather import (
ATTR_WEATHER_TEMPERATURE,
ATTR_WEATHER_TEMPERATURE_UNIT,
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import NumericalDomainSpec
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerWithUnitBase,
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
EntityNumericalStateTriggerWithUnitBase,
Trigger,
)
from homeassistant.util.unit_conversion import TemperatureConverter
TEMPERATURE_DOMAIN_SPECS = {
CLIMATE_DOMAIN: NumericalDomainSpec(
value_source=CLIMATE_ATTR_CURRENT_TEMPERATURE,
),
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.TEMPERATURE,
),
WATER_HEATER_DOMAIN: NumericalDomainSpec(
value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE
),
WEATHER_DOMAIN: NumericalDomainSpec(
value_source=ATTR_WEATHER_TEMPERATURE,
),
}
class _TemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitBase):
"""Mixin for temperature triggers providing entity filtering, value extraction, and unit conversion."""
_base_unit = UnitOfTemperature.CELSIUS
_domain_specs = TEMPERATURE_DOMAIN_SPECS
_unit_converter = TemperatureConverter
def _get_entity_unit(self, state: State) -> str | None:
"""Get the temperature unit of an entity from its state."""
if state.domain == SENSOR_DOMAIN:
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if state.domain == WEATHER_DOMAIN:
return state.attributes.get(ATTR_WEATHER_TEMPERATURE_UNIT)
# Climate and water_heater: show_temp converts to system unit
return self._hass.config.units.temperature_unit
class TemperatureChangedTrigger(
_TemperatureTriggerMixin, EntityNumericalStateChangedTriggerWithUnitBase
):
"""Trigger for temperature value changes across multiple domains."""
class TemperatureCrossedThresholdTrigger(
_TemperatureTriggerMixin, EntityNumericalStateCrossedThresholdTriggerWithUnitBase
):
"""Trigger for temperature value crossing a threshold across multiple domains."""
TRIGGERS: dict[str, type[Trigger]] = {
"changed": TemperatureChangedTrigger,
"crossed_threshold": TemperatureCrossedThresholdTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for temperature."""
return TRIGGERS

View File

@@ -1,77 +0,0 @@
.trigger_common_fields:
behavior: &trigger_behavior
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
.number_or_entity: &number_or_entity
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "°C"
- "°F"
- domain: sensor
device_class: temperature
- domain: number
device_class: temperature
translation_key: number_or_entity
.trigger_threshold_type: &trigger_threshold_type
required: true
default: above
selector:
select:
options:
- above
- below
- between
- outside
translation_key: trigger_threshold_type
.trigger_unit: &trigger_unit
required: false
selector:
select:
options:
- "°C"
- "°F"
.trigger_target: &trigger_target
entity:
- domain: sensor
device_class: temperature
- domain: climate
- domain: water_heater
- domain: weather
changed:
target: *trigger_target
fields:
above: *number_or_entity
below: *number_or_entity
unit: *trigger_unit
crossed_threshold:
target: *trigger_target
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity
upper_limit: *number_or_entity
unit: *trigger_unit

View File

@@ -298,14 +298,19 @@ class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
tibber_connection.fetch_production_data_active_homes(),
)
now = dt_util.now()
today_start = dt_util.start_of_local_day()
today_end = today_start + timedelta(days=1)
def _has_prices_today(home: tibber.TibberHome) -> bool:
"""Return True if the home has any prices today."""
for start in home.price_total:
start_dt = dt_util.as_local(datetime.fromisoformat(str(start)))
if today_start <= start_dt < today_end:
return True
return False
homes_to_update = [
home
for home in active_homes
if (
(last_data_timestamp := home.last_data_timestamp) is None
or (last_data_timestamp - now).total_seconds() < 11 * 3600
)
home for home in active_homes if not _has_prices_today(home)
]
if homes_to_update:

View File

@@ -6,6 +6,7 @@ import datetime as dt
from datetime import datetime
from typing import TYPE_CHECKING, Any, Final
import tibber
import voluptuous as vol
from homeassistant.core import (
@@ -52,7 +53,26 @@ async def __get_prices(call: ServiceCall) -> ServiceResponse:
tibber_prices: dict[str, Any] = {}
now = dt_util.now()
today_start = dt_util.start_of_local_day()
today_end = today_start + dt.timedelta(days=1)
tomorrow_end = today_start + dt.timedelta(days=2)
def _has_valid_prices(home: tibber.TibberHome) -> bool:
"""Return True if the home has valid prices."""
for start in home.price_total:
start_dt = dt_util.as_local(datetime.fromisoformat(str(start)))
if now.hour >= 13:
if today_end <= start_dt < tomorrow_end:
return True
elif today_start <= start_dt < today_end:
return True
return False
for tibber_home in tibber_connection.get_homes(only_active=True):
if not _has_valid_prices(tibber_home):
await tibber_home.update_info_and_price_info()
home_nickname = tibber_home.name
price_data = [

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["tplink-omada-client==1.5.6"]
"requirements": ["tplink-omada-client==1.5.3"]
}

View File

@@ -6,7 +6,7 @@ from unifi_access_api import ApiAuthError, ApiConnectionError, UnifiAccessApiCli
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator
@@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UnifiAccessConfigEntry)
try:
await client.authenticate()
except ApiAuthError as err:
raise ConfigEntryAuthFailed(
raise ConfigEntryNotReady(
f"Authentication failed for UniFi Access at {entry.data[CONF_HOST]}"
) from err
except ApiConnectionError as err:

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
@@ -67,48 +66,3 @@ class UnifiAccessConfigFlow(ConfigFlow, domain=DOMAIN):
),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth flow."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth confirm."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
session = async_get_clientsession(
self.hass, verify_ssl=reauth_entry.data[CONF_VERIFY_SSL]
)
client = UnifiAccessApiClient(
host=reauth_entry.data[CONF_HOST],
api_token=user_input[CONF_API_TOKEN],
session=session,
verify_ssl=reauth_entry.data[CONF_VERIFY_SSL],
)
try:
await client.authenticate()
except ApiAuthError:
errors["base"] = "invalid_auth"
except ApiConnectionError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
description_placeholders={CONF_HOST: reauth_entry.data[CONF_HOST]},
errors=errors,
)

View File

@@ -30,7 +30,6 @@ from unifi_access_api.models.websocket import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -117,7 +116,7 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
self.client.get_emergency_status(),
)
except ApiAuthError as err:
raise ConfigEntryAuthFailed(f"Authentication failed: {err}") from err
raise UpdateFailed(f"Authentication failed: {err}") from err
except ApiConnectionError as err:
raise UpdateFailed(f"Error connecting to API: {err}") from err
except ApiError as err:

View File

@@ -34,7 +34,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
reauthentication-flow: todo
test-coverage: done
# Gold

View File

@@ -1,8 +1,7 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -10,15 +9,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]"
},
"data_description": {
"api_token": "[%key:component::unifi_access::config::step::user::data_description::api_token%]"
},
"description": "The API token for UniFi Access at {host} is invalid. Please provide a new token."
},
"user": {
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]",

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["wolf_comm"],
"requirements": ["wolf-comm==0.0.48"]
"requirements": ["wolf-comm==0.0.23"]
}

View File

@@ -25,5 +25,5 @@
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["xiaomi-ble==1.10.0"]
"requirements": ["xiaomi-ble==1.6.0"]
}

View File

@@ -177,24 +177,6 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=UnitOfTime.MINUTES,
state_class=SensorStateClass.MEASUREMENT,
),
# Pressure present duration (in seconds) for Pressure Sensor
(
ExtendedSensorDeviceClass.PRESSURE_PRESENT_DURATION,
Units.TIME_SECONDS,
): SensorEntityDescription(
key=str(ExtendedSensorDeviceClass.PRESSURE_PRESENT_DURATION),
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.MEASUREMENT,
),
# Pressure not present duration (in seconds) for Pressure Sensor
(
ExtendedSensorDeviceClass.PRESSURE_NOT_PRESENT_DURATION,
Units.TIME_SECONDS,
): SensorEntityDescription(
key=str(ExtendedSensorDeviceClass.PRESSURE_NOT_PRESENT_DURATION),
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.MEASUREMENT,
),
# Low frequency impedance sensor (ohm)
(ExtendedSensorDeviceClass.IMPEDANCE_LOW, Units.OHM): SensorEntityDescription(
key=str(ExtendedSensorDeviceClass.IMPEDANCE_LOW),

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/yolink",
"integration_type": "hub",
"iot_class": "cloud_push",
"requirements": ["yolink-api==0.6.3"]
"requirements": ["yolink-api==0.6.1"]
}

View File

@@ -39,7 +39,7 @@ class DomainSpec:
class NumericalDomainSpec(DomainSpec):
"""DomainSpec with an optional value converter for numerical triggers."""
value_converter: Callable[[float], float] | None = None
value_converter: Callable[[Any], float] | None = None
"""Optional converter for numerical values (e.g. uint8 → percentage)."""

View File

@@ -26,7 +26,6 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ABOVE,
CONF_ALIAS,
CONF_BELOW,
@@ -65,7 +64,6 @@ from homeassistant.loader import (
)
from homeassistant.util.async_ import create_eager_task
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.unit_conversion import BaseUnitConverter
from homeassistant.util.yaml import load_yaml_dict
from . import config_validation as cv, selector
@@ -84,7 +82,7 @@ from .target import (
async_track_target_selector_state_change_event,
)
from .template import Template
from .typing import UNDEFINED, ConfigType, TemplateVarsType, UndefinedType
from .typing import ConfigType, TemplateVarsType
_LOGGER = logging.getLogger(__name__)
@@ -521,7 +519,7 @@ def _validate_range[_T: dict[str, Any]](
) -> Callable[[_T], _T]:
"""Generate range validator."""
def _validate_range_impl(value: _T) -> _T:
def _validate_range(value: _T) -> _T:
above = value.get(lower_limit)
below = value.get(upper_limit)
@@ -541,28 +539,7 @@ def _validate_range[_T: dict[str, Any]](
return value
return _validate_range_impl
CONF_UNIT: Final = "unit"
def _validate_unit_set_if_range_numerical[_T: dict[str, Any]](
lower_limit: str, upper_limit: str
) -> Callable[[_T], _T]:
"""Validate that unit is set if upper or lower limit is numerical."""
def _validate_unit_set_if_range_numerical_impl(options: _T) -> _T:
if (
any(
opt in options and not isinstance(options[opt], str)
for opt in (lower_limit, upper_limit)
)
) and options.get(CONF_UNIT) is None:
raise vol.Invalid("Unit must be specified when using numerical thresholds.")
return options
return _validate_unit_set_if_range_numerical_impl
return _validate_range
_NUMBER_OR_ENTITY_CHOOSE_SCHEMA = vol.Schema(
@@ -599,120 +576,38 @@ NUMERICAL_ATTRIBUTE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
)
class EntityNumericalStateTriggerBase(EntityTriggerBase[NumericalDomainSpec]):
"""Base class for numerical state and state attribute triggers."""
_valid_unit: str | None | UndefinedType = UNDEFINED
def _is_valid_unit(self, unit: str | None) -> bool:
"""Check if the given unit is valid for this trigger."""
if isinstance(self._valid_unit, UndefinedType):
return True
return unit == self._valid_unit
def _get_numerical_value(self, entity_or_float: float | str) -> float | None:
"""Get numerical value from float or entity state."""
if isinstance(entity_or_float, str):
if not (state := self._hass.states.get(entity_or_float)):
# Entity not found
return None
if not self._is_valid_unit(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
# Entity unit does not match the expected unit
return None
try:
return float(state.state)
except TypeError, ValueError:
# Entity state is not a valid number
return None
return entity_or_float
def _get_tracked_value(self, state: State) -> float | None:
"""Get the tracked numerical value from a state."""
domain_spec = self._domain_specs[state.domain]
raw_value: Any
if domain_spec.value_source is None:
if not self._is_valid_unit(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
return None
raw_value = state.state
else:
raw_value = state.attributes.get(domain_spec.value_source)
def _get_numerical_value(
hass: HomeAssistant, entity_or_float: float | str
) -> float | None:
"""Get numerical value from float or entity state."""
if isinstance(entity_or_float, str):
if not (state := hass.states.get(entity_or_float)):
# Entity not found
return None
try:
return float(raw_value)
return float(state.state)
except TypeError, ValueError:
# Entity state is not a valid number
return None
return entity_or_float
def _get_converter(self, state: State) -> Callable[[float], float]:
class EntityNumericalStateTriggerBase(EntityTriggerBase[NumericalDomainSpec]):
"""Base class for numerical state and state attribute triggers."""
def _get_tracked_value(self, state: State) -> Any:
"""Get the tracked numerical value from a state."""
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_source is None:
return state.state
return state.attributes.get(domain_spec.value_source)
def _get_converter(self, state: State) -> Callable[[Any], float]:
"""Get the value converter for an entity."""
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_converter is not None:
return domain_spec.value_converter
return lambda x: x
class EntityNumericalStateTriggerWithUnitBase(EntityNumericalStateTriggerBase):
"""Base class for numerical state and state attribute triggers."""
_base_unit: str # Base unit for the tracked value
_manual_limit_unit: str | None # Unit of above/below limits when numbers
_unit_converter: type[BaseUnitConverter]
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the trigger."""
super().__init__(hass, config)
self._manual_limit_unit = self._options.get(CONF_UNIT)
def _get_entity_unit(self, state: State) -> str | None:
"""Get the unit of an entity from its state."""
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
def _get_numerical_value(self, entity_or_float: float | str) -> float | None:
"""Get numerical value from float or entity state."""
if isinstance(entity_or_float, (int, float)):
return self._unit_converter.convert(
entity_or_float, self._manual_limit_unit, self._base_unit
)
if not (state := self._hass.states.get(entity_or_float)):
# Entity not found
return None
try:
value = float(state.state)
except TypeError, ValueError:
# Entity state is not a valid number
return None
try:
return self._unit_converter.convert(
value, state.attributes.get(ATTR_UNIT_OF_MEASUREMENT), self._base_unit
)
except HomeAssistantError:
# Unit conversion failed (i.e. incompatible units), treat as invalid number
return None
def _get_tracked_value(self, state: State) -> float | None:
"""Get the tracked numerical value from a state."""
domain_spec = self._domain_specs[state.domain]
raw_value: Any
if domain_spec.value_source is None:
raw_value = state.state
else:
raw_value = state.attributes.get(domain_spec.value_source)
try:
value = float(raw_value)
except TypeError, ValueError:
# Entity state is not a valid number
return None
try:
return self._unit_converter.convert(
value, self._get_entity_unit(state), self._base_unit
)
except HomeAssistantError:
# Unit conversion failed (i.e. incompatible units), treat as invalid number
return None
return float
class EntityNumericalStateChangedTriggerBase(EntityNumericalStateTriggerBase):
@@ -734,7 +629,7 @@ class EntityNumericalStateChangedTriggerBase(EntityNumericalStateTriggerBase):
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
return self._get_tracked_value(from_state) != self._get_tracked_value(to_state)
return self._get_tracked_value(from_state) != self._get_tracked_value(to_state) # type: ignore[no-any-return]
def is_valid_state(self, state: State) -> bool:
"""Check if the new state or state attribute matches the expected one."""
@@ -742,10 +637,14 @@ class EntityNumericalStateChangedTriggerBase(EntityNumericalStateTriggerBase):
if (_attribute_value := self._get_tracked_value(state)) is None:
return False
current_value = self._get_converter(state)(_attribute_value)
try:
current_value = self._get_converter(state)(_attribute_value)
except TypeError, ValueError:
# Value is not a valid number, don't trigger
return False
if self._above is not None:
if (above := self._get_numerical_value(self._above)) is None:
if (above := _get_numerical_value(self._hass, self._above)) is None:
# Entity not found or invalid number, don't trigger
return False
if current_value <= above:
@@ -753,7 +652,7 @@ class EntityNumericalStateChangedTriggerBase(EntityNumericalStateTriggerBase):
return False
if self._below is not None:
if (below := self._get_numerical_value(self._below)) is None:
if (below := _get_numerical_value(self._hass, self._below)) is None:
# Entity not found or invalid number, don't trigger
return False
if current_value >= below:
@@ -763,37 +662,6 @@ class EntityNumericalStateChangedTriggerBase(EntityNumericalStateTriggerBase):
return True
def make_numerical_state_changed_with_unit_schema(
unit_converter: type[BaseUnitConverter],
) -> vol.Schema:
"""Factory for numerical state trigger schema with unit option."""
return ENTITY_STATE_TRIGGER_SCHEMA.extend(
{
vol.Required(CONF_OPTIONS, default={}): vol.All(
{
vol.Optional(CONF_ABOVE): _number_or_entity,
vol.Optional(CONF_BELOW): _number_or_entity,
vol.Optional(CONF_UNIT): vol.In(unit_converter.VALID_UNITS),
},
_validate_range(CONF_ABOVE, CONF_BELOW),
_validate_unit_set_if_range_numerical(CONF_ABOVE, CONF_BELOW),
)
}
)
class EntityNumericalStateChangedTriggerWithUnitBase(
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateTriggerWithUnitBase,
):
"""Trigger for numerical state and state attribute changes."""
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Create a schema."""
super().__init_subclass__(**kwargs)
cls._schema = make_numerical_state_changed_with_unit_schema(cls._unit_converter)
CONF_LOWER_LIMIT = "lower_limit"
CONF_UPPER_LIMIT = "upper_limit"
CONF_THRESHOLD_TYPE = "threshold_type"
@@ -876,12 +744,16 @@ class EntityNumericalStateCrossedThresholdTriggerBase(EntityNumericalStateTrigge
def is_valid_state(self, state: State) -> bool:
"""Check if the new state attribute matches the expected one."""
if self._lower_limit is not None:
if (lower_limit := self._get_numerical_value(self._lower_limit)) is None:
if (
lower_limit := _get_numerical_value(self._hass, self._lower_limit)
) is None:
# Entity not found or invalid number, don't trigger
return False
if self._upper_limit is not None:
if (upper_limit := self._get_numerical_value(self._upper_limit)) is None:
if (
upper_limit := _get_numerical_value(self._hass, self._upper_limit)
) is None:
# Entity not found or invalid number, don't trigger
return False
@@ -889,7 +761,11 @@ class EntityNumericalStateCrossedThresholdTriggerBase(EntityNumericalStateTrigge
if (_attribute_value := self._get_tracked_value(state)) is None:
return False
current_value = self._get_converter(state)(_attribute_value)
try:
current_value = self._get_converter(state)(_attribute_value)
except TypeError, ValueError:
# Value is not a valid number, don't trigger
return False
# Note: We do not need to check for lower_limit/upper_limit being None here
# because of the validation done in the schema.
@@ -905,50 +781,6 @@ class EntityNumericalStateCrossedThresholdTriggerBase(EntityNumericalStateTrigge
return not between
def make_numerical_state_crossed_threshold_with_unit_schema(
unit_converter: type[BaseUnitConverter],
) -> vol.Schema:
"""Trigger for numerical state and state attribute changes.
This trigger only fires when the observed attribute changes from not within to within
the defined threshold.
"""
return ENTITY_STATE_TRIGGER_SCHEMA.extend(
{
vol.Required(CONF_OPTIONS, default={}): vol.All(
{
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
),
vol.Optional(CONF_LOWER_LIMIT): _number_or_entity,
vol.Optional(CONF_UPPER_LIMIT): _number_or_entity,
vol.Required(CONF_THRESHOLD_TYPE): vol.Coerce(ThresholdType),
vol.Optional(CONF_UNIT): vol.In(unit_converter.VALID_UNITS),
},
_validate_range(CONF_LOWER_LIMIT, CONF_UPPER_LIMIT),
_validate_limits_for_threshold_type,
_validate_unit_set_if_range_numerical(
CONF_LOWER_LIMIT, CONF_UPPER_LIMIT
),
)
}
)
class EntityNumericalStateCrossedThresholdTriggerWithUnitBase(
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerWithUnitBase,
):
"""Trigger for numerical state and state attribute changes."""
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Create a schema."""
super().__init_subclass__(**kwargs)
cls._schema = make_numerical_state_crossed_threshold_with_unit_schema(
cls._unit_converter
)
def _normalize_domain_specs(
domain_specs: Mapping[str, DomainSpec] | str,
) -> Mapping[str, DomainSpec]:
@@ -1029,7 +861,6 @@ def make_entity_origin_state_trigger(
def make_entity_numerical_state_changed_trigger(
domain_specs: Mapping[str, NumericalDomainSpec],
valid_unit: str | None | UndefinedType = UNDEFINED,
) -> type[EntityNumericalStateChangedTriggerBase]:
"""Create a trigger for numerical state value change."""
@@ -1037,14 +868,12 @@ def make_entity_numerical_state_changed_trigger(
"""Trigger for numerical state value changes."""
_domain_specs = domain_specs
_valid_unit = valid_unit
return CustomTrigger
def make_entity_numerical_state_crossed_threshold_trigger(
domain_specs: Mapping[str, NumericalDomainSpec],
valid_unit: str | None | UndefinedType = UNDEFINED,
) -> type[EntityNumericalStateCrossedThresholdTriggerBase]:
"""Create a trigger for numerical state value crossing a threshold."""
@@ -1052,7 +881,6 @@ def make_entity_numerical_state_crossed_threshold_trigger(
"""Trigger for numerical state value crossing a threshold."""
_domain_specs = domain_specs
_valid_unit = valid_unit
return CustomTrigger

View File

@@ -35,7 +35,7 @@ file-read-backwards==2.0.0
fnv-hash-fast==2.0.0
go2rtc-client==0.4.0
ha-ffmpeg==3.2.2
habluetooth==5.11.1
habluetooth==5.10.2
hass-nabucasa==2.0.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1

22
requirements_all.txt generated
View File

@@ -422,7 +422,7 @@ aiosyncthing==0.7.1
aiotankerkoenig==0.5.1
# homeassistant.components.tedee
aiotedee==0.2.27
aiotedee==0.2.25
# homeassistant.components.tractive
aiotractive==1.0.0
@@ -945,7 +945,7 @@ eufylife-ble-client==0.1.8
# evdev==1.9.3
# homeassistant.components.evohome
evohome-async==1.2.0
evohome-async==1.1.3
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -1140,7 +1140,7 @@ greeclimate==2.1.1
greeneye_monitor==3.0.3
# homeassistant.components.green_planet_energy
greenplanet-energy-api==0.1.10
greenplanet-energy-api==0.1.4
# homeassistant.components.greenwave
greenwavereality==0.5.1
@@ -1176,7 +1176,7 @@ ha-silabs-firmware-client==0.3.0
habiticalib==0.4.6
# homeassistant.components.bluetooth
habluetooth==5.11.1
habluetooth==5.10.2
# homeassistant.components.hanna
hanna-cloud==0.0.7
@@ -1732,7 +1732,7 @@ openwrt-ubus-rpc==0.0.2
opower==0.17.1
# homeassistant.components.oralb
oralb-ble==1.1.0
oralb-ble==1.0.2
# homeassistant.components.oru
oru==0.1.11
@@ -2427,7 +2427,7 @@ pyrail==0.4.1
pyrainbird==6.1.1
# homeassistant.components.playstation_network
pyrate-limiter==4.1.0
pyrate-limiter==4.0.2
# homeassistant.components.recswitch
pyrecswitch==1.0.2
@@ -2660,7 +2660,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==5.0.0
python-roborock==4.25.0
# homeassistant.components.smarttub
python-smarttub==0.0.47
@@ -3136,7 +3136,7 @@ total-connect-client==2025.12.2
tp-connected==0.0.4
# homeassistant.components.tplink_omada
tplink-omada-client==1.5.6
tplink-omada-client==1.5.3
# homeassistant.components.transmission
transmission-rpc==7.0.3
@@ -3310,7 +3310,7 @@ wirelesstagpy==0.8.1
wled==0.21.0
# homeassistant.components.wolflink
wolf-comm==0.0.48
wolf-comm==0.0.23
# homeassistant.components.wsdot
wsdot==0.0.1
@@ -3319,7 +3319,7 @@ wsdot==0.0.1
wyoming==1.7.2
# homeassistant.components.xiaomi_ble
xiaomi-ble==1.10.0
xiaomi-ble==1.6.0
# homeassistant.components.knx
xknx==3.15.0
@@ -3356,7 +3356,7 @@ yeelight==0.7.16
yeelightsunflower==0.0.10
# homeassistant.components.yolink
yolink-api==0.6.3
yolink-api==0.6.1
# homeassistant.components.youless
youless-api==2.2.0

View File

@@ -407,7 +407,7 @@ aiosyncthing==0.7.1
aiotankerkoenig==0.5.1
# homeassistant.components.tedee
aiotedee==0.2.27
aiotedee==0.2.25
# homeassistant.components.tractive
aiotractive==1.0.0
@@ -833,7 +833,7 @@ eternalegypt==0.0.18
eufylife-ble-client==0.1.8
# homeassistant.components.evohome
evohome-async==1.2.0
evohome-async==1.1.3
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -1013,7 +1013,7 @@ greeclimate==2.1.1
greeneye_monitor==3.0.3
# homeassistant.components.green_planet_energy
greenplanet-energy-api==0.1.10
greenplanet-energy-api==0.1.4
# homeassistant.components.pure_energie
gridnet==5.0.1
@@ -1046,7 +1046,7 @@ ha-silabs-firmware-client==0.3.0
habiticalib==0.4.6
# homeassistant.components.bluetooth
habluetooth==5.11.1
habluetooth==5.10.2
# homeassistant.components.hanna
hanna-cloud==0.0.7
@@ -1509,7 +1509,7 @@ openwebifpy==4.3.1
opower==0.17.1
# homeassistant.components.oralb
oralb-ble==1.1.0
oralb-ble==1.0.2
# homeassistant.components.orvibo
orvibo==1.1.2
@@ -2077,7 +2077,7 @@ pyrail==0.4.1
pyrainbird==6.1.1
# homeassistant.components.playstation_network
pyrate-limiter==4.1.0
pyrate-limiter==4.0.2
# homeassistant.components.risco
pyrisco==0.6.7
@@ -2256,7 +2256,7 @@ python-pooldose==0.8.6
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==5.0.0
python-roborock==4.25.0
# homeassistant.components.smarttub
python-smarttub==0.0.47
@@ -2639,7 +2639,7 @@ toonapi==0.3.0
total-connect-client==2025.12.2
# homeassistant.components.tplink_omada
tplink-omada-client==1.5.6
tplink-omada-client==1.5.3
# homeassistant.components.transmission
transmission-rpc==7.0.3
@@ -2792,7 +2792,7 @@ wiim==0.1.0
wled==0.21.0
# homeassistant.components.wolflink
wolf-comm==0.0.48
wolf-comm==0.0.23
# homeassistant.components.wsdot
wsdot==0.0.1
@@ -2801,7 +2801,7 @@ wsdot==0.0.1
wyoming==1.7.2
# homeassistant.components.xiaomi_ble
xiaomi-ble==1.10.0
xiaomi-ble==1.6.0
# homeassistant.components.knx
xknx==3.15.0
@@ -2832,7 +2832,7 @@ yalexs==9.2.0
yeelight==0.7.16
# homeassistant.components.yolink
yolink-api==0.6.3
yolink-api==0.6.1
# homeassistant.components.youless
youless-api==2.2.0

View File

@@ -89,7 +89,6 @@ NO_IOT_CLASS = [
"homeassistant_sky_connect",
"homeassistant_yellow",
"humidity",
"illuminance",
"image_upload",
"input_boolean",
"input_button",
@@ -121,7 +120,6 @@ NO_IOT_CLASS = [
"system_health",
"system_log",
"tag",
"temperature",
"timer",
"trace",
"web_rtc",

View File

@@ -2119,7 +2119,6 @@ NO_QUALITY_SCALE = [
"homeassistant_sky_connect",
"homeassistant_yellow",
"humidity",
"illuminance",
"image_upload",
"input_boolean",
"input_button",
@@ -2150,7 +2149,6 @@ NO_QUALITY_SCALE = [
"system_health",
"system_log",
"tag",
"temperature",
"timer",
"trace",
"usage_prediction",

View File

@@ -18,7 +18,6 @@ from homeassistant.const import (
CONF_ENTITY_ID,
CONF_OPTIONS,
CONF_TARGET,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.trigger import async_validate_trigger_config
@@ -37,8 +36,6 @@ from tests.components.common import (
target_entities,
)
_TEMPERATURE_TRIGGER_OPTIONS = {"unit": UnitOfTemperature.CELSIUS}
@pytest.fixture
async def target_climates(hass: HomeAssistant) -> dict[str, list[str]]:
@@ -194,10 +191,7 @@ async def test_climate_state_trigger_behavior_any(
"climate.target_humidity_changed", HVACMode.AUTO, ATTR_HUMIDITY
),
*parametrize_numerical_attribute_changed_trigger_states(
"climate.target_temperature_changed",
HVACMode.AUTO,
ATTR_TEMPERATURE,
trigger_options=_TEMPERATURE_TRIGGER_OPTIONS,
"climate.target_temperature_changed", HVACMode.AUTO, ATTR_TEMPERATURE
),
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
"climate.target_humidity_crossed_threshold", HVACMode.AUTO, ATTR_HUMIDITY
@@ -206,7 +200,6 @@ async def test_climate_state_trigger_behavior_any(
"climate.target_temperature_crossed_threshold",
HVACMode.AUTO,
ATTR_TEMPERATURE,
trigger_options=_TEMPERATURE_TRIGGER_OPTIONS,
),
*parametrize_trigger_states(
trigger="climate.started_cooling",
@@ -325,7 +318,6 @@ async def test_climate_state_trigger_behavior_first(
"climate.target_temperature_crossed_threshold",
HVACMode.AUTO,
ATTR_TEMPERATURE,
trigger_options=_TEMPERATURE_TRIGGER_OPTIONS,
),
*parametrize_trigger_states(
trigger="climate.started_cooling",
@@ -444,7 +436,6 @@ async def test_climate_state_trigger_behavior_last(
"climate.target_temperature_crossed_threshold",
HVACMode.AUTO,
ATTR_TEMPERATURE,
trigger_options=_TEMPERATURE_TRIGGER_OPTIONS,
),
*parametrize_trigger_states(
trigger="climate.started_cooling",

View File

@@ -518,75 +518,54 @@ def parametrize_trigger_states(
def parametrize_numerical_attribute_changed_trigger_states(
trigger: str,
state: str,
attribute: str,
*,
trigger_options: dict[str, Any] | None = None,
required_filter_attributes: dict | None = None,
unit_attributes: dict | None = None,
trigger: str, state: str, attribute: str
) -> list[tuple[str, dict[str, Any], list[TriggerStateDescription]]]:
"""Parametrize states and expected service call counts for numerical changed triggers."""
trigger_options = trigger_options or {}
unit_attributes = unit_attributes or {}
return [
*parametrize_trigger_states(
trigger=trigger,
trigger_options={**trigger_options},
trigger_options={},
target_states=[
(state, {attribute: 0} | unit_attributes),
(state, {attribute: 50} | unit_attributes),
(state, {attribute: 100} | unit_attributes),
(state, {attribute: 0}),
(state, {attribute: 50}),
(state, {attribute: 100}),
],
other_states=[(state, {attribute: None} | unit_attributes)],
required_filter_attributes=required_filter_attributes,
other_states=[(state, {attribute: None})],
retrigger_on_target_state=True,
),
*parametrize_trigger_states(
trigger=trigger,
trigger_options={CONF_ABOVE: 10, **trigger_options},
trigger_options={CONF_ABOVE: 10},
target_states=[
(state, {attribute: 50} | unit_attributes),
(state, {attribute: 100} | unit_attributes),
(state, {attribute: 50}),
(state, {attribute: 100}),
],
other_states=[
(state, {attribute: None} | unit_attributes),
(state, {attribute: 0} | unit_attributes),
(state, {attribute: None}),
(state, {attribute: 0}),
],
required_filter_attributes=required_filter_attributes,
retrigger_on_target_state=True,
),
*parametrize_trigger_states(
trigger=trigger,
trigger_options={CONF_BELOW: 90, **trigger_options},
trigger_options={CONF_BELOW: 90},
target_states=[
(state, {attribute: 0} | unit_attributes),
(state, {attribute: 50} | unit_attributes),
(state, {attribute: 0}),
(state, {attribute: 50}),
],
other_states=[
(state, {attribute: None} | unit_attributes),
(state, {attribute: 100} | unit_attributes),
(state, {attribute: None}),
(state, {attribute: 100}),
],
required_filter_attributes=required_filter_attributes,
retrigger_on_target_state=True,
),
]
def parametrize_numerical_attribute_crossed_threshold_trigger_states(
trigger: str,
state: str,
attribute: str,
*,
trigger_options: dict[str, Any] | None = None,
required_filter_attributes: dict | None = None,
unit_attributes: dict | None = None,
trigger: str, state: str, attribute: str
) -> list[tuple[str, dict[str, Any], list[TriggerStateDescription]]]:
"""Parametrize states and expected service call counts for numerical crossed threshold triggers."""
trigger_options = trigger_options or {}
unit_attributes = unit_attributes or {}
return [
*parametrize_trigger_states(
trigger=trigger,
@@ -594,18 +573,16 @@ def parametrize_numerical_attribute_crossed_threshold_trigger_states(
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
CONF_LOWER_LIMIT: 10,
CONF_UPPER_LIMIT: 90,
**trigger_options,
},
target_states=[
(state, {attribute: 50} | unit_attributes),
(state, {attribute: 60} | unit_attributes),
(state, {attribute: 50}),
(state, {attribute: 60}),
],
other_states=[
(state, {attribute: None} | unit_attributes),
(state, {attribute: 0} | unit_attributes),
(state, {attribute: 100} | unit_attributes),
(state, {attribute: None}),
(state, {attribute: 0}),
(state, {attribute: 100}),
],
required_filter_attributes=required_filter_attributes,
),
*parametrize_trigger_states(
trigger=trigger,
@@ -613,62 +590,52 @@ def parametrize_numerical_attribute_crossed_threshold_trigger_states(
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
CONF_LOWER_LIMIT: 10,
CONF_UPPER_LIMIT: 90,
**trigger_options,
},
target_states=[
(state, {attribute: 0} | unit_attributes),
(state, {attribute: 100} | unit_attributes),
(state, {attribute: 0}),
(state, {attribute: 100}),
],
other_states=[
(state, {attribute: None} | unit_attributes),
(state, {attribute: 50} | unit_attributes),
(state, {attribute: 60} | unit_attributes),
(state, {attribute: None}),
(state, {attribute: 50}),
(state, {attribute: 60}),
],
required_filter_attributes=required_filter_attributes,
),
*parametrize_trigger_states(
trigger=trigger,
trigger_options={
CONF_THRESHOLD_TYPE: ThresholdType.ABOVE,
CONF_LOWER_LIMIT: 10,
**trigger_options,
},
target_states=[
(state, {attribute: 50} | unit_attributes),
(state, {attribute: 100} | unit_attributes),
(state, {attribute: 50}),
(state, {attribute: 100}),
],
other_states=[
(state, {attribute: None} | unit_attributes),
(state, {attribute: 0} | unit_attributes),
(state, {attribute: None}),
(state, {attribute: 0}),
],
required_filter_attributes=required_filter_attributes,
),
*parametrize_trigger_states(
trigger=trigger,
trigger_options={
CONF_THRESHOLD_TYPE: ThresholdType.BELOW,
CONF_UPPER_LIMIT: 90,
**trigger_options,
},
target_states=[
(state, {attribute: 0} | unit_attributes),
(state, {attribute: 50} | unit_attributes),
(state, {attribute: 0}),
(state, {attribute: 50}),
],
other_states=[
(state, {attribute: None} | unit_attributes),
(state, {attribute: 100} | unit_attributes),
(state, {attribute: None}),
(state, {attribute: 100}),
],
required_filter_attributes=required_filter_attributes,
),
]
def parametrize_numerical_state_value_changed_trigger_states(
trigger: str,
*,
device_class: str,
trigger_options: dict[str, Any] | None = None,
unit_attributes: dict | None = None,
trigger: str, device_class: str
) -> list[tuple[str, dict[str, Any], list[TriggerStateDescription]]]:
"""Parametrize states and expected service call counts for numerical state-value changed triggers.
@@ -679,37 +646,30 @@ def parametrize_numerical_state_value_changed_trigger_states(
from homeassistant.const import ATTR_DEVICE_CLASS # noqa: PLC0415
required_filter_attributes = {ATTR_DEVICE_CLASS: device_class}
trigger_options = trigger_options or {}
unit_attributes = unit_attributes or {}
return [
*parametrize_trigger_states(
trigger=trigger,
trigger_options=trigger_options,
target_states=[
("0", unit_attributes),
("50", unit_attributes),
("100", unit_attributes),
],
other_states=[("none", unit_attributes)],
trigger_options={},
target_states=["0", "50", "100"],
other_states=["none"],
required_filter_attributes=required_filter_attributes,
retrigger_on_target_state=True,
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger=trigger,
trigger_options={CONF_ABOVE: 10} | trigger_options,
target_states=[("50", unit_attributes), ("100", unit_attributes)],
other_states=[("none", unit_attributes), ("0", unit_attributes)],
trigger_options={CONF_ABOVE: 10},
target_states=["50", "100"],
other_states=["none", "0"],
required_filter_attributes=required_filter_attributes,
retrigger_on_target_state=True,
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger=trigger,
trigger_options={CONF_BELOW: 90} | trigger_options,
target_states=[("0", unit_attributes), ("50", unit_attributes)],
other_states=[("none", unit_attributes), ("100", unit_attributes)],
trigger_options={CONF_BELOW: 90},
target_states=["0", "50"],
other_states=["none", "100"],
required_filter_attributes=required_filter_attributes,
retrigger_on_target_state=True,
trigger_from_none=False,
@@ -718,11 +678,7 @@ def parametrize_numerical_state_value_changed_trigger_states(
def parametrize_numerical_state_value_crossed_threshold_trigger_states(
trigger: str,
*,
device_class: str,
trigger_options: dict[str, Any] | None = None,
unit_attributes: dict | None = None,
trigger: str, device_class: str
) -> list[tuple[str, dict[str, Any], list[TriggerStateDescription]]]:
"""Parametrize states and expected service call counts for numerical state-value crossed threshold triggers.
@@ -733,9 +689,6 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
from homeassistant.const import ATTR_DEVICE_CLASS # noqa: PLC0415
required_filter_attributes = {ATTR_DEVICE_CLASS: device_class}
trigger_options = trigger_options or {}
unit_attributes = unit_attributes or {}
return [
*parametrize_trigger_states(
trigger=trigger,
@@ -743,14 +696,9 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
CONF_LOWER_LIMIT: 10,
CONF_UPPER_LIMIT: 90,
**trigger_options,
},
target_states=[("50", unit_attributes), ("60", unit_attributes)],
other_states=[
("none", unit_attributes),
("0", unit_attributes),
("100", unit_attributes),
],
target_states=["50", "60"],
other_states=["none", "0", "100"],
required_filter_attributes=required_filter_attributes,
trigger_from_none=False,
),
@@ -760,14 +708,9 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
CONF_LOWER_LIMIT: 10,
CONF_UPPER_LIMIT: 90,
**trigger_options,
},
target_states=[("0", unit_attributes), ("100", unit_attributes)],
other_states=[
("none", unit_attributes),
("50", unit_attributes),
("60", unit_attributes),
],
target_states=["0", "100"],
other_states=["none", "50", "60"],
required_filter_attributes=required_filter_attributes,
trigger_from_none=False,
),
@@ -776,10 +719,9 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
trigger_options={
CONF_THRESHOLD_TYPE: ThresholdType.ABOVE,
CONF_LOWER_LIMIT: 10,
**trigger_options,
},
target_states=[("50", unit_attributes), ("100", unit_attributes)],
other_states=[("none", unit_attributes), ("0", unit_attributes)],
target_states=["50", "100"],
other_states=["none", "0"],
required_filter_attributes=required_filter_attributes,
trigger_from_none=False,
),
@@ -788,10 +730,9 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
trigger_options={
CONF_THRESHOLD_TYPE: ThresholdType.BELOW,
CONF_UPPER_LIMIT: 90,
**trigger_options,
},
target_states=[("0", unit_attributes), ("50", unit_attributes)],
other_states=[("none", unit_attributes), ("100", unit_attributes)],
target_states=["0", "50"],
other_states=["none", "100"],
required_filter_attributes=required_filter_attributes,
trigger_from_none=False,
),

View File

@@ -49,13 +49,13 @@ LOG_FAIL_GATEWAY = (
"homeassistant.components.evohome",
logging.ERROR,
"Failed to fetch initial data: "
"Authenticator response is invalid: 502 Bad Gateway, response=<no response>",
"Authenticator response is invalid: 502 Bad Gateway, response=None",
)
LOG_FAIL_TOO_MANY = (
"homeassistant.components.evohome",
logging.ERROR,
"Failed to fetch initial data: "
"Authenticator response is invalid: 429 Too Many Requests, response=<no response>",
"Authenticator response is invalid: 429 Too Many Requests, response=None",
)
LOG_FGET_CONNECTION = (
@@ -70,14 +70,14 @@ LOG_FGET_GATEWAY = (
logging.ERROR,
"Failed to fetch initial data: "
"GET https://tccna.resideo.com/WebAPI/emea/api/v1/userAccount: "
"502 Bad Gateway",
"502 Bad Gateway, response=None",
)
LOG_FGET_TOO_MANY = (
"homeassistant.components.evohome",
logging.ERROR,
"Failed to fetch initial data: "
"GET https://tccna.resideo.com/WebAPI/emea/api/v1/userAccount: "
"429 Too Many Requests",
"429 Too Many Requests, response=None",
)

View File

@@ -76,7 +76,7 @@ async def test_set_operation_mode(
mock_fcn.assert_awaited_once_with()
# SERVICE_SET_OPERATION_MODE: off (until next scheduled setpoint)
with patch("evohomeasync2.hotwater.HotWater.set_off") as mock_fcn:
with patch("evohomeasync2.hotwater.HotWater.off") as mock_fcn:
await hass.services.async_call(
WATER_HEATER_DOMAIN,
SERVICE_SET_OPERATION_MODE,
@@ -96,7 +96,7 @@ async def test_set_operation_mode(
results.append(mock_fcn.await_args.kwargs)
# SERVICE_SET_OPERATION_MODE: on (until next scheduled setpoint)
with patch("evohomeasync2.hotwater.HotWater.set_on") as mock_fcn:
with patch("evohomeasync2.hotwater.HotWater.on") as mock_fcn:
await hass.services.async_call(
WATER_HEATER_DOMAIN,
SERVICE_SET_OPERATION_MODE,
@@ -137,7 +137,7 @@ async def test_set_away_mode(hass: HomeAssistant, evohome: EvohomeClient) -> Non
mock_fcn.assert_awaited_once_with()
# set_away_mode: on
with patch("evohomeasync2.hotwater.HotWater.set_off") as mock_fcn:
with patch("evohomeasync2.hotwater.HotWater.off") as mock_fcn:
await hass.services.async_call(
WATER_HEATER_DOMAIN,
SERVICE_SET_AWAY_MODE,
@@ -156,7 +156,7 @@ async def test_turn_off(hass: HomeAssistant, evohome: EvohomeClient) -> None:
"""Test SERVICE_TURN_OFF of an evohome DHW zone."""
# turn_off
with patch("evohomeasync2.hotwater.HotWater.set_off") as mock_fcn:
with patch("evohomeasync2.hotwater.HotWater.off") as mock_fcn:
await hass.services.async_call(
WATER_HEATER_DOMAIN,
SERVICE_TURN_OFF,
@@ -174,7 +174,7 @@ async def test_turn_on(hass: HomeAssistant, evohome: EvohomeClient) -> None:
"""Test SERVICE_TURN_ON of an evohome DHW zone."""
# turn_on
with patch("evohomeasync2.hotwater.HotWater.set_on") as mock_fcn:
with patch("evohomeasync2.hotwater.HotWater.on") as mock_fcn:
await hass.services.async_call(
WATER_HEATER_DOMAIN,
SERVICE_TURN_ON,

View File

@@ -11,7 +11,6 @@ from homeassistant.components.climate import (
from homeassistant.components.humidifier import (
ATTR_CURRENT_HUMIDITY as HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.weather import ATTR_WEATHER_HUMIDITY
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, STATE_ON
from homeassistant.core import HomeAssistant, ServiceCall
@@ -82,10 +81,10 @@ async def test_humidity_triggers_gated_by_labs_flag(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_changed_trigger_states(
"humidity.changed", device_class=SensorDeviceClass.HUMIDITY
"humidity.changed", "humidity"
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"humidity.crossed_threshold", device_class=SensorDeviceClass.HUMIDITY
"humidity.crossed_threshold", "humidity"
),
],
)
@@ -123,7 +122,7 @@ async def test_humidity_trigger_sensor_behavior_any(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"humidity.crossed_threshold", device_class=SensorDeviceClass.HUMIDITY
"humidity.crossed_threshold", "humidity"
),
],
)
@@ -161,7 +160,7 @@ async def test_humidity_trigger_sensor_crossed_threshold_behavior_first(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"humidity.crossed_threshold", device_class=SensorDeviceClass.HUMIDITY
"humidity.crossed_threshold", "humidity"
),
],
)

View File

@@ -1 +0,0 @@
"""Tests for the illuminance integration."""

View File

@@ -1,482 +0,0 @@
"""Test illuminance trigger."""
from typing import Any
import pytest
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.number import NumberDeviceClass
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
LIGHT_LUX,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant, ServiceCall
from tests.components.common import (
TriggerStateDescription,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_numerical_state_value_changed_trigger_states,
parametrize_numerical_state_value_crossed_threshold_trigger_states,
parametrize_target_entities,
parametrize_trigger_states,
target_entities,
)
@pytest.fixture
async def target_binary_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple binary sensor entities associated with different targets."""
return await target_entities(hass, "binary_sensor")
@pytest.fixture
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple number entities associated with different targets."""
return await target_entities(hass, "number")
@pytest.fixture
async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple sensor entities associated with different targets."""
return await target_entities(hass, "sensor")
@pytest.mark.parametrize(
"trigger_key",
[
"illuminance.detected",
"illuminance.cleared",
"illuminance.changed",
"illuminance.crossed_threshold",
],
)
async def test_illuminance_triggers_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
) -> None:
"""Test the illuminance triggers are gated by the labs flag."""
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
# --- Binary sensor detected/cleared tests ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("binary_sensor"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_trigger_states(
trigger="illuminance.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LIGHT
},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="illuminance.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LIGHT
},
trigger_from_none=False,
),
],
)
async def test_illuminance_trigger_binary_sensor_behavior_any(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_binary_sensors: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance trigger fires for binary_sensor entities with device_class light."""
await assert_trigger_behavior_any(
hass,
service_calls=service_calls,
target_entities=target_binary_sensors,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("binary_sensor"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_trigger_states(
trigger="illuminance.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LIGHT
},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="illuminance.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LIGHT
},
trigger_from_none=False,
),
],
)
async def test_illuminance_trigger_binary_sensor_behavior_first(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_binary_sensors: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance trigger fires on the first binary_sensor state change."""
await assert_trigger_behavior_first(
hass,
service_calls=service_calls,
target_entities=target_binary_sensors,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("binary_sensor"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_trigger_states(
trigger="illuminance.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LIGHT
},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="illuminance.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LIGHT
},
trigger_from_none=False,
),
],
)
async def test_illuminance_trigger_binary_sensor_behavior_last(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_binary_sensors: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance trigger fires when the last binary_sensor changes state."""
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_binary_sensors,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
# --- Sensor changed/crossed_threshold tests ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("sensor"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_changed_trigger_states(
"illuminance.changed",
device_class=SensorDeviceClass.ILLUMINANCE,
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"illuminance.crossed_threshold",
device_class=SensorDeviceClass.ILLUMINANCE,
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
),
],
)
async def test_illuminance_trigger_sensor_behavior_any(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_sensors: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance trigger fires for sensor entities with device_class illuminance."""
await assert_trigger_behavior_any(
hass,
service_calls=service_calls,
target_entities=target_sensors,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("sensor"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"illuminance.crossed_threshold",
device_class=SensorDeviceClass.ILLUMINANCE,
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
),
],
)
async def test_illuminance_trigger_sensor_crossed_threshold_behavior_first(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_sensors: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance crossed_threshold trigger fires on the first sensor state change."""
await assert_trigger_behavior_first(
hass,
service_calls=service_calls,
target_entities=target_sensors,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("sensor"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"illuminance.crossed_threshold",
device_class=SensorDeviceClass.ILLUMINANCE,
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
),
],
)
async def test_illuminance_trigger_sensor_crossed_threshold_behavior_last(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_sensors: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance crossed_threshold trigger fires when the last sensor changes state."""
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_sensors,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
# --- Number changed/crossed_threshold tests ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("number"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_changed_trigger_states(
"illuminance.changed",
device_class=NumberDeviceClass.ILLUMINANCE,
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"illuminance.crossed_threshold",
device_class=NumberDeviceClass.ILLUMINANCE,
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
),
],
)
async def test_illuminance_trigger_number_behavior_any(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_numbers: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance trigger fires for number entities with device_class illuminance."""
await assert_trigger_behavior_any(
hass,
service_calls=service_calls,
target_entities=target_numbers,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("number"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"illuminance.crossed_threshold",
device_class=NumberDeviceClass.ILLUMINANCE,
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
),
],
)
async def test_illuminance_trigger_number_crossed_threshold_behavior_first(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_numbers: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance crossed_threshold trigger fires on the first number state change."""
await assert_trigger_behavior_first(
hass,
service_calls=service_calls,
target_entities=target_numbers,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("number"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"illuminance.crossed_threshold",
device_class=NumberDeviceClass.ILLUMINANCE,
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
),
],
)
async def test_illuminance_trigger_number_crossed_threshold_behavior_last(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_numbers: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test illuminance crossed_threshold trigger fires when the last number changes state."""
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_numbers,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -287,9 +287,7 @@ async def mock_immich(
) -> AsyncGenerator[AsyncMock]:
"""Mock the Immich API."""
with (
patch(
"homeassistant.components.immich.coordinator.Immich", autospec=True
) as mock_immich,
patch("homeassistant.components.immich.Immich", autospec=True) as mock_immich,
patch("homeassistant.components.immich.config_flow.Immich", new=mock_immich),
):
client = mock_immich.return_value

View File

@@ -0,0 +1,175 @@
"""Test input boolean triggers."""
from typing import Any
import pytest
from homeassistant.components.input_boolean import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, ServiceCall
from tests.components.common import (
TriggerStateDescription,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
target_entities,
)
@pytest.fixture
async def target_input_booleans(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple input_boolean entities associated with different targets."""
return await target_entities(hass, DOMAIN)
@pytest.mark.parametrize(
"trigger_key",
[
"input_boolean.turned_off",
"input_boolean.turned_on",
],
)
async def test_input_boolean_triggers_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
) -> None:
"""Test the input_boolean triggers are gated by the labs flag."""
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities(DOMAIN),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_trigger_states(
trigger="input_boolean.turned_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
),
*parametrize_trigger_states(
trigger="input_boolean.turned_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
],
)
async def test_input_boolean_state_trigger_behavior_any(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_input_booleans: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test that the input_boolean state trigger fires when any input_boolean state changes to a specific state."""
await assert_trigger_behavior_any(
hass,
service_calls=service_calls,
target_entities=target_input_booleans,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities(DOMAIN),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_trigger_states(
trigger="input_boolean.turned_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
),
*parametrize_trigger_states(
trigger="input_boolean.turned_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
],
)
async def test_input_boolean_state_trigger_behavior_first(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_input_booleans: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test that the input_boolean state trigger fires when the first input_boolean changes to a specific state."""
await assert_trigger_behavior_first(
hass,
service_calls=service_calls,
target_entities=target_input_booleans,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities(DOMAIN),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_trigger_states(
trigger="input_boolean.turned_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
),
*parametrize_trigger_states(
trigger="input_boolean.turned_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
],
)
async def test_input_boolean_state_trigger_behavior_last(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_input_booleans: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[TriggerStateDescription],
) -> None:
"""Test that the input_boolean state trigger fires when the last input_boolean changes to a specific state."""
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_input_booleans,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -5474,7 +5474,6 @@
'hens_eggs_size_xl_soft',
'huanghuanian_rapid_steam_cooking',
'huanghuanian_steam_cooking',
'hydroclean',
'intensive_bake',
'iridescent_shark_fillet',
'jasmine_rice_rapid_steam_cooking',
@@ -5676,7 +5675,6 @@
'rhubarb_chunks',
'rice_pudding_rapid_steam_cooking',
'rice_pudding_steam_cooking',
'rinse',
'risotto',
'roast_beef_low_temperature_cooking',
'roast_beef_roast',
@@ -6087,7 +6085,6 @@
'hens_eggs_size_xl_soft',
'huanghuanian_rapid_steam_cooking',
'huanghuanian_steam_cooking',
'hydroclean',
'intensive_bake',
'iridescent_shark_fillet',
'jasmine_rice_rapid_steam_cooking',
@@ -6289,7 +6286,6 @@
'rhubarb_chunks',
'rice_pudding_rapid_steam_cooking',
'rice_pudding_steam_cooking',
'rinse',
'risotto',
'roast_beef_low_temperature_cooking',
'roast_beef_roast',
@@ -9272,7 +9268,6 @@
'hens_eggs_size_xl_soft',
'huanghuanian_rapid_steam_cooking',
'huanghuanian_steam_cooking',
'hydroclean',
'intensive_bake',
'iridescent_shark_fillet',
'jasmine_rice_rapid_steam_cooking',
@@ -9474,7 +9469,6 @@
'rhubarb_chunks',
'rice_pudding_rapid_steam_cooking',
'rice_pudding_steam_cooking',
'rinse',
'risotto',
'roast_beef_low_temperature_cooking',
'roast_beef_roast',
@@ -9885,7 +9879,6 @@
'hens_eggs_size_xl_soft',
'huanghuanian_rapid_steam_cooking',
'huanghuanian_steam_cooking',
'hydroclean',
'intensive_bake',
'iridescent_shark_fillet',
'jasmine_rice_rapid_steam_cooking',
@@ -10087,7 +10080,6 @@
'rhubarb_chunks',
'rice_pudding_rapid_steam_cooking',
'rice_pudding_steam_cooking',
'rinse',
'risotto',
'roast_beef_low_temperature_cooking',
'roast_beef_roast',

View File

@@ -26,7 +26,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None:
result["flow_id"], user_input={}
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "Triumph D36 48BE"
assert result2["title"] == "Smart Series 7000 48BE"
assert result2["data"] == {}
assert result2["result"].unique_id == "78:DB:2F:C2:48:BE"
@@ -91,7 +91,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None:
user_input={"address": "78:DB:2F:C2:48:BE"},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "Triumph D36 48BE"
assert result2["title"] == "Smart Series 7000 48BE"
assert result2["data"] == {}
assert result2["result"].unique_id == "78:DB:2F:C2:48:BE"
@@ -121,7 +121,7 @@ async def test_async_step_user_replace_ignored(hass: HomeAssistant) -> None:
user_input={"address": "78:DB:2F:C2:48:BE"},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "Triumph D36 48BE"
assert result2["title"] == "Smart Series 7000 48BE"
assert result2["data"] == {}
assert result2["result"].unique_id == "78:DB:2F:C2:48:BE"
@@ -240,7 +240,7 @@ async def test_async_step_user_takes_precedence_over_discovery(
user_input={"address": "78:DB:2F:C2:48:BE"},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "Triumph D36 48BE"
assert result2["title"] == "Smart Series 7000 48BE"
assert result2["data"] == {}
assert result2["result"].unique_id == "78:DB:2F:C2:48:BE"

View File

@@ -47,10 +47,10 @@ async def test_sensors(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert len(hass.states.async_all("sensor")) == 9
toothbrush_sensor = hass.states.get("sensor.triumph_d36_48be")
toothbrush_sensor = hass.states.get("sensor.smart_series_7000_48be")
toothbrush_sensor_attrs = toothbrush_sensor.attributes
assert toothbrush_sensor.state == "running"
assert toothbrush_sensor_attrs[ATTR_FRIENDLY_NAME] == "Triumph D36 48BE"
assert toothbrush_sensor_attrs[ATTR_FRIENDLY_NAME] == "Smart Series 7000 48BE"
assert ATTR_ASSUMED_STATE not in toothbrush_sensor_attrs
assert await hass.config_entries.async_unload(entry.entry_id)
@@ -76,7 +76,7 @@ async def test_sensors(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
# All of these devices are sleepy so we should still be available
toothbrush_sensor = hass.states.get("sensor.triumph_d36_48be")
toothbrush_sensor = hass.states.get("sensor.smart_series_7000_48be")
assert toothbrush_sensor.state == "running"
@@ -155,9 +155,9 @@ async def test_sensors_battery(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 7
bat_sensor = hass.states.get("sensor.io_series_1dcf_battery")
bat_sensor = hass.states.get("sensor.io_series_6_7_1dcf_battery")
assert bat_sensor.state == "49"
assert bat_sensor.name == "IO Series 1DCF Battery"
assert bat_sensor.name == "IO Series 6/7 1DCF Battery"
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()

View File

@@ -653,56 +653,6 @@
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_shutdown-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': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_db_shutdown',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Shutdown',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Shutdown',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'shutdown',
'unique_id': '1234_101_shutdown',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_db_shutdown-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'vm-db Shutdown',
}),
'context': <ANY>,
'entity_id': 'button.vm_db_shutdown',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_db_start-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -954,56 +904,6 @@
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_web_shutdown-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': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.vm_web_shutdown',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Shutdown',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Shutdown',
'platform': 'proxmoxve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'shutdown',
'unique_id': '1234_100_shutdown',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities[button.vm_web_shutdown-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'vm-web Shutdown',
}),
'context': <ANY>,
'entity_id': 'button.vm_web_shutdown',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities[button.vm_web_start-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -115,7 +115,6 @@ async def test_node_startall_stopall_buttons(
("button.vm_web_restart", 100, "reboot"),
("button.vm_web_hibernate", 100, "hibernate"),
("button.vm_web_reset", 100, "reset"),
("button.vm_web_shutdown", 100, "shutdown"),
],
)
async def test_vm_buttons(

View File

@@ -1570,8 +1570,8 @@ Q10_STATUS = Q10Status(
clean_time=120,
clean_area=15,
battery=100,
status=YXDeviceState.CHARGING,
status=YXDeviceState.CHARGING_STATE,
fan_level=YXFanLevel.BALANCED,
water_level=YXWaterLevel.MEDIUM,
water_level=YXWaterLevel.MIDDLE,
clean_count=1,
)

View File

@@ -1,54 +1,4 @@
# serializer version: 1
# name: test_buttons[button.roborock_q10_s5_empty_dustbin-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': 'button',
'entity_category': None,
'entity_id': 'button.roborock_q10_s5_empty_dustbin',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Empty dustbin',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Empty dustbin',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'empty_dustbin',
'unique_id': 'empty_dustbin_q10_duid',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[button.roborock_q10_s5_empty_dustbin-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Roborock Q10 S5+ Empty dustbin',
}),
'context': <ANY>,
'entity_id': 'button.roborock_q10_s5_empty_dustbin',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[button.roborock_s7_2_reset_air_filter_consumable-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -272,55 +272,3 @@ async def test_press_a01_button_failure(
washing_machine.zeo.set_value.assert_called_once()
assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00"
@pytest.mark.freeze_time("2023-10-30 08:50:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_press_q10_empty_dustbin_button_success(
hass: HomeAssistant,
bypass_api_client_fixture: None,
setup_entry: MockConfigEntry,
fake_q10_vacuum: FakeDevice,
) -> None:
"""Test pressing Q10 empty dustbin button entity."""
entity_id = "button.roborock_q10_s5_empty_dustbin"
assert hass.states.get(entity_id) is not None
await hass.services.async_call(
"button",
SERVICE_PRESS,
blocking=True,
target={"entity_id": entity_id},
)
assert fake_q10_vacuum.b01_q10_properties is not None
fake_q10_vacuum.b01_q10_properties.vacuum.empty_dustbin.assert_called_once()
assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00"
@pytest.mark.freeze_time("2023-10-30 08:50:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_press_q10_empty_dustbin_button_failure(
hass: HomeAssistant,
bypass_api_client_fixture: None,
setup_entry: MockConfigEntry,
fake_q10_vacuum: FakeDevice,
) -> None:
"""Test failure while pressing Q10 empty dustbin button entity."""
entity_id = "button.roborock_q10_s5_empty_dustbin"
assert fake_q10_vacuum.b01_q10_properties is not None
fake_q10_vacuum.b01_q10_properties.vacuum.empty_dustbin.side_effect = (
RoborockException
)
assert hass.states.get(entity_id) is not None
with pytest.raises(HomeAssistantError, match="Error while calling empty_dustbin"):
await hass.services.async_call(
"button",
SERVICE_PRESS,
blocking=True,
target={"entity_id": entity_id},
)
fake_q10_vacuum.b01_q10_properties.vacuum.empty_dustbin.assert_called_once()
assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00"

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