mirror of
https://github.com/home-assistant/core.git
synced 2026-05-07 10:26:51 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2ce313ec8 | |||
| b8ba1c123d | |||
| 10f1cbb51e | |||
| e3bcce06bf | |||
| 4e0472feb5 | |||
| 046298f2ca | |||
| c92128b282 | |||
| 886e66e7e3 | |||
| 7da49570b5 | |||
| b8baa3271b | |||
| 65bc4bf1d0 | |||
| 27a8d185c9 | |||
| 1e5992f2b5 | |||
| ac84a14846 | |||
| fa265b18ce | |||
| 38634ddd55 | |||
| 13dd831874 | |||
| 3be5906398 |
@@ -323,7 +323,7 @@ jobs:
|
||||
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
|
||||
publish_container:
|
||||
name: Publish meta container for ${{ matrix.registry }}
|
||||
name: Publish to ${{ matrix.registry }}
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
|
||||
Generated
+2
-2
@@ -1495,8 +1495,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/roku/ @ctalkington
|
||||
/homeassistant/components/romy/ @xeniter
|
||||
/tests/components/romy/ @xeniter
|
||||
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn
|
||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn
|
||||
/homeassistant/components/roon/ @pavoni
|
||||
/tests/components/roon/ @pavoni
|
||||
/homeassistant/components/route_b_smart_meter/ @SeraphicRav
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["serialx==1.7.0"]
|
||||
"requirements": ["serialx==1.7.1"]
|
||||
}
|
||||
|
||||
@@ -1,36 +1,16 @@
|
||||
"""Provides triggers for buttons."""
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
)
|
||||
from homeassistant.helpers.trigger import StatelessEntityTriggerBase, Trigger
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class ButtonPressedTrigger(EntityTriggerBase):
|
||||
class ButtonPressedTrigger(StatelessEntityTriggerBase):
|
||||
"""Trigger for button entity presses."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and different from the current state."""
|
||||
|
||||
# UNKNOWN is a valid from_state, otherwise the first time the button is pressed
|
||||
# would not trigger
|
||||
if from_state.state == STATE_UNAVAILABLE:
|
||||
return False
|
||||
|
||||
return from_state.state != to_state.state
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state is not invalid."""
|
||||
return state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
|
||||
@@ -6,39 +6,22 @@ from homeassistant.components.event import (
|
||||
DoorbellEventType,
|
||||
EventDeviceClass,
|
||||
)
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
)
|
||||
from homeassistant.helpers.trigger import StatelessEntityTriggerBase, Trigger
|
||||
|
||||
|
||||
class DoorbellRangTrigger(EntityTriggerBase):
|
||||
class DoorbellRangTrigger(StatelessEntityTriggerBase):
|
||||
"""Trigger for doorbell event entity when a ring event is received."""
|
||||
|
||||
_domain_specs = {EVENT_DOMAIN: DomainSpec(device_class=EventDeviceClass.DOORBELL)}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the entity is available and the event type is ring."""
|
||||
return (
|
||||
state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
and state.attributes.get(ATTR_EVENT_TYPE) == DoorbellEventType.RING
|
||||
return super().is_valid_state(state) and (
|
||||
state.attributes.get(ATTR_EVENT_TYPE) == DoorbellEventType.RING
|
||||
)
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and different from the current state."""
|
||||
|
||||
# UNKNOWN is a valid from_state, otherwise the first time the event is received
|
||||
# would not trigger
|
||||
if from_state.state == STATE_UNAVAILABLE:
|
||||
return False
|
||||
|
||||
return from_state.state != to_state.state
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"rang": DoorbellRangTrigger,
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_OPTIONS, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.const import CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
StatelessEntityTriggerBase,
|
||||
Trigger,
|
||||
TriggerConfig,
|
||||
)
|
||||
@@ -28,7 +28,7 @@ EVENT_RECEIVED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
class EventReceivedTrigger(EntityTriggerBase):
|
||||
class EventReceivedTrigger(StatelessEntityTriggerBase):
|
||||
"""Trigger for event entity when it receives a matching event."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
@@ -39,21 +39,10 @@ class EventReceivedTrigger(EntityTriggerBase):
|
||||
super().__init__(hass, config)
|
||||
self._event_types = set(self._options[CONF_EVENT_TYPE])
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and different from the current state."""
|
||||
|
||||
# UNKNOWN is a valid from_state, otherwise the first time the event is received
|
||||
# would not trigger
|
||||
if from_state.state == STATE_UNAVAILABLE:
|
||||
return False
|
||||
|
||||
return from_state.state != to_state.state
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the event type is valid and matches one of the configured types."""
|
||||
return (
|
||||
state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
and state.attributes.get(ATTR_EVENT_TYPE) in self._event_types
|
||||
"""Check if the event type matches one of the configured types."""
|
||||
return super().is_valid_state(state) and (
|
||||
state.attributes.get(ATTR_EVENT_TYPE) in self._event_types
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
|
||||
"requirements": ["gardena-bluetooth==2.4.0"]
|
||||
"requirements": ["gardena-bluetooth==2.8.1"]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import voluptuous as vol
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.providers import homeassistant as auth_ha
|
||||
from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, HomeAssistantView
|
||||
from homeassistant.components.http.const import is_supervisor_unix_socket_request
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -41,14 +42,18 @@ class HassIOBaseAuth(HomeAssistantView):
|
||||
|
||||
def _check_access(self, request: web.Request) -> None:
|
||||
"""Check if this call is from Supervisor."""
|
||||
# Check caller IP
|
||||
hassio_ip = os.environ["SUPERVISOR"].split(":")[0]
|
||||
assert request.transport
|
||||
if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address(
|
||||
hassio_ip
|
||||
):
|
||||
_LOGGER.error("Invalid auth request from %s", request.remote)
|
||||
raise HTTPUnauthorized
|
||||
# Requests over the Supervisor Unix socket are authenticated by the
|
||||
# http auth middleware as the Supervisor user, so the caller-IP check
|
||||
# below does not apply (and would crash, since `peername` is empty for
|
||||
# Unix sockets). The user-ID check still runs to ensure only the
|
||||
# Supervisor user can reach this endpoint.
|
||||
if not is_supervisor_unix_socket_request(request):
|
||||
hassio_ip = os.environ["SUPERVISOR"].split(":")[0]
|
||||
assert request.transport
|
||||
peername = request.transport.get_extra_info("peername")
|
||||
if not peername or ip_address(peername[0]) != ip_address(hassio_ip):
|
||||
_LOGGER.error("Invalid auth request from %s", request.remote)
|
||||
raise HTTPUnauthorized
|
||||
|
||||
# Check caller token
|
||||
if request[KEY_HASS_USER].id != self.user.id:
|
||||
|
||||
@@ -44,14 +44,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> bool
|
||||
except HiveReauthRequired as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
hub_data = devices["parent"][0]
|
||||
connections: set[tuple[str, str]] = set()
|
||||
if mac := hub_data.get("macAddress"):
|
||||
connections.add((dr.CONNECTION_NETWORK_MAC, dr.format_mac(mac)))
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, devices["parent"][0]["device_id"])},
|
||||
name=devices["parent"][0]["hiveName"],
|
||||
model=devices["parent"][0]["deviceData"]["model"],
|
||||
sw_version=devices["parent"][0]["deviceData"]["version"],
|
||||
manufacturer=devices["parent"][0]["deviceData"]["manufacturer"],
|
||||
identifiers={(DOMAIN, hub_data["device_id"])},
|
||||
connections=connections,
|
||||
name=hub_data["hiveName"],
|
||||
model=hub_data["deviceData"]["model"],
|
||||
sw_version=hub_data["deviceData"]["version"],
|
||||
manufacturer=hub_data["deviceData"]["manufacturer"],
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.95", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.96", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.9.0"]
|
||||
"requirements": ["homematicip==2.10.0"]
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.4.0"]
|
||||
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.8.1"]
|
||||
}
|
||||
|
||||
@@ -76,14 +76,12 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str,
|
||||
# The default for new entries is to not include text and headers
|
||||
vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): EVENT_MESSAGE_DATA_SELECTOR,
|
||||
vol.Optional(
|
||||
CONF_SSL_CIPHER_LIST, default=SSLCipherList.PYTHON_DEFAULT
|
||||
): CIPHER_SELECTOR,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): BOOLEAN_SELECTOR,
|
||||
}
|
||||
)
|
||||
CONFIG_SCHEMA_ADVANCED = {
|
||||
vol.Optional(
|
||||
CONF_SSL_CIPHER_LIST, default=SSLCipherList.PYTHON_DEFAULT
|
||||
): CIPHER_SELECTOR,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): BOOLEAN_SELECTOR,
|
||||
}
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -93,18 +91,15 @@ OPTIONS_SCHEMA = vol.Schema(
|
||||
vol.Optional(
|
||||
CONF_EVENT_MESSAGE_DATA, default=MESSAGE_DATA_OPTIONS
|
||||
): EVENT_MESSAGE_DATA_SELECTOR,
|
||||
vol.Optional(CONF_CUSTOM_EVENT_DATA_TEMPLATE): TEMPLATE_SELECTOR,
|
||||
vol.Optional(CONF_MAX_MESSAGE_SIZE, default=DEFAULT_MAX_MESSAGE_SIZE): vol.All(
|
||||
cv.positive_int,
|
||||
vol.Range(min=DEFAULT_MAX_MESSAGE_SIZE, max=MAX_MESSAGE_SIZE_LIMIT),
|
||||
),
|
||||
vol.Optional(CONF_ENABLE_PUSH, default=True): BOOLEAN_SELECTOR,
|
||||
}
|
||||
)
|
||||
|
||||
OPTIONS_SCHEMA_ADVANCED = {
|
||||
vol.Optional(CONF_CUSTOM_EVENT_DATA_TEMPLATE): TEMPLATE_SELECTOR,
|
||||
vol.Optional(CONF_MAX_MESSAGE_SIZE, default=DEFAULT_MAX_MESSAGE_SIZE): vol.All(
|
||||
cv.positive_int,
|
||||
vol.Range(min=DEFAULT_MAX_MESSAGE_SIZE, max=MAX_MESSAGE_SIZE_LIMIT),
|
||||
),
|
||||
vol.Optional(CONF_ENABLE_PUSH, default=True): BOOLEAN_SELECTOR,
|
||||
}
|
||||
|
||||
|
||||
async def validate_input(
|
||||
hass: HomeAssistant, user_input: dict[str, Any]
|
||||
@@ -151,8 +146,6 @@ class IMAPConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
|
||||
schema = CONFIG_SCHEMA
|
||||
if self.show_advanced_options:
|
||||
schema = schema.extend(CONFIG_SCHEMA_ADVANCED)
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=schema)
|
||||
@@ -250,8 +243,6 @@ class ImapOptionsFlow(OptionsFlow):
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
schema = OPTIONS_SCHEMA
|
||||
if self.show_advanced_options:
|
||||
schema = schema.extend(OPTIONS_SCHEMA_ADVANCED)
|
||||
schema = self.add_suggested_values_to_schema(schema, entry_data)
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=schema, errors=errors)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
@@ -61,29 +61,6 @@ async def async_unload_entry(
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: MetWeatherConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old config entry."""
|
||||
if config_entry.version > 1:
|
||||
return False
|
||||
|
||||
if config_entry.version == 1 and config_entry.minor_version < 2:
|
||||
data = dict(config_entry.data)
|
||||
title = config_entry.title
|
||||
if name := data.pop(CONF_NAME, None):
|
||||
title = name
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=data,
|
||||
title=title,
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def cleanup_old_device(hass: HomeAssistant) -> None:
|
||||
"""Cleanup device without proper device identifier."""
|
||||
device_reg = dr.async_get(hass)
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
CONF_ELEVATION,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
UnitOfLength,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -33,15 +34,6 @@ from .const import (
|
||||
)
|
||||
|
||||
|
||||
def _location_data(user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return config entry data for a fixed location."""
|
||||
return {
|
||||
CONF_LATITUDE: user_input[CONF_LATITUDE],
|
||||
CONF_LONGITUDE: user_input[CONF_LONGITUDE],
|
||||
CONF_ELEVATION: user_input[CONF_ELEVATION],
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def configured_instances(hass: HomeAssistant) -> set[str]:
|
||||
"""Return a set of configured met.no instances."""
|
||||
@@ -64,6 +56,7 @@ def _get_data_schema(
|
||||
if config_entry is None or config_entry.data.get(CONF_TRACK_HOME, False):
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=HOME_LOCATION_NAME): str,
|
||||
vol.Required(CONF_LATITUDE, default=hass.config.latitude): cv.latitude,
|
||||
vol.Required(
|
||||
CONF_LONGITUDE, default=hass.config.longitude
|
||||
@@ -81,6 +74,7 @@ def _get_data_schema(
|
||||
# Not tracking home, default values come from config entry
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=config_entry.data.get(CONF_NAME)): str,
|
||||
vol.Required(
|
||||
CONF_LATITUDE, default=config_entry.data.get(CONF_LATITUDE)
|
||||
): cv.latitude,
|
||||
@@ -103,7 +97,6 @@ class MetConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Met component."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -112,13 +105,14 @@ class MetConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
data = _location_data(user_input)
|
||||
if (
|
||||
f"{data[CONF_LATITUDE]}-{data[CONF_LONGITUDE]}"
|
||||
f"{user_input.get(CONF_LATITUDE)}-{user_input.get(CONF_LONGITUDE)}"
|
||||
not in configured_instances(self.hass)
|
||||
):
|
||||
return self.async_create_entry(title="", data=data)
|
||||
errors["base"] = "already_configured"
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
)
|
||||
errors[CONF_NAME] = "already_configured"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -160,11 +154,13 @@ class MetOptionsFlowHandler(OptionsFlowWithReload):
|
||||
"""Configure options for Met."""
|
||||
|
||||
if user_input is not None:
|
||||
data = {**self.config_entry.data, **_location_data(user_input)}
|
||||
# Update config entry with data from user input while preserving
|
||||
# existing fields such as legacy stored names.
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
|
||||
return self.async_create_entry(title=self.config_entry.title, data=data)
|
||||
# Update config entry with data from user input
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data=user_input
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=self.config_entry.title, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"data": {
|
||||
"elevation": "[%key:common::config_flow::data::elevation%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]"
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"description": "Meteorologisk institutt",
|
||||
"title": "[%key:common::config_flow::data::location%]"
|
||||
@@ -29,7 +30,8 @@
|
||||
"data": {
|
||||
"elevation": "[%key:common::config_flow::data::elevation%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]"
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"title": "[%key:common::config_flow::data::location%]"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for Met.no weather service."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
@@ -23,6 +23,7 @@ from homeassistant.components.weather import (
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
@@ -64,7 +65,9 @@ async def async_setup_entry(
|
||||
if config_entry.data.get(CONF_TRACK_HOME, False):
|
||||
name = hass.config.location_name
|
||||
else:
|
||||
name = config_entry.title or DEFAULT_NAME
|
||||
name = config_entry.data.get(CONF_NAME, DEFAULT_NAME)
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(name, str)
|
||||
|
||||
entities = [MetWeather(coordinator, config_entry, name, is_metric)]
|
||||
|
||||
|
||||
@@ -5,30 +5,31 @@ from datetime import timedelta
|
||||
from mill import Mill
|
||||
from mill_local import Mill as MillLocal
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from .coordinator import MillDataUpdateCoordinator, MillHistoricDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
MillConfigEntry,
|
||||
MillDataUpdateCoordinator,
|
||||
MillHistoricDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR]
|
||||
|
||||
__all__ = ["CLOUD", "CONNECTION_TYPE", "DOMAIN", "LOCAL"]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MillConfigEntry) -> bool:
|
||||
"""Set up the Mill heater."""
|
||||
hass.data.setdefault(DOMAIN, {LOCAL: {}, CLOUD: {}})
|
||||
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
mill_data_connection = MillLocal(
|
||||
entry.data[CONF_IP_ADDRESS],
|
||||
websession=async_get_clientsession(hass),
|
||||
)
|
||||
update_interval = timedelta(seconds=15)
|
||||
key = entry.data[CONF_IP_ADDRESS]
|
||||
conn_type = LOCAL
|
||||
else:
|
||||
mill_data_connection = Mill(
|
||||
entry.data[CONF_USERNAME],
|
||||
@@ -36,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
websession=async_get_clientsession(hass),
|
||||
)
|
||||
update_interval = timedelta(seconds=30)
|
||||
key = entry.data[CONF_USERNAME]
|
||||
conn_type = CLOUD
|
||||
|
||||
historic_data_coordinator = MillHistoricDataUpdateCoordinator(
|
||||
hass,
|
||||
@@ -56,14 +55,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
await data_coordinator.async_config_entry_first_refresh()
|
||||
# Uses legacy hass.data[DOMAIN] pattern
|
||||
# pylint: disable-next=hass-use-runtime-data
|
||||
hass.data[DOMAIN][conn_type][key] = data_coordinator
|
||||
entry.runtime_data = data_coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MillConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Support for mill wifi-enabled home heaters."""
|
||||
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
|
||||
|
||||
from typing import Any
|
||||
|
||||
@@ -14,14 +13,7 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_USERNAME,
|
||||
PRECISION_TENTHS,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
@@ -33,7 +25,6 @@ from .const import (
|
||||
ATTR_COMFORT_TEMP,
|
||||
ATTR_ROOM_NAME,
|
||||
ATTR_SLEEP_TEMP,
|
||||
CLOUD,
|
||||
CONNECTION_TYPE,
|
||||
DOMAIN,
|
||||
LOCAL,
|
||||
@@ -42,7 +33,7 @@ from .const import (
|
||||
MIN_TEMP,
|
||||
SERVICE_SET_ROOM_TEMP,
|
||||
)
|
||||
from .coordinator import MillDataUpdateCoordinator
|
||||
from .coordinator import MillConfigEntry, MillDataUpdateCoordinator
|
||||
from .entity import MillBaseEntity
|
||||
|
||||
SET_ROOM_TEMP_SCHEMA = vol.Schema(
|
||||
@@ -57,17 +48,16 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MillConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Mill climate."""
|
||||
mill_data_coordinator = entry.runtime_data
|
||||
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
mill_data_coordinator = hass.data[DOMAIN][LOCAL][entry.data[CONF_IP_ADDRESS]]
|
||||
async_add_entities([LocalMillHeater(mill_data_coordinator)])
|
||||
return
|
||||
|
||||
mill_data_coordinator = hass.data[DOMAIN][CLOUD][entry.data[CONF_USERNAME]]
|
||||
|
||||
entities = [
|
||||
MillHeater(mill_data_coordinator, mill_device)
|
||||
for mill_device in mill_data_coordinator.data.values()
|
||||
|
||||
@@ -57,6 +57,9 @@ class MillDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
)
|
||||
|
||||
|
||||
type MillConfigEntry = ConfigEntry[MillDataUpdateCoordinator]
|
||||
|
||||
|
||||
class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching Mill historic data."""
|
||||
|
||||
|
||||
@@ -3,28 +3,23 @@
|
||||
from mill import Heater, MillDevice
|
||||
|
||||
from homeassistant.components.number import NumberDeviceClass, NumberEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_USERNAME, UnitOfPower
|
||||
from homeassistant.const import UnitOfPower
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CLOUD, CONNECTION_TYPE, DOMAIN
|
||||
from .coordinator import MillDataUpdateCoordinator
|
||||
from .const import CLOUD, CONNECTION_TYPE
|
||||
from .coordinator import MillConfigEntry, MillDataUpdateCoordinator
|
||||
from .entity import MillBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MillConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Mill Number."""
|
||||
if entry.data.get(CONNECTION_TYPE) == CLOUD:
|
||||
# Uses legacy hass.data[DOMAIN] pattern
|
||||
# pylint: disable-next=hass-use-runtime-data
|
||||
mill_data_coordinator: MillDataUpdateCoordinator = hass.data[DOMAIN][CLOUD][
|
||||
entry.data[CONF_USERNAME]
|
||||
]
|
||||
mill_data_coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MillNumber(mill_data_coordinator, mill_device)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Support for mill wifi-enabled home heaters."""
|
||||
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
|
||||
|
||||
import mill
|
||||
|
||||
@@ -9,12 +8,9 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_USERNAME,
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfEnergy,
|
||||
@@ -29,11 +25,9 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
BATTERY,
|
||||
CLOUD,
|
||||
CONNECTION_TYPE,
|
||||
CONSUMPTION_TODAY,
|
||||
CONSUMPTION_YEAR,
|
||||
DOMAIN,
|
||||
ECO2,
|
||||
HUMIDITY,
|
||||
LOCAL,
|
||||
@@ -41,7 +35,7 @@ from .const import (
|
||||
TEMPERATURE,
|
||||
TVOC,
|
||||
)
|
||||
from .coordinator import MillDataUpdateCoordinator
|
||||
from .coordinator import MillConfigEntry, MillDataUpdateCoordinator
|
||||
from .entity import MillBaseEntity
|
||||
|
||||
HEATER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
@@ -146,13 +140,13 @@ SOCKET_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MillConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Mill sensor."""
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
mill_data_coordinator = hass.data[DOMAIN][LOCAL][entry.data[CONF_IP_ADDRESS]]
|
||||
mill_data_coordinator = entry.runtime_data
|
||||
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
async_add_entities(
|
||||
LocalMillSensor(
|
||||
mill_data_coordinator,
|
||||
@@ -162,8 +156,6 @@ async def async_setup_entry(
|
||||
)
|
||||
return
|
||||
|
||||
mill_data_coordinator = hass.data[DOMAIN][CLOUD][entry.data[CONF_USERNAME]]
|
||||
|
||||
entities = [
|
||||
MillSensor(
|
||||
mill_data_coordinator,
|
||||
|
||||
@@ -16,6 +16,8 @@ from typing import TYPE_CHECKING, Any
|
||||
from uuid import uuid4
|
||||
|
||||
import certifi
|
||||
import paho.mqtt.client as mqtt
|
||||
from paho.mqtt.matcher import MQTTMatcher
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -47,6 +49,7 @@ from homeassistant.setup import SetupPhases, async_pause_setup
|
||||
from homeassistant.util.collection import chunked_or_all
|
||||
from homeassistant.util.logging import catch_log_exception, log_exception
|
||||
|
||||
from .async_client import AsyncMQTTClient
|
||||
from .const import (
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
@@ -86,13 +89,6 @@ from .models import (
|
||||
)
|
||||
from .util import EnsureJobAfterCooldown, get_file_path, mqtt_config_entry_enabled
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Only import for paho-mqtt type checking here, imports are done locally
|
||||
# because integrations should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from .async_client import AsyncMQTTClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_BUFFER_SIZE = 131072 # Minimum buffer size to use if preferred size fails
|
||||
@@ -323,12 +319,6 @@ class MqttClientSetup:
|
||||
The setup of the MQTT client should be run in an executor job,
|
||||
because it accesses files, so it does IO.
|
||||
"""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
from paho.mqtt import client as mqtt # noqa: PLC0415
|
||||
|
||||
from .async_client import AsyncMQTTClient # noqa: PLC0415
|
||||
|
||||
config = self._config
|
||||
clean_session: bool | None = None
|
||||
# If no protocol setting is set in the config entry data
|
||||
@@ -561,7 +551,6 @@ class MQTT:
|
||||
"""Start the misc periodic."""
|
||||
assert self._misc_timer is None, "Misc periodic already started"
|
||||
_LOGGER.debug("%s: Starting client misc loop", self.config_entry.title)
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
# Inner function to avoid having to check late import
|
||||
# each time the function is called.
|
||||
@@ -705,7 +694,6 @@ class MQTT:
|
||||
|
||||
async def async_connect(self, client_available: asyncio.Future[bool]) -> None:
|
||||
"""Connect to the host. Does not process messages yet."""
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
result: int | None = None
|
||||
self._available_future = client_available
|
||||
@@ -763,7 +751,6 @@ class MQTT:
|
||||
|
||||
async def _reconnect_loop(self) -> None:
|
||||
"""Reconnect to the MQTT server."""
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
while True:
|
||||
if not self.connected:
|
||||
@@ -1265,9 +1252,6 @@ class MQTT:
|
||||
@callback
|
||||
def _async_handle_callback_exception(self, status: mqtt.MQTTErrorCode) -> None:
|
||||
"""Handle a callback exception."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
_LOGGER.warning(
|
||||
"Error returned from MQTT server: %s",
|
||||
@@ -1312,8 +1296,6 @@ class MQTT:
|
||||
) -> None:
|
||||
"""Wait for ACK from broker or raise on error."""
|
||||
if result_code != 0:
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mqtt_broker_error",
|
||||
@@ -1360,8 +1342,6 @@ class MQTT:
|
||||
|
||||
|
||||
def _matcher_for_topic(subscription: str) -> Callable[[str], bool]:
|
||||
from paho.mqtt.matcher import MQTTMatcher # noqa: PLC0415
|
||||
|
||||
matcher = MQTTMatcher() # type: ignore[no-untyped-call]
|
||||
matcher[subscription] = True
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key,
|
||||
)
|
||||
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
|
||||
import paho.mqtt.client as mqtt
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
@@ -5479,10 +5480,6 @@ def try_connection(
|
||||
user_input: dict[str, Any],
|
||||
) -> bool:
|
||||
"""Test if we can connect to an MQTT broker."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
mqtt_client_setup = MqttClientSetup(user_input)
|
||||
mqtt_client_setup.setup()
|
||||
client = mqtt_client_setup.client
|
||||
|
||||
@@ -9,6 +9,8 @@ from enum import StrEnum
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
from paho.mqtt.client import MQTTMessage
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, Platform
|
||||
from homeassistant.core import CALLBACK_TYPE, callback
|
||||
from homeassistant.exceptions import ServiceValidationError, TemplateError
|
||||
@@ -24,8 +26,6 @@ from homeassistant.helpers.typing import (
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from paho.mqtt.client import MQTTMessage
|
||||
|
||||
from .client import MQTT, Subscription
|
||||
from .debug_info import TimestampedPublishMessage
|
||||
from .device_trigger import Trigger
|
||||
|
||||
@@ -67,25 +67,11 @@ OPENING_CATEGORY_TO_DEVICE_CLASS: Final[dict[str | None, BinarySensorDeviceClass
|
||||
|
||||
|
||||
def get_opening_category(netatmo_device: NetatmoDevice) -> str:
|
||||
"""Helper function to get opening category from Netatmo API raw data."""
|
||||
"""Helper function to get opening category for doortag."""
|
||||
|
||||
# Iterate through each home in the raw data.
|
||||
for home in netatmo_device.data_handler.account.raw_data["homes"]:
|
||||
# Check if the modules list exists for the current home.
|
||||
if "modules" in home:
|
||||
# Iterate through each module to find a matching ID.
|
||||
for module in home["modules"]:
|
||||
if module["id"] == netatmo_device.device.entity_id:
|
||||
# We found the matching device. Get its category.
|
||||
if module.get("category") is not None:
|
||||
return cast(str, module["category"])
|
||||
raise ValueError(
|
||||
f"Device {netatmo_device.device.entity_id} found, "
|
||||
"but 'category' is missing in raw data."
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"Device {netatmo_device.device.entity_id} not found in Netatmo raw data."
|
||||
return (
|
||||
getattr(netatmo_device.device, "doortag_category", None)
|
||||
or DOORTAG_CATEGORY_OTHER
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "roomba",
|
||||
"name": "iRobot Roomba and Braava",
|
||||
"codeowners": ["@pschmitt", "@cyr-ius", "@shenxn", "@Orhideous"],
|
||||
"codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
|
||||
@@ -1,36 +1,16 @@
|
||||
"""Provides triggers for scenes."""
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
)
|
||||
from homeassistant.helpers.trigger import StatelessEntityTriggerBase, Trigger
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class SceneActivatedTrigger(EntityTriggerBase):
|
||||
class SceneActivatedTrigger(StatelessEntityTriggerBase):
|
||||
"""Trigger for scene entity activations."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and different from the current state."""
|
||||
|
||||
# UNKNOWN is a valid from_state, otherwise the first time the scene is activated
|
||||
# it would not trigger
|
||||
if from_state.state == STATE_UNAVAILABLE:
|
||||
return False
|
||||
|
||||
return from_state.state != to_state.state
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state is not invalid."""
|
||||
return state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": ["@fabaff"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/serial",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["serialx==1.7.0"]
|
||||
"requirements": ["serialx==1.7.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tibber"],
|
||||
"requirements": ["pyTibber==0.37.4"]
|
||||
"requirements": ["pyTibber==0.37.5"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_TARGET
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_OPTIONS, CONF_TARGET
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -25,6 +25,7 @@ from .const import DATA_COMPONENT, DOMAIN, TodoItemStatus
|
||||
ITEM_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
vol.Required(CONF_OPTIONS, default={}): {},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiousbwatcher==1.1.2", "serialx==1.7.0"]
|
||||
"requirements": ["aiousbwatcher==1.1.2", "serialx==1.7.1"]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["PyViCare"],
|
||||
"requirements": ["PyViCare==2.60.1"]
|
||||
"requirements": ["PyViCare==2.60.2"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.95"]
|
||||
"requirements": ["holidays==0.96"]
|
||||
}
|
||||
|
||||
@@ -626,6 +626,30 @@ class EntityOriginStateTriggerBase(EntityTriggerBase):
|
||||
)
|
||||
|
||||
|
||||
class StatelessEntityTriggerBase(EntityTriggerBase):
|
||||
"""Trigger for entities that don't carry meaningful state.
|
||||
|
||||
Used for stateless entities (buttons, scenes, doorbells, events)
|
||||
whose `state.state` is just a timestamp of the last activation.
|
||||
"""
|
||||
|
||||
_schema: vol.Schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is available and the state has changed.
|
||||
|
||||
STATE_UNKNOWN is allowed as the origin state so the first
|
||||
activation fires.
|
||||
"""
|
||||
if from_state.state == STATE_UNAVAILABLE:
|
||||
return False
|
||||
return from_state.state != to_state.state
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check that the entity has been activated at least once."""
|
||||
return state.state not in self._excluded_states
|
||||
|
||||
|
||||
NUMERICAL_ATTRIBUTE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS, default={}): vol.All(
|
||||
|
||||
@@ -63,7 +63,7 @@ PyTurboJPEG==1.8.3
|
||||
PyYAML==6.0.3
|
||||
requests==2.33.1
|
||||
securetar==2026.4.1
|
||||
serialx==1.7.0
|
||||
serialx==1.7.1
|
||||
SQLAlchemy==2.0.49
|
||||
standard-aifc==3.13.0
|
||||
standard-telnetlib==3.13.0
|
||||
|
||||
Generated
+6
-6
@@ -99,7 +99,7 @@ PyTransportNSW==0.1.1
|
||||
PyTurboJPEG==1.8.3
|
||||
|
||||
# homeassistant.components.vicare
|
||||
PyViCare==2.60.1
|
||||
PyViCare==2.60.2
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.14.3
|
||||
@@ -1051,7 +1051,7 @@ gTTS==2.5.3
|
||||
|
||||
# homeassistant.components.gardena_bluetooth
|
||||
# homeassistant.components.husqvarna_automower_ble
|
||||
gardena-bluetooth==2.4.0
|
||||
gardena-bluetooth==2.8.1
|
||||
|
||||
# homeassistant.components.google_assistant_sdk
|
||||
gassist-text==0.0.14
|
||||
@@ -1245,7 +1245,7 @@ hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.95
|
||||
holidays==0.96
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260429.3
|
||||
@@ -1260,7 +1260,7 @@ homekit-audio-proxy==1.2.1
|
||||
homelink-integration-api==0.0.1
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.9.0
|
||||
homematicip==2.10.0
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.5.0
|
||||
@@ -1947,7 +1947,7 @@ pyRFXtrx==0.31.1
|
||||
pySDCP==1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.37.4
|
||||
pyTibber==0.37.5
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.8.0
|
||||
@@ -2951,7 +2951,7 @@ sentry-sdk==2.48.0
|
||||
# homeassistant.components.acer_projector
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.usb
|
||||
serialx==1.7.0
|
||||
serialx==1.7.1
|
||||
|
||||
# homeassistant.components.sfr_box
|
||||
sfrbox-api==0.1.1
|
||||
|
||||
Generated
+6
-6
@@ -96,7 +96,7 @@ PyTransportNSW==0.1.1
|
||||
PyTurboJPEG==1.8.3
|
||||
|
||||
# homeassistant.components.vicare
|
||||
PyViCare==2.60.1
|
||||
PyViCare==2.60.2
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.14.3
|
||||
@@ -933,7 +933,7 @@ gTTS==2.5.3
|
||||
|
||||
# homeassistant.components.gardena_bluetooth
|
||||
# homeassistant.components.husqvarna_automower_ble
|
||||
gardena-bluetooth==2.4.0
|
||||
gardena-bluetooth==2.8.1
|
||||
|
||||
# homeassistant.components.google_assistant_sdk
|
||||
gassist-text==0.0.14
|
||||
@@ -1109,7 +1109,7 @@ hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.95
|
||||
holidays==0.96
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260429.3
|
||||
@@ -1124,7 +1124,7 @@ homekit-audio-proxy==1.2.1
|
||||
homelink-integration-api==0.0.1
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.9.0
|
||||
homematicip==2.10.0
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.5.0
|
||||
@@ -1690,7 +1690,7 @@ pyHomee==1.3.8
|
||||
pyRFXtrx==0.31.1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.37.4
|
||||
pyTibber==0.37.5
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.8.0
|
||||
@@ -2517,7 +2517,7 @@ sentry-sdk==2.48.0
|
||||
# homeassistant.components.acer_projector
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.usb
|
||||
serialx==1.7.0
|
||||
serialx==1.7.1
|
||||
|
||||
# homeassistant.components.sfr_box
|
||||
sfrbox-api==0.1.1
|
||||
|
||||
+1
-5
@@ -29,6 +29,7 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
from aiohttp.test_utils import unused_port as get_test_instance_port
|
||||
from annotatedyaml import load_yaml_dict, loader as yaml_loader
|
||||
import attr
|
||||
from paho.mqtt.client import MQTTMessage
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
import voluptuous as vol
|
||||
@@ -453,11 +454,6 @@ def async_fire_mqtt_message(
|
||||
retain: bool = False,
|
||||
) -> None:
|
||||
"""Fire the MQTT message."""
|
||||
# Local import to avoid processing MQTT modules when running a testcase
|
||||
# which does not use MQTT.
|
||||
|
||||
from paho.mqtt.client import MQTTMessage # noqa: PLC0415
|
||||
|
||||
from homeassistant.components.mqtt import MqttData # noqa: PLC0415
|
||||
|
||||
if isinstance(payload, str):
|
||||
|
||||
@@ -63,6 +63,9 @@ async def test_battery_conditions_gated_by_labs_flag(
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_LEVEL_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
@@ -71,6 +74,7 @@ async def test_battery_conditions_gated_by_labs_flag(
|
||||
("battery.is_not_low", {}, True, True),
|
||||
("battery.is_charging", {}, True, True),
|
||||
("battery.is_not_charging", {}, True, True),
|
||||
("battery.is_level", _LEVEL_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_battery_condition_options_validation(
|
||||
|
||||
@@ -63,6 +63,10 @@ async def test_battery_triggers_gated_by_labs_flag(
|
||||
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
|
||||
|
||||
|
||||
_LEVEL_CHANGED_THRESHOLD = {"threshold": {"type": "any"}}
|
||||
_LEVEL_CROSSED_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
@@ -71,6 +75,8 @@ async def test_battery_triggers_gated_by_labs_flag(
|
||||
("battery.not_low", {}, True, True),
|
||||
("battery.started_charging", {}, True, True),
|
||||
("battery.stopped_charging", {}, True, True),
|
||||
("battery.level_changed", _LEVEL_CHANGED_THRESHOLD, False, False),
|
||||
("battery.level_crossed_threshold", _LEVEL_CROSSED_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_battery_trigger_options_validation(
|
||||
|
||||
@@ -56,10 +56,35 @@ from tests.common import (
|
||||
async_mock_service,
|
||||
mock_device_registry,
|
||||
)
|
||||
from tests.components.common import assert_trigger_options_supported
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("calendar.event_started", {}, False, False),
|
||||
("calendar.event_ended", {}, False, False),
|
||||
],
|
||||
)
|
||||
async def test_calendar_trigger_options_validation(
|
||||
hass: HomeAssistant,
|
||||
trigger_key: str,
|
||||
base_options: dict[str, Any] | None,
|
||||
supports_behavior: bool,
|
||||
supports_duration: bool,
|
||||
) -> None:
|
||||
"""Test that calendar triggers support the expected options."""
|
||||
await assert_trigger_options_supported(
|
||||
hass,
|
||||
trigger_key,
|
||||
base_options,
|
||||
supports_behavior=supports_behavior,
|
||||
supports_duration=supports_duration,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TriggerFormat:
|
||||
"""Abstraction for different trigger configuration formats."""
|
||||
|
||||
@@ -60,6 +60,15 @@ async def test_climate_conditions_gated_by_labs_flag(
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_HUMIDITY_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
_TEMPERATURE_THRESHOLD = {
|
||||
"threshold": {
|
||||
"type": "above",
|
||||
"value": {"number": 20, "unit_of_measurement": UnitOfTemperature.CELSIUS},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
@@ -69,6 +78,9 @@ async def test_climate_conditions_gated_by_labs_flag(
|
||||
("climate.is_cooling", {}, True, True),
|
||||
("climate.is_drying", {}, True, True),
|
||||
("climate.is_heating", {}, True, True),
|
||||
("climate.is_hvac_mode", {"hvac_mode": [HVACMode.HEAT]}, True, True),
|
||||
("climate.target_humidity", _HUMIDITY_THRESHOLD, True, True),
|
||||
("climate.target_temperature", _TEMPERATURE_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_climate_condition_options_validation(
|
||||
|
||||
@@ -67,6 +67,16 @@ async def test_climate_triggers_gated_by_labs_flag(
|
||||
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
|
||||
|
||||
|
||||
_CHANGED_THRESHOLD = {"threshold": {"type": "any"}}
|
||||
_HUMIDITY_CROSSED_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
_TEMPERATURE_CROSSED_THRESHOLD = {
|
||||
"threshold": {
|
||||
"type": "above",
|
||||
"value": {"number": 20, "unit_of_measurement": UnitOfTemperature.CELSIUS},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
@@ -76,6 +86,21 @@ async def test_climate_triggers_gated_by_labs_flag(
|
||||
("climate.started_heating", {}, True, True),
|
||||
("climate.turned_off", {}, True, True),
|
||||
("climate.turned_on", {}, True, True),
|
||||
("climate.hvac_mode_changed", {"hvac_mode": [HVACMode.HEAT]}, True, True),
|
||||
("climate.target_humidity_changed", _CHANGED_THRESHOLD, False, False),
|
||||
(
|
||||
"climate.target_humidity_crossed_threshold",
|
||||
_HUMIDITY_CROSSED_THRESHOLD,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
("climate.target_temperature_changed", _CHANGED_THRESHOLD, False, False),
|
||||
(
|
||||
"climate.target_temperature_crossed_threshold",
|
||||
_TEMPERATURE_CROSSED_THRESHOLD,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_climate_trigger_options_validation(
|
||||
|
||||
@@ -25,11 +25,17 @@ async def target_counters(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "counter")
|
||||
|
||||
|
||||
async def test_counter_condition_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
"counter.is_value",
|
||||
],
|
||||
)
|
||||
async def test_counter_conditions_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str
|
||||
) -> None:
|
||||
"""Test the counter condition is gated by the labs flag."""
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, "counter.is_value")
|
||||
"""Test the counter conditions are gated by the labs flag."""
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_PLAIN_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
@@ -39,9 +39,16 @@ async def target_covers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
condition
|
||||
for _, is_open, is_closed in DEVICE_CLASS_CONDITIONS
|
||||
for condition in (is_open, is_closed)
|
||||
"cover.awning_is_closed",
|
||||
"cover.awning_is_open",
|
||||
"cover.blind_is_closed",
|
||||
"cover.blind_is_open",
|
||||
"cover.curtain_is_closed",
|
||||
"cover.curtain_is_open",
|
||||
"cover.shade_is_closed",
|
||||
"cover.shade_is_open",
|
||||
"cover.shutter_is_closed",
|
||||
"cover.shutter_is_open",
|
||||
],
|
||||
)
|
||||
async def test_cover_conditions_gated_by_labs_flag(
|
||||
|
||||
@@ -38,9 +38,16 @@ async def target_covers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
trigger
|
||||
for _, opened, closed in DEVICE_CLASS_TRIGGERS
|
||||
for trigger in (opened, closed)
|
||||
"cover.awning_closed",
|
||||
"cover.awning_opened",
|
||||
"cover.blind_closed",
|
||||
"cover.blind_opened",
|
||||
"cover.curtain_closed",
|
||||
"cover.curtain_opened",
|
||||
"cover.shade_closed",
|
||||
"cover.shade_opened",
|
||||
"cover.shutter_closed",
|
||||
"cover.shutter_opened",
|
||||
],
|
||||
)
|
||||
async def test_cover_triggers_gated_by_labs_flag(
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
"""The tests for the hassio component."""
|
||||
|
||||
from contextlib import AbstractContextManager, ExitStack as DefaultContext
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||
import pytest
|
||||
|
||||
from homeassistant.auth.providers.homeassistant import InvalidAuth
|
||||
from homeassistant.components.hassio.auth import HassIOBaseAuth
|
||||
from homeassistant.components.hassio.const import DATA_CONFIG_STORE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def test_auth_success(hassio_client_supervisor: TestClient) -> None:
|
||||
@@ -162,6 +168,45 @@ async def test_password_fails_no_auth(hassio_noauth_client: TestClient) -> None:
|
||||
assert resp.status == HTTPStatus.UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("peername", "unix_socket", "expectation"),
|
||||
[
|
||||
# Unix socket transports report an empty string for peername. Before
|
||||
# the fix this raised IndexError on `peername[0]`.
|
||||
("", True, DefaultContext()),
|
||||
# Defensive: a TCP transport with no peername at all should be
|
||||
# rejected, not crash.
|
||||
(None, False, pytest.raises(HTTPUnauthorized)),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("hassio_stubs")
|
||||
async def test_check_access_unix_socket_or_missing_peername(
|
||||
hass: HomeAssistant,
|
||||
peername: str | None,
|
||||
unix_socket: bool,
|
||||
expectation: AbstractContextManager,
|
||||
) -> None:
|
||||
"""Test _check_access handles Unix socket requests and missing peername."""
|
||||
hassio_user_id = hass.data[DATA_CONFIG_STORE].data.hassio_user
|
||||
assert hassio_user_id is not None
|
||||
user = await hass.auth.async_get_user(hassio_user_id)
|
||||
assert user is not None
|
||||
|
||||
auth_view = HassIOBaseAuth(hass, user)
|
||||
request = MagicMock()
|
||||
request.transport.get_extra_info.return_value = peername
|
||||
request.__getitem__.return_value = user
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hassio.auth.is_supervisor_unix_socket_request",
|
||||
return_value=unix_socket,
|
||||
),
|
||||
expectation,
|
||||
):
|
||||
auth_view._check_access(request)
|
||||
|
||||
|
||||
async def test_password_no_user(hassio_client_supervisor: TestClient) -> None:
|
||||
"""Test changing password for invalid user."""
|
||||
resp = await hassio_client_supervisor.post(
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Tests for the Hive integration __init__."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from homeassistant.components.hive.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
_ENTRY_DATA = {
|
||||
CONF_USERNAME: "user@example.com",
|
||||
CONF_PASSWORD: "password",
|
||||
"tokens": {
|
||||
"AuthenticationResult": {
|
||||
"AccessToken": "mock-access-token",
|
||||
"RefreshToken": "mock-refresh-token",
|
||||
},
|
||||
"ChallengeName": "SUCCESS",
|
||||
},
|
||||
}
|
||||
|
||||
_HUB_BASE = {
|
||||
"device_id": "hive-hub-id",
|
||||
"hiveName": "Hive Hub",
|
||||
"deviceData": {
|
||||
"model": "Hub",
|
||||
"version": "1.2.3",
|
||||
"manufacturer": "Hive",
|
||||
"online": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _make_mock_hive(hub_extra: dict) -> MagicMock:
|
||||
"""Return a mocked Hive instance whose startSession returns a minimal devices dict."""
|
||||
hub_data = {**_HUB_BASE, **hub_extra}
|
||||
mock_hive = MagicMock()
|
||||
mock_hive.session.startSession = AsyncMock(return_value={"parent": [hub_data]})
|
||||
return mock_hive
|
||||
|
||||
|
||||
async def test_hub_device_registers_mac_connection(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Hub device entry includes a MAC connection when macAddress is present."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
mock_hive = _make_mock_hive({"macAddress": "00:1C:2B:1C:2E:68"})
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hive.Hive",
|
||||
return_value=mock_hive,
|
||||
),
|
||||
patch("homeassistant.components.hive.aiohttp_client.async_get_clientsession"),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, "hive-hub-id")})
|
||||
assert device is not None
|
||||
assert (dr.CONNECTION_NETWORK_MAC, "00:1c:2b:1c:2e:68") in device.connections
|
||||
|
||||
|
||||
async def test_hub_device_no_mac_connection_when_absent(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Hub device entry has no MAC connection when macAddress is absent."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
mock_hive = _make_mock_hive({}) # no macAddress key
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hive.Hive",
|
||||
return_value=mock_hive,
|
||||
),
|
||||
patch("homeassistant.components.hive.aiohttp_client.async_get_clientsession"),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, "hive-hub-id")})
|
||||
assert device is not None
|
||||
assert not any(
|
||||
conn_type == dr.CONNECTION_NETWORK_MAC for conn_type, _ in device.connections
|
||||
)
|
||||
@@ -64,6 +64,9 @@ async def test_humidifier_conditions_gated_by_labs_flag(
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_HUMIDITY_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
@@ -72,6 +75,8 @@ async def test_humidifier_conditions_gated_by_labs_flag(
|
||||
("humidifier.is_on", {}, True, True),
|
||||
("humidifier.is_drying", {}, True, True),
|
||||
("humidifier.is_humidifying", {}, True, True),
|
||||
("humidifier.is_mode", {"mode": ["normal"]}, True, True),
|
||||
("humidifier.is_target_humidity", _HUMIDITY_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_humidifier_condition_options_validation(
|
||||
|
||||
@@ -68,6 +68,7 @@ async def test_humidifier_triggers_gated_by_labs_flag(
|
||||
("humidifier.started_humidifying", {}, True, True),
|
||||
("humidifier.turned_on", {}, True, True),
|
||||
("humidifier.turned_off", {}, True, True),
|
||||
("humidifier.mode_changed", {"mode": ["normal"]}, True, True),
|
||||
],
|
||||
)
|
||||
async def test_humidifier_trigger_options_validation(
|
||||
|
||||
@@ -56,12 +56,16 @@ async def test_illuminance_conditions_gated_by_labs_flag(
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_ILLUMINANCE_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("illuminance.is_detected", {}, True, True),
|
||||
("illuminance.is_not_detected", {}, True, True),
|
||||
("illuminance.is_value", _ILLUMINANCE_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_illuminance_condition_options_validation(
|
||||
|
||||
@@ -58,12 +58,18 @@ async def test_illuminance_triggers_gated_by_labs_flag(
|
||||
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
|
||||
|
||||
|
||||
_CHANGED_THRESHOLD = {"threshold": {"type": "any"}}
|
||||
_CROSSED_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("illuminance.detected", {}, True, True),
|
||||
("illuminance.cleared", {}, True, True),
|
||||
("illuminance.changed", _CHANGED_THRESHOLD, False, False),
|
||||
("illuminance.crossed_threshold", _CROSSED_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_illuminance_trigger_options_validation(
|
||||
|
||||
@@ -30,6 +30,8 @@ MOCK_CONFIG = {
|
||||
"folder": "INBOX",
|
||||
"search": "UnSeen UnDeleted",
|
||||
"event_message_data": ["text", "headers"],
|
||||
"ssl_cipher_list": "python_default",
|
||||
"verify_ssl": True,
|
||||
}
|
||||
|
||||
MOCK_OPTIONS = {
|
||||
@@ -301,7 +303,7 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
async def test_options_form(hass: HomeAssistant) -> None:
|
||||
"""Test we show the options form."""
|
||||
"""Test the options form."""
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -381,7 +383,7 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("advanced_options", "assert_result"),
|
||||
("test_options", "assert_result"),
|
||||
[
|
||||
({"max_message_size": 8192}, FlowResultType.CREATE_ENTRY),
|
||||
({"max_message_size": 1024}, FlowResultType.FORM),
|
||||
@@ -407,12 +409,12 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None:
|
||||
"enable_push_false",
|
||||
],
|
||||
)
|
||||
async def test_advanced_options_form(
|
||||
async def test_options_flow_when_connection_fails(
|
||||
hass: HomeAssistant,
|
||||
advanced_options: dict[str, str],
|
||||
test_options: dict[str, str],
|
||||
assert_result: FlowResultType,
|
||||
) -> None:
|
||||
"""Test we show the advanced options."""
|
||||
"""Test the options flow when the connection fails."""
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -420,14 +422,14 @@ async def test_advanced_options_form(
|
||||
|
||||
result = await hass.config_entries.options.async_init(
|
||||
entry.entry_id,
|
||||
context={"source": config_entries.SOURCE_USER, "show_advanced_options": True},
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
new_config = MOCK_OPTIONS.copy()
|
||||
new_config.update(advanced_options)
|
||||
new_config.update(test_options)
|
||||
|
||||
try:
|
||||
with patch(
|
||||
@@ -462,7 +464,7 @@ async def test_config_flow_with_cipherlist_and_ssl_verify(
|
||||
config["verify_ssl"] = verify_ssl
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER, "show_advanced_options": True},
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
@@ -494,7 +496,7 @@ async def test_config_flow_with_event_message_data(
|
||||
config["event_message_data"] = event_message_data
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER, "show_advanced_options": False},
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
@@ -517,16 +519,14 @@ async def test_config_flow_with_event_message_data(
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_config_flow_from_with_advanced_settings(
|
||||
async def test_cipher_settings_in_config_flow(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test if advanced settings show correctly."""
|
||||
"""Test cipher settings in config flow."""
|
||||
config = MOCK_CONFIG.copy()
|
||||
config["ssl_cipher_list"] = "python_default"
|
||||
config["verify_ssl"] = True
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER, "show_advanced_options": True},
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
@@ -72,6 +72,8 @@ async def test_entry_diagnostics(
|
||||
],
|
||||
"search": "UnSeen UnDeleted",
|
||||
"custom_event_data_template": "{{ 4 * 4 }}",
|
||||
"ssl_cipher_list": "python_default",
|
||||
"verify_ssl": True,
|
||||
}
|
||||
expected_event_data = {
|
||||
"date": "2023-03-24T13:52:00+01:00",
|
||||
|
||||
@@ -47,12 +47,16 @@ async def test_light_conditions_gated_by_labs_flag(
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_BRIGHTNESS_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("light.is_off", {}, True, True),
|
||||
("light.is_on", {}, True, True),
|
||||
("light.is_brightness", _BRIGHTNESS_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_light_condition_options_validation(
|
||||
|
||||
@@ -53,12 +53,25 @@ async def test_light_triggers_gated_by_labs_flag(
|
||||
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
|
||||
|
||||
|
||||
_CHANGED_THRESHOLD = {"threshold": {"type": "any"}}
|
||||
_BRIGHTNESS_CROSSED_THRESHOLD = {
|
||||
"threshold": {"type": "above", "value": {"number": 50}}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("light.turned_on", {}, True, True),
|
||||
("light.turned_off", {}, True, True),
|
||||
("light.brightness_changed", _CHANGED_THRESHOLD, False, False),
|
||||
(
|
||||
"light.brightness_crossed_threshold",
|
||||
_BRIGHTNESS_CROSSED_THRESHOLD,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_light_trigger_options_validation(
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.met.const import (
|
||||
CONF_TRACK_HOME,
|
||||
DOMAIN,
|
||||
HOME_LOCATION_NAME,
|
||||
)
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -18,6 +14,7 @@ async def init_integration(
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Met integration in Home Assistant."""
|
||||
entry_data = {
|
||||
CONF_NAME: "test",
|
||||
CONF_LATITUDE: 0,
|
||||
CONF_LONGITUDE: 1.0,
|
||||
CONF_ELEVATION: 1.0,
|
||||
@@ -26,12 +23,7 @@ async def init_integration(
|
||||
if track_home:
|
||||
entry_data = {CONF_TRACK_HOME: True}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=entry_data,
|
||||
title=HOME_LOCATION_NAME if track_home else "",
|
||||
minor_version=2,
|
||||
)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
|
||||
with patch(
|
||||
"homeassistant.components.met.coordinator.metno.MetWeatherData.fetching_data",
|
||||
return_value=True,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
'elevation': 1.0,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'name': 'test',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core_config import async_process_ha_core_config
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -56,6 +56,7 @@ async def test_flow_with_home_location(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
default_data = result["data_schema"]({})
|
||||
assert default_data["name"] == HOME_LOCATION_NAME
|
||||
assert default_data["latitude"] == 1
|
||||
assert default_data["longitude"] == 2
|
||||
assert default_data["elevation"] == 3
|
||||
@@ -64,6 +65,7 @@ async def test_flow_with_home_location(hass: HomeAssistant) -> None:
|
||||
async def test_create_entry(hass: HomeAssistant) -> None:
|
||||
"""Test create entry from user input."""
|
||||
test_data = {
|
||||
"name": "home",
|
||||
CONF_LONGITUDE: 0,
|
||||
CONF_LATITUDE: 0,
|
||||
CONF_ELEVATION: 0,
|
||||
@@ -74,7 +76,7 @@ async def test_create_entry(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ""
|
||||
assert result["title"] == "home"
|
||||
assert result["data"] == test_data
|
||||
|
||||
|
||||
@@ -86,11 +88,12 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None:
|
||||
"""
|
||||
first_entry = MockConfigEntry(
|
||||
domain="met",
|
||||
data={CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_ELEVATION: 0},
|
||||
data={"name": "home", CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_ELEVATION: 0},
|
||||
)
|
||||
first_entry.add_to_hass(hass)
|
||||
|
||||
test_data = {
|
||||
"name": "home",
|
||||
CONF_LONGITUDE: 0,
|
||||
CONF_LATITUDE: 0,
|
||||
CONF_ELEVATION: 0,
|
||||
@@ -101,7 +104,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "already_configured"
|
||||
assert result["errors"]["name"] == "already_configured"
|
||||
|
||||
|
||||
async def test_onboarding_step(hass: HomeAssistant) -> None:
|
||||
@@ -119,7 +122,7 @@ async def test_onboarding_step(hass: HomeAssistant) -> None:
|
||||
("latitude", "longitude"), [(52.3731339, 4.8903147), (0.0, 0.0)]
|
||||
)
|
||||
async def test_onboarding_step_abort_no_home(
|
||||
hass: HomeAssistant, latitude: float, longitude: float
|
||||
hass: HomeAssistant, latitude, longitude
|
||||
) -> None:
|
||||
"""Test entry not created when default step fails."""
|
||||
await async_process_ha_core_config(
|
||||
@@ -142,6 +145,7 @@ async def test_onboarding_step_abort_no_home(
|
||||
async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
"""Test show options form."""
|
||||
update_data = {
|
||||
CONF_NAME: "test",
|
||||
CONF_LATITUDE: 12,
|
||||
CONF_LONGITUDE: 23,
|
||||
CONF_ELEVATION: 456,
|
||||
@@ -164,7 +168,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ""
|
||||
assert result["title"] == "Mock Title"
|
||||
assert result["data"] == update_data
|
||||
weatherdatamock.assert_called_with(
|
||||
{"lat": "12", "lon": "23", "msl": "456"}, ANY, api_url=ANY
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Test the Met integration init."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.met.const import (
|
||||
@@ -9,16 +7,13 @@ from homeassistant.components.met.const import (
|
||||
DEFAULT_HOME_LONGITUDE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core_config import async_process_ha_core_config
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant) -> None:
|
||||
"""Test successful unload of entry."""
|
||||
@@ -61,7 +56,7 @@ async def test_removing_incorrect_devices(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_weather: MagicMock,
|
||||
mock_weather,
|
||||
) -> None:
|
||||
"""Test we remove incorrect devices."""
|
||||
entry = await init_integration(hass)
|
||||
@@ -82,36 +77,3 @@ async def test_removing_incorrect_devices(
|
||||
assert not device_registry.async_get_device(identifiers={(DOMAIN,)})
|
||||
assert device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
|
||||
assert "Removing improper device Forecast_legacy" in caplog.text
|
||||
|
||||
|
||||
async def test_migrate_name_to_title(
|
||||
hass: HomeAssistant,
|
||||
mock_weather: MagicMock,
|
||||
) -> None:
|
||||
"""Test legacy stored names migrate to the config entry title."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
source=SOURCE_USER,
|
||||
data={
|
||||
CONF_NAME: "Somewhere",
|
||||
CONF_LATITUDE: 10,
|
||||
CONF_LONGITUDE: 20,
|
||||
CONF_ELEVATION: 0,
|
||||
},
|
||||
title="",
|
||||
minor_version=1,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.version == 1
|
||||
assert entry.minor_version == 2
|
||||
assert entry.title == "Somewhere"
|
||||
assert entry.data == {
|
||||
CONF_LATITUDE: 10,
|
||||
CONF_LONGITUDE: 20,
|
||||
CONF_ELEVATION: 0,
|
||||
}
|
||||
assert hass.states.async_entity_ids("weather") == ["weather.forecast_somewhere"]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Test Met weather entity."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.met import DOMAIN
|
||||
from homeassistant.components.weather import (
|
||||
@@ -22,9 +20,7 @@ from . import init_integration
|
||||
|
||||
|
||||
async def test_new_config_entry(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_weather: MagicMock,
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_weather
|
||||
) -> None:
|
||||
"""Test the expected entities are created."""
|
||||
await hass.config_entries.flow.async_init("met", context={"source": "onboarding"})
|
||||
@@ -36,9 +32,7 @@ async def test_new_config_entry(
|
||||
|
||||
|
||||
async def test_legacy_config_entry(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_weather: MagicMock,
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_weather
|
||||
) -> None:
|
||||
"""Test the expected entities are created."""
|
||||
entity_registry.async_get_or_create(
|
||||
@@ -54,7 +48,7 @@ async def test_legacy_config_entry(
|
||||
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 1
|
||||
|
||||
|
||||
async def test_weather(hass: HomeAssistant, mock_weather: MagicMock) -> None:
|
||||
async def test_weather(hass: HomeAssistant, mock_weather) -> None:
|
||||
"""Test states of the weather."""
|
||||
|
||||
await init_integration(hass)
|
||||
@@ -73,7 +67,7 @@ async def test_weather(hass: HomeAssistant, mock_weather: MagicMock) -> None:
|
||||
assert state.attributes[ATTR_WEATHER_UV_INDEX] == 1.1
|
||||
|
||||
|
||||
async def test_tracking_home(hass: HomeAssistant, mock_weather: MagicMock) -> None:
|
||||
async def test_tracking_home(hass: HomeAssistant, mock_weather) -> None:
|
||||
"""Test we track home."""
|
||||
await hass.config_entries.flow.async_init("met", context={"source": "onboarding"})
|
||||
await hass.async_block_till_done()
|
||||
@@ -97,13 +91,13 @@ async def test_tracking_home(hass: HomeAssistant, mock_weather: MagicMock) -> No
|
||||
assert len(hass.states.async_entity_ids("weather")) == 0
|
||||
|
||||
|
||||
async def test_not_tracking_home(hass: HomeAssistant, mock_weather: MagicMock) -> None:
|
||||
async def test_not_tracking_home(hass: HomeAssistant, mock_weather) -> None:
|
||||
"""Test when we not track home."""
|
||||
|
||||
await hass.config_entries.flow.async_init(
|
||||
"met",
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={"latitude": 10, "longitude": 20, "elevation": 0},
|
||||
data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_entity_ids("weather")) == 1
|
||||
@@ -122,9 +116,7 @@ async def test_not_tracking_home(hass: HomeAssistant, mock_weather: MagicMock) -
|
||||
|
||||
|
||||
async def test_remove_hourly_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_weather: MagicMock,
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_weather
|
||||
) -> None:
|
||||
"""Test removing the hourly entity."""
|
||||
|
||||
@@ -143,8 +135,8 @@ async def test_remove_hourly_entity(
|
||||
await hass.config_entries.flow.async_init(
|
||||
"met",
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={"latitude": 10, "longitude": 20, "elevation": 0},
|
||||
data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.async_entity_ids("weather") == ["weather.forecast_met_no"]
|
||||
assert list(entity_registry.entities.keys()) == ["weather.forecast_met_no"]
|
||||
assert hass.states.async_entity_ids("weather") == ["weather.forecast_somewhere"]
|
||||
assert list(entity_registry.entities.keys()) == ["weather.forecast_somewhere"]
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components import mill
|
||||
from homeassistant.components.mill.coordinator import MillDataUpdateCoordinator
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -156,7 +157,8 @@ async def test_unload_entry(recorder_mock: Recorder, hass: HomeAssistant) -> Non
|
||||
):
|
||||
assert await async_setup_component(hass, "mill", {})
|
||||
|
||||
assert isinstance(entry.runtime_data, MillDataUpdateCoordinator)
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
assert unload_entry.call_count == 3
|
||||
assert entry.entry_id not in hass.data[mill.DOMAIN]
|
||||
|
||||
@@ -56,12 +56,16 @@ async def test_moisture_conditions_gated_by_labs_flag(
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_MOISTURE_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("moisture.is_detected", {}, True, True),
|
||||
("moisture.is_not_detected", {}, True, True),
|
||||
("moisture.is_value", _MOISTURE_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_moisture_condition_options_validation(
|
||||
|
||||
@@ -58,12 +58,18 @@ async def test_moisture_triggers_gated_by_labs_flag(
|
||||
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
|
||||
|
||||
|
||||
_CHANGED_THRESHOLD = {"threshold": {"type": "any"}}
|
||||
_CROSSED_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("moisture.detected", {}, True, True),
|
||||
("moisture.cleared", {}, True, True),
|
||||
("moisture.changed", _CHANGED_THRESHOLD, False, False),
|
||||
("moisture.crossed_threshold", _CROSSED_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_moisture_trigger_options_validation(
|
||||
|
||||
@@ -88,9 +88,7 @@ async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None:
|
||||
mid = 100
|
||||
rc = 0
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client:
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client:
|
||||
mqtt_client = mock_client.return_value
|
||||
mqtt_client.connect = MagicMock(
|
||||
return_value=0,
|
||||
@@ -1305,9 +1303,7 @@ async def test_publish_error(
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# simulate an Out of memory error
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client:
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client:
|
||||
mock_client().connect = lambda **kwargs: 1
|
||||
mock_client().publish().rc = 1
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
@@ -1404,9 +1400,7 @@ async def test_setup_mqtt_client_clean_session_and_protocol(
|
||||
clean_session: bool | None,
|
||||
) -> None:
|
||||
"""Test MQTT client clean_session and protocol setup."""
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client:
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client:
|
||||
await mqtt_mock_entry()
|
||||
|
||||
# check if clean_session was correctly
|
||||
@@ -1470,9 +1464,7 @@ async def test_handle_mqtt_timeout_on_callback(
|
||||
mid = 102
|
||||
rc = 0
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client:
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client:
|
||||
|
||||
def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]:
|
||||
# Handle ACK for subscribe normally
|
||||
@@ -1539,9 +1531,7 @@ async def test_setup_raises_config_entry_not_ready_if_no_connect_broker(
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client:
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client:
|
||||
mock_client().connect = MagicMock(side_effect=exception)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -1576,9 +1566,7 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure(
|
||||
def mock_tls_insecure_set(insecure_param) -> None:
|
||||
insecure_check["insecure"] = insecure_param
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client:
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client:
|
||||
mock_client().tls_set = mock_tls_set
|
||||
mock_client().tls_insecure_set = mock_tls_insecure_set
|
||||
await mqtt_mock_entry()
|
||||
@@ -1618,7 +1606,7 @@ async def test_client_id_is_set(
|
||||
) -> None:
|
||||
"""Test setup defaults for tls."""
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
"homeassistant.components.mqtt.client.AsyncMQTTClient"
|
||||
) as async_client_mock:
|
||||
await mqtt_mock_entry()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -254,9 +254,7 @@ def mock_try_connection_success() -> Generator[MqttMockPahoClient]:
|
||||
mock_client().on_unsubscribe(mock_client, 0, mid, [MockMqttReasonCode()], None)
|
||||
return (0, mid)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client:
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client:
|
||||
mock_client().loop_start = loop_start
|
||||
mock_client().subscribe = _subscribe
|
||||
mock_client().unsubscribe = _unsubscribe
|
||||
@@ -270,9 +268,7 @@ def mock_try_connection_time_out() -> Generator[MagicMock]:
|
||||
|
||||
# Patch prevent waiting 5 sec for a timeout
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client,
|
||||
patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client,
|
||||
patch("homeassistant.components.mqtt.config_flow.MQTT_TIMEOUT", 0),
|
||||
):
|
||||
mock_client().loop_start = lambda *args: 1
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any, TypedDict
|
||||
from unittest.mock import ANY, MagicMock, Mock, mock_open, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from paho.mqtt.client import MQTTMessage
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -700,11 +701,6 @@ async def test_receiving_message_with_non_utf8_topic_gets_logged(
|
||||
await mqtt_mock_entry()
|
||||
await mqtt.async_subscribe(hass, "test-topic", record_calls)
|
||||
|
||||
# Local import to avoid processing MQTT modules when running a testcase
|
||||
# which does not use MQTT.
|
||||
|
||||
from paho.mqtt.client import MQTTMessage # noqa: PLC0415
|
||||
|
||||
from homeassistant.components.mqtt.models import MqttData # noqa: PLC0415
|
||||
|
||||
msg = MQTTMessage(topic=b"tasmota/discovery/18FE34E0B760\xcc\x02")
|
||||
@@ -1910,7 +1906,7 @@ async def test_link_config_entry(
|
||||
assert _check_entities() == 2
|
||||
|
||||
# reload entry and assert again
|
||||
with patch("homeassistant.components.mqtt.async_client.AsyncMQTTClient"):
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient"):
|
||||
await hass.config_entries.async_reload(mqtt_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -39,11 +39,15 @@ async def test_todo_conditions_gated_by_labs_flag(
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_TODO_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 5}}}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("todo.all_completed", {}, True, True),
|
||||
("todo.incomplete", _TODO_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_todo_condition_options_validation(
|
||||
|
||||
@@ -36,6 +36,10 @@ from homeassistant.setup import async_setup_component
|
||||
from . import MockTodoListEntity, create_mock_platform
|
||||
|
||||
from tests.common import async_mock_service, mock_device_registry
|
||||
from tests.components.common import (
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
)
|
||||
|
||||
TODO_ENTITY_ID1 = "todo.list_one"
|
||||
TODO_ENTITY_ID2 = "todo.list_two"
|
||||
@@ -122,6 +126,47 @@ def service_calls(hass: HomeAssistant) -> list[ServiceCall]:
|
||||
return async_mock_service(hass, "test", "item_added")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
"todo.item_added",
|
||||
"todo.item_completed",
|
||||
"todo.item_removed",
|
||||
],
|
||||
)
|
||||
async def test_todo_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the todo 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_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("todo.item_added", None, False, False),
|
||||
("todo.item_completed", None, False, False),
|
||||
("todo.item_removed", None, False, False),
|
||||
],
|
||||
)
|
||||
async def test_todo_trigger_options_validation(
|
||||
hass: HomeAssistant,
|
||||
trigger_key: str,
|
||||
base_options: dict[str, Any] | None,
|
||||
supports_behavior: bool,
|
||||
supports_duration: bool,
|
||||
) -> None:
|
||||
"""Test that todo triggers support the expected options."""
|
||||
await assert_trigger_options_supported(
|
||||
hass,
|
||||
trigger_key,
|
||||
base_options,
|
||||
supports_behavior=supports_behavior,
|
||||
supports_duration=supports_duration,
|
||||
)
|
||||
|
||||
|
||||
def _assert_service_calls(
|
||||
service_calls: list[ServiceCall], expected_calls: list[dict[str, Any]]
|
||||
) -> None:
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
{
|
||||
"name": "Security Camera",
|
||||
"category": "sp",
|
||||
"product_id": "knkaf1d0dytgyhix",
|
||||
"product_name": "Security Camera",
|
||||
"online": true,
|
||||
"sub": false,
|
||||
"time_zone": "+02:00",
|
||||
"active_time": "2025-09-11T13:22:03+00:00",
|
||||
"create_time": "2025-09-11T13:22:03+00:00",
|
||||
"update_time": "2025-09-11T13:22:03+00:00",
|
||||
"function": {
|
||||
"basic_indicator": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"basic_flip": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"basic_osd": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"basic_private": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"motion_sensitivity": {
|
||||
"type": "Enum",
|
||||
"value": "{\"range\":[\"0\",\"1\",\"2\"]}"
|
||||
},
|
||||
"basic_wdr": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"basic_nightvision": {
|
||||
"type": "Enum",
|
||||
"value": "{\"range\":[\"0\",\"1\",\"2\"]}"
|
||||
},
|
||||
"sd_format": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"motion_record": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"ipc_auto_siren": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"ipc_sharp": {
|
||||
"type": "Integer",
|
||||
"value": "{\"unit\":\"%\",\"min\":0,\"max\":100,\"scale\":0,\"step\":1}"
|
||||
},
|
||||
"motion_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"record_switch1": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"decibel_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"decibel_sensitivity": {
|
||||
"type": "Enum",
|
||||
"value": "{\"range\":[\"0\",\"1\"]}"
|
||||
},
|
||||
"record_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"record_mode": {
|
||||
"type": "Enum",
|
||||
"value": "{\"range\":[\"1\",\"2\"]}"
|
||||
},
|
||||
"siren_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"device_restart": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"motion_area_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}"
|
||||
},
|
||||
"motion_area": {
|
||||
"type": "String",
|
||||
"value": "{\"maxlen\":255}"
|
||||
},
|
||||
"ipc_contrast": {
|
||||
"type": "Integer",
|
||||
"value": "{\"unit\":\"%\",\"min\":0,\"max\":100,\"scale\":0,\"step\":1}"
|
||||
},
|
||||
"ipc_bright": {
|
||||
"type": "Integer",
|
||||
"value": "{\"unit\":\"%\",\"min\":0,\"max\":100,\"scale\":0,\"step\":1}"
|
||||
}
|
||||
},
|
||||
"local_strategy": {},
|
||||
"status_range": {
|
||||
"basic_indicator": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"basic_flip": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"basic_osd": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"basic_private": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"motion_sensitivity": {
|
||||
"type": "Enum",
|
||||
"value": "{\"range\":[\"0\",\"1\",\"2\"]}",
|
||||
"report_type": null
|
||||
},
|
||||
"basic_wdr": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"basic_nightvision": {
|
||||
"type": "Enum",
|
||||
"value": "{\"range\":[\"0\",\"1\",\"2\"]}",
|
||||
"report_type": null
|
||||
},
|
||||
"sd_storge": {
|
||||
"type": "String",
|
||||
"value": "{\"maxlen\":255}",
|
||||
"report_type": null
|
||||
},
|
||||
"sd_status": {
|
||||
"type": "Integer",
|
||||
"value": "{\"min\":1,\"max\":5,\"scale\":0,\"step\":1}",
|
||||
"report_type": null
|
||||
},
|
||||
"sd_format": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"motion_record": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"movement_detect_pic": {
|
||||
"type": "Raw",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"sd_format_state": {
|
||||
"type": "Integer",
|
||||
"value": "{\"min\":-20000,\"max\":200000,\"scale\":0,\"step\":1}",
|
||||
"report_type": null
|
||||
},
|
||||
"ipc_auto_siren": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"ipc_sharp": {
|
||||
"type": "Integer",
|
||||
"value": "{\"unit\":\"%\",\"min\":0,\"max\":100,\"scale\":0,\"step\":1}",
|
||||
"report_type": null
|
||||
},
|
||||
"motion_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"record_switch1": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"decibel_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"decibel_sensitivity": {
|
||||
"type": "Enum",
|
||||
"value": "{\"range\":[\"0\",\"1\"]}",
|
||||
"report_type": null
|
||||
},
|
||||
"record_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"record_mode": {
|
||||
"type": "Enum",
|
||||
"value": "{\"range\":[\"1\",\"2\"]}",
|
||||
"report_type": null
|
||||
},
|
||||
"siren_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"device_restart": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"motion_area_switch": {
|
||||
"type": "Boolean",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"motion_area": {
|
||||
"type": "String",
|
||||
"value": "{\"maxlen\":255}",
|
||||
"report_type": null
|
||||
},
|
||||
"alarm_message": {
|
||||
"type": "String",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
},
|
||||
"ipc_contrast": {
|
||||
"type": "Integer",
|
||||
"value": "{\"unit\":\"%\",\"min\":0,\"max\":100,\"scale\":0,\"step\":1}",
|
||||
"report_type": null
|
||||
},
|
||||
"ipc_bright": {
|
||||
"type": "Integer",
|
||||
"value": "{\"unit\":\"%\",\"min\":0,\"max\":100,\"scale\":0,\"step\":1}",
|
||||
"report_type": null
|
||||
},
|
||||
"initiative_message": {
|
||||
"type": "Raw",
|
||||
"value": "{}",
|
||||
"report_type": null
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"basic_indicator": true,
|
||||
"basic_flip": false,
|
||||
"basic_osd": true,
|
||||
"basic_private": false,
|
||||
"motion_sensitivity": "1",
|
||||
"basic_wdr": false,
|
||||
"basic_nightvision": "1",
|
||||
"sd_storge": "896|896|0",
|
||||
"sd_status": 5,
|
||||
"sd_format": false,
|
||||
"motion_record": false,
|
||||
"movement_detect_pic": "**REDACTED**",
|
||||
"sd_format_state": 0,
|
||||
"ipc_auto_siren": false,
|
||||
"ipc_sharp": 50,
|
||||
"motion_switch": true,
|
||||
"record_switch1": false,
|
||||
"decibel_switch": false,
|
||||
"decibel_sensitivity": "0",
|
||||
"record_switch": true,
|
||||
"record_mode": "1",
|
||||
"siren_switch": false,
|
||||
"device_restart": true,
|
||||
"motion_area_switch": false,
|
||||
"motion_area": "{\"num\":1,\"region0\":{\"x\":0,\"y\":0,\"xlen\":100,\"ylen\":100}}",
|
||||
"alarm_message": "**REDACTED**",
|
||||
"ipc_contrast": 50,
|
||||
"ipc_bright": 50,
|
||||
"initiative_message": ""
|
||||
},
|
||||
"set_up": true,
|
||||
"support_local": false,
|
||||
"quirk": null,
|
||||
"warnings": null
|
||||
}
|
||||
@@ -441,3 +441,59 @@
|
||||
'state': 'idle',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[camera.security_camera-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': 'camera',
|
||||
'entity_category': None,
|
||||
'entity_id': 'camera.security_camera',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <CameraEntityFeature: 2>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkps',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[camera.security_camera-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'access_token': '1',
|
||||
'brand': 'Tuya',
|
||||
'entity_picture': '/api/camera_proxy/camera.security_camera?token=1',
|
||||
'friendly_name': 'Security Camera',
|
||||
'model_name': 'Security Camera',
|
||||
'motion_detection': True,
|
||||
'supported_features': <CameraEntityFeature: 2>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'camera.security_camera',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'recording',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -806,3 +806,63 @@
|
||||
'state': '2023-11-01T12:14:15.000+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[event.security_camera_doorbell_message-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'triggered',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.security_camera_doorbell_message',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Doorbell message',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <EventDeviceClass.DOORBELL: 'doorbell'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Doorbell message',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'doorbell_message',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsalarm_message',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[event.security_camera_doorbell_message-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'doorbell',
|
||||
'event_type': 'triggered',
|
||||
'event_types': list([
|
||||
'triggered',
|
||||
]),
|
||||
'friendly_name': 'Security Camera Doorbell message',
|
||||
'message': '',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.security_camera_doorbell_message',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2023-11-01T12:14:15.000+00:00',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -8865,6 +8865,37 @@
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_device_registry[xihygtyd0d1faknkps]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'tuya',
|
||||
'xihygtyd0d1faknkps',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Tuya',
|
||||
'model': 'Security Camera',
|
||||
'model_id': 'knkaf1d0dytgyhix',
|
||||
'name': 'Security Camera',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_device_registry[xms6qowipdvjnkdgqdt]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
|
||||
@@ -3245,6 +3245,65 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[light.security_camera_indicator_light-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'light.security_camera_indicator_light',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Indicator light',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Indicator light',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsbasic_indicator',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[light.security_camera_indicator_light-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'color_mode': <ColorMode.ONOFF: 'onoff'>,
|
||||
'friendly_name': 'Security Camera Indicator light',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.security_camera_indicator_light',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[light.shop_light_5-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -5025,6 +5025,246 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_camera_motion_detection_sensitivity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.security_camera_motion_detection_sensitivity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Motion detection sensitivity',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Motion detection sensitivity',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'motion_sensitivity',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsmotion_sensitivity',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_camera_motion_detection_sensitivity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Motion detection sensitivity',
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.security_camera_motion_detection_sensitivity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_camera_night_vision-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.security_camera_night_vision',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Night vision',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Night vision',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'basic_nightvision',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsbasic_nightvision',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_camera_night_vision-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Night vision',
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.security_camera_night_vision',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_camera_record_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'1',
|
||||
'2',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.security_camera_record_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Record mode',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Record mode',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'record_mode',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsrecord_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_camera_record_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Record mode',
|
||||
'options': list([
|
||||
'1',
|
||||
'2',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.security_camera_record_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_camera_sound_detection_sensitivity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.security_camera_sound_detection_sensitivity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Sound detection sensitivity',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Sound detection sensitivity',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'decibel_sensitivity',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsdecibel_sensitivity',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_camera_sound_detection_sensitivity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Sound detection sensitivity',
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.security_camera_sound_detection_sensitivity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.security_light_indicator_light_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -152,6 +152,57 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[siren.security_camera-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': 'siren',
|
||||
'entity_category': None,
|
||||
'entity_id': 'siren.security_camera',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <SirenEntityFeature: 3>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpssiren_switch',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[siren.security_camera-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera',
|
||||
'supported_features': <SirenEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'siren.security_camera',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[siren.siren_veranda-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -9125,6 +9125,406 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_flip-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.security_camera_flip',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Flip',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Flip',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'flip',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsbasic_flip',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_flip-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Flip',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.security_camera_flip',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_motion_alarm-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.security_camera_motion_alarm',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Motion alarm',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Motion alarm',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'motion_alarm',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsmotion_switch',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_motion_alarm-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Motion alarm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.security_camera_motion_alarm',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_motion_recording-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.security_camera_motion_recording',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Motion recording',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Motion recording',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'motion_recording',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsmotion_record',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_motion_recording-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Motion recording',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.security_camera_motion_recording',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_privacy_mode-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.security_camera_privacy_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Privacy mode',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Privacy mode',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'privacy_mode',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsbasic_private',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_privacy_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Privacy mode',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.security_camera_privacy_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_sound_detection-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.security_camera_sound_detection',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Sound detection',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Sound detection',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'sound_detection',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsdecibel_switch',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_sound_detection-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Sound detection',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.security_camera_sound_detection',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_time_watermark-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.security_camera_time_watermark',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Time watermark',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Time watermark',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'time_watermark',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsbasic_osd',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_time_watermark-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Time watermark',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.security_camera_time_watermark',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_video_recording-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.security_camera_video_recording',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Video recording',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Video recording',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'video_recording',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsrecord_switch',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_video_recording-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Video recording',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.security_camera_video_recording',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_wide_dynamic_range-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.security_camera_wide_dynamic_range',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Wide dynamic range',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wide dynamic range',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wide_dynamic_range',
|
||||
'unique_id': 'tuya.xihygtyd0d1faknkpsbasic_wdr',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_camera_wide_dynamic_range-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Camera Wide dynamic range',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.security_camera_wide_dynamic_range',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.security_light_child_lock-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -72,12 +72,27 @@ async def test_water_heater_conditions_gated_by_labs_flag(
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
_TEMPERATURE_THRESHOLD = {
|
||||
"threshold": {
|
||||
"type": "above",
|
||||
"value": {"number": 20, "unit_of_measurement": UnitOfTemperature.CELSIUS},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("water_heater.is_off", {}, True, True),
|
||||
("water_heater.is_on", {}, True, True),
|
||||
(
|
||||
"water_heater.is_operation_mode",
|
||||
{"operation_mode": [STATE_ECO]},
|
||||
True,
|
||||
True,
|
||||
),
|
||||
("water_heater.is_target_temperature", _TEMPERATURE_THRESHOLD, True, True),
|
||||
],
|
||||
)
|
||||
async def test_water_heater_condition_options_validation(
|
||||
|
||||
@@ -65,12 +65,39 @@ async def test_water_heater_triggers_gated_by_labs_flag(
|
||||
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
|
||||
|
||||
|
||||
_CHANGED_THRESHOLD = {"threshold": {"type": "any"}}
|
||||
_CROSSED_THRESHOLD = {
|
||||
"threshold": {
|
||||
"type": "above",
|
||||
"value": {"number": 20, "unit_of_measurement": UnitOfTemperature.CELSIUS},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("water_heater.turned_off", {}, True, True),
|
||||
("water_heater.turned_on", {}, True, True),
|
||||
(
|
||||
"water_heater.operation_mode_changed",
|
||||
{"operation_mode": [STATE_ECO]},
|
||||
True,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"water_heater.target_temperature_changed",
|
||||
_CHANGED_THRESHOLD,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"water_heater.target_temperature_crossed_threshold",
|
||||
_CROSSED_THRESHOLD,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_water_heater_trigger_options_validation(
|
||||
|
||||
+1
-3
@@ -1057,9 +1057,7 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient]:
|
||||
self.mid = mid
|
||||
self.rc = 0
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
|
||||
) as mock_client:
|
||||
with patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client:
|
||||
# The below use a call_soon for the on_publish/on_subscribe/on_unsubscribe
|
||||
# callbacks to simulate the behavior of the real MQTT client which will
|
||||
# not be synchronous.
|
||||
|
||||
@@ -57,6 +57,7 @@ from homeassistant.helpers.trigger import (
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
EntityTriggerBase,
|
||||
PluggableAction,
|
||||
StatelessEntityTriggerBase,
|
||||
Trigger,
|
||||
TriggerActionRunner,
|
||||
TriggerConfig,
|
||||
@@ -3098,6 +3099,88 @@ async def test_make_entity_origin_state_trigger(
|
||||
assert not trig.is_valid_state(from_state)
|
||||
|
||||
|
||||
class _ActivatedTrigger(StatelessEntityTriggerBase):
|
||||
"""Test trigger leaf for StatelessEntityTriggerBase."""
|
||||
|
||||
_domain_specs = {"test": DomainSpec()}
|
||||
|
||||
|
||||
async def _arm_activated_trigger(
|
||||
hass: HomeAssistant,
|
||||
entity_ids: list[str],
|
||||
calls: list[dict[str, Any]],
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Set up _ActivatedTrigger via async_initialize_triggers."""
|
||||
|
||||
async def async_get_triggers(
|
||||
hass: HomeAssistant,
|
||||
) -> dict[str, type[Trigger]]:
|
||||
return {"activated": _ActivatedTrigger}
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", Mock(async_get_triggers=async_get_triggers))
|
||||
|
||||
trigger_config = {
|
||||
CONF_PLATFORM: "test.activated",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: entity_ids},
|
||||
}
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@callback
|
||||
def action(run_variables: dict[str, Any], context: Context | None = None) -> None:
|
||||
calls.append(run_variables["trigger"])
|
||||
|
||||
validated_config = await async_validate_trigger_config(hass, [trigger_config])
|
||||
return await async_initialize_triggers(
|
||||
hass,
|
||||
validated_config,
|
||||
action,
|
||||
domain="test",
|
||||
name="test_activated",
|
||||
log_cb=log.log,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("initial_state", "sequence", "expected_calls"),
|
||||
[
|
||||
(STATE_UNKNOWN, ["2026-05-06T12:00:00+00:00", "2026-05-06T12:00:01+00:00"], 2),
|
||||
(STATE_UNAVAILABLE, ["2026-05-06T12:00:00+00:00"], 0),
|
||||
("2026-05-06T12:00:00+00:00", [STATE_UNAVAILABLE], 0),
|
||||
("2026-05-06T12:00:00+00:00", [STATE_UNKNOWN], 0),
|
||||
("2026-05-06T12:00:00+00:00", ["2026-05-06T12:00:00+00:00"], 0),
|
||||
],
|
||||
)
|
||||
async def test_stateless_entity_trigger(
|
||||
hass: HomeAssistant,
|
||||
initial_state: str,
|
||||
sequence: list[str],
|
||||
expected_calls: int,
|
||||
) -> None:
|
||||
"""Test StatelessEntityTriggerBase end-to-end via a mocked platform.
|
||||
|
||||
StatelessEntityTriggerBase covers entities (buttons, scenes,
|
||||
doorbells, events) that have no meaningful prior state — STATE_UNKNOWN
|
||||
must be a valid origin so the first activation after startup fires,
|
||||
but UNAVAILABLE/UNKNOWN are never valid target states.
|
||||
"""
|
||||
entity_id = "test.bell"
|
||||
hass.states.async_set(entity_id, initial_state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
calls: list[dict[str, Any]] = []
|
||||
unsub = await _arm_activated_trigger(hass, [entity_id], calls)
|
||||
|
||||
for state in sequence:
|
||||
hass.states.async_set(entity_id, state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == expected_calls
|
||||
|
||||
unsub()
|
||||
|
||||
|
||||
class _OffToOnTrigger(EntityTriggerBase):
|
||||
"""Test trigger that fires when state becomes 'on'."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user