Compare commits

...

24 Commits

Author SHA1 Message Date
Petar Petrov 3d36c6f7f8 Allow non-admins to evaluate conditions over the websocket
Drop the admin requirement from the `test_condition` and
`subscribe_condition` websocket commands. Both only evaluate a
condition config and return a read-only result; neither mutates state.

This lets the frontend evaluate dashboard visibility conditions
server-side for everyone, including the non-admin users who view
dashboards. The capability is already available to non-admins through
the `render_template` command, of which a `template` condition is a
subset, so this does not widen the attack surface.
2026-06-29 16:37:08 +03:00
Franck Nijhof e0ee456bfa Fix invalid marker usage in LIFX service schemas (#175117) 2026-06-29 15:26:04 +02:00
Maciej Bieniek ebeb98dd83 Add missing translation key in the NextDNS integration (#175120) 2026-06-29 15:15:57 +02:00
Onero-testdev 49cff5f980 Add Candle Warmer Lamp support to switchbot (#173585)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:15:26 +02:00
TimL c24186e571 Add Bluetooth proxy support for SMLIGHT (#174710)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-06-29 15:12:49 +02:00
FuNK3Y c9ea8baf61 Bump ical to 13.3.0 (#175090)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:53:58 +02:00
Guillaume Winter f21426dfa6 Add fan speed sensor to Freebox integration (#175081) 2026-06-29 14:50:56 +02:00
Ariel Ebersberger a450999646 Rename "advanced_settings" section to "additional_settings" in Autoskope (#175106) 2026-06-29 14:50:18 +02:00
Ariel Ebersberger 5b8ff19d8d Rename "advanced_settings" section to "additional_settings" in Telegram bot (#175107) 2026-06-29 14:48:47 +02:00
Ronald van der Meer 25d505bcf3 Fix Duco ventilation sensors not being created for valve nodes (#174971) 2026-06-29 14:47:19 +02:00
Franck Nijhof 91cb829881 Fix invalid use of Exclusive marker in Kira name schema (#175115) 2026-06-29 14:44:02 +02:00
renovate[bot] 4fdc4e6219 Update ruff to v0.15.18 (#175087) 2026-06-29 14:41:47 +02:00
Ariel Ebersberger ca7ae00c7e Rename "advanced_settings" section to "additional_settings" in History Stats (#175109) 2026-06-29 14:32:57 +02:00
Ariel Ebersberger ff460901b7 Rename "advanced_options" section to "additional_options" in DNS IP (#175105) 2026-06-29 13:14:58 +02:00
Ariel Ebersberger 30512f08a8 Rename "advanced" step to "additional" in OpenAI Conversation (#175104) 2026-06-29 13:14:19 +02:00
Ariel Ebersberger 9dd1a59d50 Rename "advanced" step to "additional" in Anthropic (#175101) 2026-06-29 13:14:01 +02:00
TrojanHorsePower 91aded4474 Update vsure to 2.8.0 (#175060) 2026-06-29 11:46:21 +02:00
Erwin Douna 543eab3354 Portainer add utility.py for duplicate code (#175082) 2026-06-29 11:04:02 +02:00
Ariel Ebersberger 47b331a869 Rename "Advanced settings" title in OpenAI Conversation (#175096) 2026-06-29 11:00:21 +02:00
Ariel Ebersberger 696dd45803 Rename "Advanced settings" title in Anthropic (#175095) 2026-06-29 10:59:53 +02:00
Ariel Ebersberger f92239877f Rename "Advanced migration/setup" options in ZHA (#175094) 2026-06-29 10:59:35 +02:00
Ariel Ebersberger 45ceb13937 Rename "Advanced settings" sections in Scrape (#175098) 2026-06-29 10:58:26 +02:00
Ariel Ebersberger c5aeee8097 Rename "(advanced)" service name in Music Assistant (#175097) 2026-06-29 10:58:04 +02:00
Erik Montnemery bfc750b608 Tell bots that we like small try-clauses (#174654) 2026-06-29 11:16:46 +03:00
70 changed files with 1362 additions and 226 deletions
+1
View File
@@ -54,3 +54,4 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
- When catching exceptions, try-clauses should be as small as possible, i.e. avoid wrapping large blocks of code in a try-clause, and avoid catching exceptions from functions that are not expected to raise them.
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.17
rev: v0.15.18
hooks:
- id: ruff-check
args:
+1
View File
@@ -43,3 +43,4 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
- When catching exceptions, try-clauses should be as small as possible, i.e. avoid wrapping large blocks of code in a try-clause, and avoid catching exceptions from functions that are not expected to raise them.
@@ -298,7 +298,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
):
self.options.pop(CONF_LLM_HASS_API)
if not errors:
return await self.async_step_advanced()
return await self.async_step_additional()
return self.async_show_form(
step_id="init",
@@ -308,10 +308,10 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
errors=errors or None,
)
async def async_step_advanced(
async def async_step_additional(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Manage advanced options."""
"""Manage additional options."""
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
@@ -360,7 +360,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
return await self.async_step_model()
return self.async_show_form(
step_id="advanced",
step_id="additional",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(step_schema), self.options
),
@@ -48,16 +48,16 @@
"user": "Add AI task"
},
"step": {
"advanced": {
"additional": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::prompt_caching%]"
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::additional::data::prompt_caching%]"
},
"data_description": {
"chat_model": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::chat_model%]",
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::prompt_caching%]"
"chat_model": "[%key:component::anthropic::config_subentries::conversation::step::additional::data_description::chat_model%]",
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::additional::data_description::prompt_caching%]"
},
"title": "[%key:component::anthropic::config_subentries::conversation::step::advanced::title%]"
"title": "[%key:component::anthropic::config_subentries::conversation::step::additional::title%]"
},
"init": {
"data": {
@@ -115,7 +115,7 @@
"user": "Add conversation agent"
},
"step": {
"advanced": {
"additional": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"prompt_caching": "Caching strategy"
@@ -124,7 +124,7 @@
"chat_model": "The model to serve the responses.",
"prompt_caching": "Optimize your API cost and response times based on your usage."
},
"title": "Advanced settings"
"title": "Additional settings"
},
"init": {
"data": {
@@ -17,7 +17,7 @@ from homeassistant.helpers.selector import (
TextSelectorType,
)
from .const import DEFAULT_HOST, DOMAIN, SECTION_ADVANCED_SETTINGS
from .const import DEFAULT_HOST, DOMAIN, SECTION_ADDITIONAL_SETTINGS
STEP_USER_DATA_SCHEMA = vol.Schema(
{
@@ -25,7 +25,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Required(SECTION_ADDITIONAL_SETTINGS): section(
vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): TextSelector(
@@ -79,7 +79,7 @@ class AutoskopeConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
username = user_input[CONF_USERNAME].lower()
host = user_input[SECTION_ADVANCED_SETTINGS][CONF_HOST].lower()
host = user_input[SECTION_ADDITIONAL_SETTINGS][CONF_HOST].lower()
try:
cv.url(host)
+1 -1
View File
@@ -5,5 +5,5 @@ from datetime import timedelta
DOMAIN = "autoskope"
DEFAULT_HOST = "https://portal.autoskope.de"
SECTION_ADVANCED_SETTINGS = "advanced_settings"
SECTION_ADDITIONAL_SETTINGS = "additional_settings"
UPDATE_INTERVAL = timedelta(seconds=60)
@@ -31,7 +31,7 @@
},
"description": "Enter your Autoskope credentials.",
"sections": {
"advanced_settings": {
"additional_settings": {
"data": {
"host": "API endpoint"
},
@@ -20,7 +20,7 @@ from homeassistant.data_entry_flow import SectionConfig, section
from homeassistant.helpers import config_validation as cv
from .const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_HOSTNAME,
CONF_IPV4,
CONF_IPV6,
@@ -39,7 +39,7 @@ from .const import (
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string,
vol.Required(CONF_ADVANCED_OPTIONS): section(
vol.Required(CONF_ADDITIONAL_OPTIONS): section(
vol.Schema(
{
vol.Optional(CONF_RESOLVER): cv.string,
@@ -117,13 +117,13 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input:
hostname = user_input[CONF_HOSTNAME]
name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname
advanced_options = user_input[CONF_ADVANCED_OPTIONS]
resolver = advanced_options.get(CONF_RESOLVER, DEFAULT_RESOLVER)
resolver_ipv6 = advanced_options.get(
additional_options = user_input[CONF_ADDITIONAL_OPTIONS]
resolver = additional_options.get(CONF_RESOLVER, DEFAULT_RESOLVER)
resolver_ipv6 = additional_options.get(
CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6
)
port = advanced_options.get(CONF_PORT, DEFAULT_PORT)
port_ipv6 = advanced_options.get(CONF_PORT_IPV6, DEFAULT_PORT)
port = additional_options.get(CONF_PORT, DEFAULT_PORT)
port_ipv6 = additional_options.get(CONF_PORT_IPV6, DEFAULT_PORT)
validate = await async_validate_hostname(
hostname, resolver, resolver_ipv6, port, port_ipv6
+1 -1
View File
@@ -12,7 +12,7 @@ CONF_PORT_IPV6 = "port_ipv6"
CONF_IPV4 = "ipv4"
CONF_IPV6 = "ipv6"
CONF_IPV6_V4 = "ipv6_v4"
CONF_ADVANCED_OPTIONS = "advanced_options"
CONF_ADDITIONAL_OPTIONS = "additional_options"
DEFAULT_HOSTNAME = "myip.opendns.com"
DEFAULT_IPV6 = False
+9 -9
View File
@@ -15,7 +15,7 @@
"hostname": "The hostname for which to perform the DNS query."
},
"sections": {
"advanced_options": {
"additional_options": {
"data": {
"port": "IPv4 port",
"port_ipv6": "IPv6 port",
@@ -63,16 +63,16 @@
"step": {
"init": {
"data": {
"port": "[%key:component::dnsip::config::step::user::sections::advanced_options::data::port%]",
"port_ipv6": "[%key:component::dnsip::config::step::user::sections::advanced_options::data::port_ipv6%]",
"resolver": "[%key:component::dnsip::config::step::user::sections::advanced_options::data::resolver%]",
"resolver_ipv6": "[%key:component::dnsip::config::step::user::sections::advanced_options::data::resolver_ipv6%]"
"port": "[%key:component::dnsip::config::step::user::sections::additional_options::data::port%]",
"port_ipv6": "[%key:component::dnsip::config::step::user::sections::additional_options::data::port_ipv6%]",
"resolver": "[%key:component::dnsip::config::step::user::sections::additional_options::data::resolver%]",
"resolver_ipv6": "[%key:component::dnsip::config::step::user::sections::additional_options::data::resolver_ipv6%]"
},
"data_description": {
"port": "[%key:component::dnsip::config::step::user::sections::advanced_options::data_description::port%]",
"port_ipv6": "[%key:component::dnsip::config::step::user::sections::advanced_options::data_description::port_ipv6%]",
"resolver": "[%key:component::dnsip::config::step::user::sections::advanced_options::data_description::resolver%]",
"resolver_ipv6": "[%key:component::dnsip::config::step::user::sections::advanced_options::data_description::resolver_ipv6%]"
"port": "[%key:component::dnsip::config::step::user::sections::additional_options::data_description::port%]",
"port_ipv6": "[%key:component::dnsip::config::step::user::sections::additional_options::data_description::port_ipv6%]",
"resolver": "[%key:component::dnsip::config::step::user::sections::additional_options::data_description::resolver%]",
"resolver_ipv6": "[%key:component::dnsip::config::step::user::sections::additional_options::data_description::resolver_ipv6%]"
},
"description": "Optionally change resolvers and ports."
}
+14
View File
@@ -2,9 +2,23 @@
from datetime import timedelta
from duco_connectivity.models import NodeType
from homeassistant.const import Platform
DOMAIN = "duco"
PLATFORMS = [Platform.FAN, Platform.SELECT, Platform.SENSOR]
SCAN_INTERVAL = timedelta(seconds=10)
BOX_NODE_ID = 1
VENTILATION_CAPABLE_NODE_TYPES: tuple[NodeType, ...] = (
NodeType.BOX,
NodeType.VLV,
NodeType.VLVRH,
NodeType.VLVVOC,
NodeType.VLVCO2,
NodeType.VLVCO2RH,
NodeType.EAV,
NodeType.EAVRH,
NodeType.EAVVOC,
NodeType.EAVCO2,
)
+2 -16
View File
@@ -10,7 +10,6 @@ from duco_connectivity import (
KnownActionName,
Node,
NodeListActionItemList,
NodeType,
VentilationState,
)
@@ -19,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .const import DOMAIN, VENTILATION_CAPABLE_NODE_TYPES
from .coordinator import DucoConfigEntry, DucoCoordinator
from .entity import DucoEntity
@@ -27,19 +26,6 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
SUPPORTED_SELECT_NODE_TYPES = {
NodeType.BOX,
NodeType.VLV,
NodeType.VLVRH,
NodeType.VLVVOC,
NodeType.VLVCO2,
NodeType.VLVCO2RH,
NodeType.EAV,
NodeType.EAVRH,
NodeType.EAVVOC,
NodeType.EAVCO2,
}
def _get_ventilation_options(action: ActionItem) -> tuple[str, ...] | None:
"""Return ventilation options advertised by a node action."""
@@ -86,7 +72,7 @@ async def async_setup_entry(
# Duco advertises SetVentilationState broadly, so keep the select
# limited to the box and known valve node families.
if node.general.node_type not in SUPPORTED_SELECT_NODE_TYPES:
if node.general.node_type not in VENTILATION_CAPABLE_NODE_TYPES:
continue
options = options_by_node.get(node.node_id)
+4 -4
View File
@@ -25,7 +25,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import BOX_NODE_ID, DOMAIN
from .const import BOX_NODE_ID, DOMAIN, VENTILATION_CAPABLE_NODE_TYPES
from .coordinator import DucoConfigEntry, DucoCoordinator
from .entity import DucoEntity
@@ -65,7 +65,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
if node.ventilation and node.ventilation.state != VentilationState.UNKNOWN
else None
),
node_types=(NodeType.BOX,),
node_types=VENTILATION_CAPABLE_NODE_TYPES,
),
DucoSensorEntityDescription(
key="target_flow_level",
@@ -76,7 +76,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
value_fn=lambda node: (
node.ventilation.flow_lvl_tgt if node.ventilation else None
),
node_types=(NodeType.BOX,),
node_types=VENTILATION_CAPABLE_NODE_TYPES,
),
DucoSensorEntityDescription(
key="time_state_end",
@@ -89,7 +89,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
if node.ventilation and node.ventilation.time_state_end != 0
else None
),
node_types=(NodeType.BOX,),
node_types=VENTILATION_CAPABLE_NODE_TYPES,
),
DucoSensorEntityDescription(
key="co2",
+13 -1
View File
@@ -126,6 +126,8 @@ class FreeboxRouter:
self.raids: dict[int, dict[str, Any]] = {}
self.sensors_temperature: dict[str, int] = {}
self.sensors_temperature_names: dict[str, str] = {}
self.sensors_fan: dict[str, int] = {}
self.sensors_fan_names: dict[str, str] = {}
self.sensors_connection: dict[str, float] = {}
self.call_list: list[dict[str, Any]] = []
self.home_granted = True
@@ -190,6 +192,12 @@ class FreeboxRouter:
self.sensors_temperature[sensor_id] = sensor.get("value")
self.sensors_temperature_names[sensor_id] = sensor["name"]
# Fan speed sensors (rpm). Name and id may vary under Freebox devices.
for fan in syst_datas.get("fans", []):
fan_id = fan["id"]
self.sensors_fan[fan_id] = fan.get("value")
self.sensors_fan_names[fan_id] = fan["name"]
# Connection sensors
connection_datas: dict[str, Any] = await self._api.connection.get_status()
for sensor_key in CONNECTION_SENSORS_KEYS:
@@ -321,7 +329,11 @@ class FreeboxRouter:
@property
def sensors(self) -> dict[str, Any]:
"""Return sensors."""
return {**self.sensors_temperature, **self.sensors_connection}
return {
**self.sensors_temperature,
**self.sensors_fan,
**self.sensors_connection,
}
@property
def call(self) -> Call:
@@ -11,6 +11,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
EntityCategory,
UnitOfDataRate,
UnitOfTemperature,
@@ -93,6 +94,27 @@ async def async_setup_entry(
for sensor_id, sensor_name in router.sensors_temperature_names.items()
]
_LOGGER.debug(
"%s - %s - %s fan sensors",
router.name,
router.mac,
len(router.sensors_fan_names),
)
entities.extend(
FreeboxSensor(
router,
SensorEntityDescription(
key=fan_id,
name=fan_name,
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:fan",
),
)
for fan_id, fan_name in router.sensors_fan_names.items()
)
entities.extend(
[FreeboxSensor(router, description) for description in CONNECTION_SENSORS]
)
@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.5"]
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.3.0"]
}
@@ -20,7 +20,7 @@ from .const import (
CONF_MIN_STATE_DURATION,
CONF_START,
PLATFORMS,
SECTION_ADVANCED_SETTINGS,
SECTION_ADDITIONAL_SETTINGS,
)
from .coordinator import HistoryStatsUpdateCoordinator
from .data import HistoryStats
@@ -44,8 +44,8 @@ async def async_setup_entry(
min_state_duration: timedelta
if duration_dict := entry.options.get(CONF_DURATION):
duration = timedelta(**duration_dict)
advanced_settings = entry.options.get(SECTION_ADVANCED_SETTINGS, {})
if min_state_duration_dict := advanced_settings.get(CONF_MIN_STATE_DURATION):
additional_settings = entry.options.get(SECTION_ADDITIONAL_SETTINGS, {})
if min_state_duration_dict := additional_settings.get(CONF_MIN_STATE_DURATION):
min_state_duration = timedelta(**min_state_duration_dict)
else:
min_state_duration = timedelta(0)
@@ -121,6 +121,13 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
hass.config_entries.async_update_entry(
config_entry, options=options, minor_version=3
)
if config_entry.minor_version < 4:
# The "advanced_settings" section was renamed to "additional_settings"
if (additional := options.pop("advanced_settings", None)) is not None:
options[SECTION_ADDITIONAL_SETTINGS] = additional
hass.config_entries.async_update_entry(
config_entry, options=options, minor_version=4
)
_LOGGER.debug(
"Migration to version %s.%s successful",
@@ -44,7 +44,7 @@ from .const import (
CONF_TYPE_TIME,
DEFAULT_NAME,
DOMAIN,
SECTION_ADVANCED_SETTINGS,
SECTION_ADDITIONAL_SETTINGS,
)
from .coordinator import HistoryStatsUpdateCoordinator
from .data import HistoryStats
@@ -149,7 +149,7 @@ def _get_options_schema_with_entity_id(entity_id: str, type: str) -> vol.Schema:
mode=SelectSelectorMode.DROPDOWN,
),
),
vol.Optional(SECTION_ADVANCED_SETTINGS): section(
vol.Optional(SECTION_ADDITIONAL_SETTINGS): section(
vol.Schema(
{
vol.Optional(CONF_MIN_STATE_DURATION): DurationSelector(
@@ -189,7 +189,7 @@ OPTIONS_FLOW = {
class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for History stats."""
MINOR_VERSION = 3
MINOR_VERSION = 4
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
@@ -290,8 +290,8 @@ async def ws_start_preview(
start = validated_data.get(CONF_START)
end = validated_data.get(CONF_END)
duration = validated_data.get(CONF_DURATION)
advanced_settings = validated_data.get(SECTION_ADVANCED_SETTINGS, {})
min_state_duration = advanced_settings.get(CONF_MIN_STATE_DURATION)
additional_settings = validated_data.get(SECTION_ADDITIONAL_SETTINGS, {})
min_state_duration = additional_settings.get(CONF_MIN_STATE_DURATION)
state_class = validated_data.get(CONF_STATE_CLASS)
history_stats = HistoryStats(
@@ -18,4 +18,4 @@ CONF_TYPE_KEYS = [CONF_TYPE_TIME, CONF_TYPE_RATIO, CONF_TYPE_COUNT]
DEFAULT_NAME = "unnamed statistics"
SECTION_ADVANCED_SETTINGS = "advanced_settings"
SECTION_ADDITIONAL_SETTINGS = "additional_settings"
@@ -28,7 +28,7 @@
},
"description": "Read the documentation for further details on how to configure the history stats sensor using these options.",
"sections": {
"advanced_settings": {
"additional_settings": {
"data": { "min_state_duration": "Minimum state duration" },
"data_description": {
"min_state_duration": "The minimum state duration to account for the statistics. Default is 0 seconds."
@@ -93,14 +93,14 @@
},
"description": "[%key:component::history_stats::config::step::options::description%]",
"sections": {
"advanced_settings": {
"additional_settings": {
"data": {
"min_state_duration": "[%key:component::history_stats::config::step::options::sections::advanced_settings::data::min_state_duration%]"
"min_state_duration": "[%key:component::history_stats::config::step::options::sections::additional_settings::data::min_state_duration%]"
},
"data_description": {
"min_state_duration": "[%key:component::history_stats::config::step::options::sections::advanced_settings::data_description::min_state_duration%]"
"min_state_duration": "[%key:component::history_stats::config::step::options::sections::additional_settings::data_description::min_state_duration%]"
},
"name": "[%key:component::history_stats::config::step::options::sections::advanced_settings::name%]"
"name": "[%key:component::history_stats::config::step::options::sections::additional_settings::name%]"
}
}
}
+2 -2
View File
@@ -49,7 +49,7 @@ CODE_SCHEMA = vol.Schema(
SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DOMAIN): vol.Exclusive(cv.string, "sensors"),
vol.Optional(CONF_NAME, default=DOMAIN): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
@@ -57,7 +57,7 @@ SENSOR_SCHEMA = vol.Schema(
REMOTE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DOMAIN): vol.Exclusive(cv.string, "remotes"),
vol.Optional(CONF_NAME, default=DOMAIN): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
+1 -1
View File
@@ -66,7 +66,7 @@ LIFX_SET_STATE_SCHEMA: VolDictType = {
SERVICE_LIFX_SET_HEV_CYCLE_STATE = "set_hev_cycle_state"
LIFX_SET_HEV_CYCLE_STATE_SCHEMA: VolDictType = {
ATTR_POWER: vol.Required(cv.boolean),
vol.Required(ATTR_POWER): cv.boolean,
ATTR_DURATION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=86400)),
}
+3 -7
View File
@@ -178,9 +178,7 @@ LIFX_EFFECT_MORPH_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)),
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.Optional(
vol.In(ThemeLibrary().themes)
),
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.In(ThemeLibrary().themes),
vol.Exclusive(ATTR_PALETTE, COLOR_GROUP): vol.All(
cv.ensure_list, [HSBK_SCHEMA]
),
@@ -192,7 +190,7 @@ LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)),
ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS),
ATTR_THEME: vol.Optional(vol.In(ThemeLibrary().themes)),
vol.Optional(ATTR_THEME): vol.In(ThemeLibrary().themes),
}
)
@@ -211,9 +209,7 @@ LIFX_PAINT_THEME_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_TRANSITION: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=3600)),
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.Optional(
vol.In(ThemeLibrary().themes)
),
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.In(ThemeLibrary().themes),
vol.Exclusive(ATTR_PALETTE, COLOR_GROUP): vol.All(
cv.ensure_list, [HSBK_SCHEMA]
),
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==13.2.5"]
"requirements": ["ical==13.3.0"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==13.2.5"]
"requirements": ["ical==13.3.0"]
}
@@ -374,7 +374,7 @@
},
"get_queue": {
"description": "Retrieves the details of the currently active queue of a Music Assistant player.",
"name": "Get playerQueue details (advanced)"
"name": "Get playerQueue details"
},
"play_announcement": {
"description": "Plays an announcement on a Music Assistant player with more fine-grained control options.",
+1 -1
View File
@@ -267,7 +267,7 @@ SWITCHES = (
),
NextDnsSwitchEntityDescription(
key="block_hulu",
name="Block Hulu",
translation_key="block_hulu",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_hulu,
@@ -326,7 +326,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
options.update(user_input)
if CONF_LLM_HASS_API in options and CONF_LLM_HASS_API not in user_input:
options.pop(CONF_LLM_HASS_API)
return await self.async_step_advanced()
return await self.async_step_additional()
return self.async_show_form(
step_id="init",
@@ -335,10 +335,10 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
),
)
async def async_step_advanced(
async def async_step_additional(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Manage advanced options."""
"""Manage additional options."""
options = self.options
errors: dict[str, str] = {}
@@ -374,7 +374,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
return await self.async_step_model()
return self.async_show_form(
step_id="advanced",
step_id="additional",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(step_schema), options
),
@@ -47,18 +47,18 @@
"user": "Add AI task"
},
"step": {
"advanced": {
"additional": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::max_tokens%]",
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::store_responses%]",
"temperature": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::temperature%]",
"top_p": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::top_p%]"
"max_tokens": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data::max_tokens%]",
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data::store_responses%]",
"temperature": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data::temperature%]",
"top_p": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data::top_p%]"
},
"data_description": {
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data_description::store_responses%]"
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data_description::store_responses%]"
},
"title": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::title%]"
"title": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::title%]"
},
"init": {
"data": {
@@ -109,7 +109,7 @@
"user": "Add conversation agent"
},
"step": {
"advanced": {
"additional": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "Maximum tokens to return in response",
@@ -120,7 +120,7 @@
"data_description": {
"store_responses": "If enabled, requests and responses are stored by OpenAI and visible in your OpenAI dashboard logs"
},
"title": "Advanced settings"
"title": "Additional settings"
},
"init": {
"data": {
@@ -34,6 +34,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .util import sanitize_container_name
type PortainerConfigEntry = ConfigEntry[PortainerCoordinator]
@@ -263,7 +264,7 @@ class PortainerCoordinator(
# Map containers, started and stopped
for container in containers:
container_name = self._get_container_name(container.names[0])
container_name = sanitize_container_name(container.names[0])
prev_container = (
prev_endpoint.containers.get(container_name)
if prev_endpoint
@@ -313,7 +314,7 @@ class PortainerCoordinator(
container_stats = dict(
zip(
(
self._get_container_name(container.names[0])
sanitize_container_name(container.names[0])
for container in active_containers
),
await asyncio.gather(
@@ -431,10 +432,6 @@ class PortainerCoordinator(
for stack_callback in self.new_stacks_callbacks:
stack_callback(new_stack_data)
def _get_container_name(self, container_name: str) -> str:
"""Sanitize to get a proper container name."""
return container_name.replace("/", " ").strip()
class PortainerDockerDiskSpaceCoordinator(
PortainerBaseCoordinator[dict[int, DockerSystemDF]]
+2 -1
View File
@@ -19,6 +19,7 @@ from .coordinator import (
PortainerStackData,
PortainerVolumeData,
)
from .util import sanitize_container_name
class PortainerCoordinatorEntity(CoordinatorEntity[PortainerCoordinator]):
@@ -95,7 +96,7 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
# According to Docker's API docs, the first name is unique
names = self._device_info.container.names
assert names, "Container names list unexpectedly empty"
self.device_name = names[0].replace("/", " ").strip()
self.device_name = sanitize_container_name(names[0])
self._attr_device_info = DeviceInfo(
identifiers={
@@ -0,0 +1,6 @@
"""Utility functions for the Portainer integration."""
def sanitize_container_name(container_name: str) -> str:
"""Sanitize to get a proper container name."""
return container_name.replace("/", " ").strip()
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==13.2.5"]
"requirements": ["ical==13.3.0"]
}
+4 -4
View File
@@ -32,8 +32,8 @@
"timeout": "Timeout for connection to website.",
"verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed."
},
"description": "Provide additional advanced settings for the resource.",
"name": "Advanced settings"
"description": "Provide additional settings for the resource.",
"name": "Additional settings"
},
"auth": {
"data": {
@@ -117,8 +117,8 @@
"unit_of_measurement": "Choose unit of measurement or create your own.",
"value_template": "Defines a template to get the state of the sensor."
},
"description": "Provide additional advanced settings for the sensor.",
"name": "Advanced settings"
"description": "Provide additional settings for the sensor.",
"name": "Additional settings"
}
}
}
+19 -6
View File
@@ -1,19 +1,21 @@
"""SMLIGHT SLZB Zigbee device integration."""
"""SMLIGHT SLZB device integration."""
from pysmlight import Api2
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .bluetooth import async_connect_scanner
from .const import DOMAIN
from .coordinator import (
SmConfigEntry,
SmDataUpdateCoordinator,
SmFirmwareUpdateCoordinator,
SmlightData,
base_device_info,
)
from .services import async_setup_services
@@ -37,7 +39,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
"""Set up SMLIGHT Zigbee from a config entry."""
"""Set up SMLIGHT from a config entry."""
client = Api2(host=entry.data[CONF_HOST], session=async_get_clientsession(hass))
data_coordinator = SmDataUpdateCoordinator(hass, entry, client)
@@ -46,13 +48,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
await data_coordinator.async_config_entry_first_refresh()
await firmware_coordinator.async_config_entry_first_refresh()
if data_coordinator.data.info.legacy_api < 2:
info = data_coordinator.data.info
if info.legacy_api < 2:
entry.async_create_background_task(
hass, client.sse.client(), "smlight-sse-client"
)
if info.ble is not None and info.ble.proxy_enabled:
device_registry = dr.async_get(hass)
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
**base_device_info(info, client.host),
)
entry.async_on_unload(async_connect_scanner(hass, entry, info.model, device.id))
entry.runtime_data = SmlightData(
data=data_coordinator, firmware=firmware_coordinator
data=data_coordinator,
firmware=firmware_coordinator,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -60,5 +73,5 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
"""Unload a config entry."""
"""Unload SMLIGHT config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,67 @@
"""Bluetooth proxy for SLZB devices using bleak-smlight."""
from functools import partial
from bleak_smlight import SLZB_BLE_SERVER_PORT, connect_scanner
from pysmlight import BleProxyClient
from homeassistant.components.bluetooth import (
BluetoothScanningMode,
async_register_scanner,
)
from homeassistant.const import CONF_HOST
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from .const import DOMAIN
from .coordinator import SmConfigEntry
@callback
def _async_unload(
unload_callbacks: list[CALLBACK_TYPE],
client: BleProxyClient,
) -> None:
"""Unload callbacks and stop client."""
for callback_func in unload_callbacks:
callback_func()
client.stop()
@callback
def async_connect_scanner(
hass: HomeAssistant,
entry: SmConfigEntry,
model: str | None,
device_id: str,
) -> CALLBACK_TYPE:
"""Connect scanner using the external bleak-smlight backend."""
assert entry.unique_id is not None
client_data = connect_scanner(
source=entry.unique_id,
name=entry.title,
host=entry.data[CONF_HOST],
port=SLZB_BLE_SERVER_PORT,
)
client_data.scanner.async_set_scanning_mode(BluetoothScanningMode.AUTO)
entry.async_create_background_task(
hass,
client_data.client.start(),
f"smlight-ble-proxy-client-{entry.unique_id}",
)
unload_callbacks = [
async_register_scanner(
hass,
client_data.scanner,
source_domain=DOMAIN,
source_model=model,
source_config_entry_id=entry.entry_id,
source_device_id=device_id,
),
client_data.scanner.async_setup(),
]
return partial(_async_unload, unload_callbacks, client_data.client)
@@ -15,11 +15,21 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_FIRMWARE_INTERVAL, SCAN_INTERVAL
from .const import (
ATTR_MANUFACTURER,
DOMAIN,
LOGGER,
SCAN_FIRMWARE_INTERVAL,
SCAN_INTERVAL,
)
@dataclass(kw_only=True)
@@ -50,6 +60,17 @@ class SmFwData:
type SmConfigEntry = ConfigEntry[SmlightData]
def base_device_info(info: Info, host: str) -> DeviceInfo:
"""Return device registry information."""
return DeviceInfo(
configuration_url=f"http://{host}",
connections={(CONNECTION_NETWORK_MAC, str(info.MAC))},
manufacturer=ATTR_MANUFACTURER,
model=info.model,
sw_version=f"core: {info.sw_version} / zigbee: {info.zb_version}",
)
class SmBaseDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Base Coordinator for SMLIGHT."""
@@ -93,6 +114,7 @@ class SmBaseDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
info = await self.client.get_info()
self.unique_id = format_mac(info.MAC)
self.legacy_api = info.legacy_api
if info.legacy_api == 2:
ir.async_create_issue(
self.hass,
+3 -17
View File
@@ -1,14 +1,8 @@
"""Base class for all SMLIGHT entities."""
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_MANUFACTURER
from .coordinator import SmBaseDataUpdateCoordinator
from .coordinator import SmBaseDataUpdateCoordinator, base_device_info
class SmEntity(CoordinatorEntity[SmBaseDataUpdateCoordinator]):
@@ -19,14 +13,6 @@ class SmEntity(CoordinatorEntity[SmBaseDataUpdateCoordinator]):
def __init__(self, coordinator: SmBaseDataUpdateCoordinator) -> None:
"""Initialize entity with device."""
super().__init__(coordinator)
mac = format_mac(coordinator.data.info.MAC)
self._attr_device_info = DeviceInfo(
configuration_url=f"http://{coordinator.client.host}",
connections={(CONNECTION_NETWORK_MAC, mac)},
manufacturer=ATTR_MANUFACTURER,
model=coordinator.data.info.model,
sw_version=(
f"core: {coordinator.data.info.sw_version}"
f" / zigbee: {coordinator.data.info.zb_version}"
),
self._attr_device_info = base_device_info(
coordinator.data.info, coordinator.client.host
)
@@ -3,6 +3,7 @@
"name": "SMLIGHT SLZB",
"codeowners": ["@tl-sl"],
"config_flow": true,
"dependencies": ["bluetooth"],
"dhcp": [
{
"registered_devices": true
@@ -12,7 +13,7 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["pysmlight==0.5.0"],
"requirements": ["pysmlight==0.5.0", "bleak-smlight==1.1.0"],
"zeroconf": [
{
"type": "_slzb-06._tcp.local."
@@ -192,6 +192,7 @@ PLATFORMS_BY_TYPE = {
Platform.SENSOR,
],
SupportedModels.WEATHER_STATION.value: [Platform.SENSOR],
SupportedModels.CANDLE_WARMER_LAMP.value: [Platform.LIGHT, Platform.SENSOR],
}
CLASS_BY_DEVICE = {
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
@@ -245,6 +246,7 @@ CLASS_BY_DEVICE = {
SupportedModels.LOCK_VISION_PRO.value: switchbot.SwitchbotLock,
SupportedModels.LOCK_VISION.value: switchbot.SwitchbotLock,
SupportedModels.LOCK_PRO_WIFI.value: switchbot.SwitchbotLock,
SupportedModels.CANDLE_WARMER_LAMP.value: switchbot.SwitchbotCandleWarmerLamp,
}
@@ -72,6 +72,7 @@ class SupportedModels(StrEnum):
LOCK_PRO_WIFI = "lock_pro_wifi"
WEATHER_STATION = "weather_station"
STANDING_FAN = "standing_fan"
CANDLE_WARMER_LAMP = "candle_warmer_lamp"
CONNECTABLE_SUPPORTED_MODEL_TYPES = {
@@ -122,6 +123,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
SwitchbotModel.LOCK_VISION: SupportedModels.LOCK_VISION,
SwitchbotModel.LOCK_PRO_WIFI: SupportedModels.LOCK_PRO_WIFI,
SwitchbotModel.STANDING_FAN: SupportedModels.STANDING_FAN,
SwitchbotModel.CANDLE_WARMER_LAMP: SupportedModels.CANDLE_WARMER_LAMP,
}
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
@@ -171,6 +173,7 @@ ENCRYPTED_MODELS = {
SwitchbotModel.LOCK_VISION_PRO,
SwitchbotModel.LOCK_VISION,
SwitchbotModel.LOCK_PRO_WIFI,
SwitchbotModel.CANDLE_WARMER_LAMP,
}
ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
@@ -204,6 +207,7 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
SwitchbotModel.LOCK_VISION_PRO: switchbot.SwitchbotLock,
SwitchbotModel.LOCK_VISION: switchbot.SwitchbotLock,
SwitchbotModel.LOCK_PRO_WIFI: switchbot.SwitchbotLock,
SwitchbotModel.CANDLE_WARMER_LAMP: switchbot.SwitchbotCandleWarmerLamp,
}
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {
@@ -26,6 +26,7 @@ from .entity import SwitchbotEntity, exception_handler
SWITCHBOT_COLOR_MODE_TO_HASS = {
SwitchBotColorMode.RGB: ColorMode.RGB,
SwitchBotColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
SwitchBotColorMode.BRIGHTNESS: ColorMode.BRIGHTNESS,
}
_LOGGER = logging.getLogger(__name__)
@@ -53,7 +53,7 @@ from .const import (
PLATFORM_BROADCAST,
PLATFORM_POLLING,
PLATFORM_WEBHOOKS,
SECTION_ADVANCED_SETTINGS,
SECTION_ADDITIONAL_SETTINGS,
SUBENTRY_TYPE_ALLOWED_CHAT_IDS,
)
@@ -65,7 +65,7 @@ DESCRIPTION_PLACEHOLDERS: dict[str, str] = {
"id_bot_username": "@id_bot",
"id_bot_url": "https://t.me/id_bot",
"socks_url": "socks5://username:password@proxy_ip:proxy_port",
# used in advanced settings section
# used in additional settings section
"default_api_endpoint": DEFAULT_API_ENDPOINT,
}
@@ -87,7 +87,7 @@ STEP_USER_DATA_SCHEMA: vol.Schema = vol.Schema(
autocomplete="current-password",
)
),
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Required(SECTION_ADDITIONAL_SETTINGS): section(
vol.Schema(
{
vol.Required(
@@ -117,7 +117,7 @@ STEP_RECONFIGURE_USER_DATA_SCHEMA: vol.Schema = vol.Schema(
translation_key="platforms",
)
),
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Required(SECTION_ADDITIONAL_SETTINGS): section(
vol.Schema(
{
vol.Required(
@@ -241,10 +241,10 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
# validate connection to Telegram API
errors: dict[str, str] = {}
user_input[CONF_API_ENDPOINT] = user_input[SECTION_ADVANCED_SETTINGS][
user_input[CONF_API_ENDPOINT] = user_input[SECTION_ADDITIONAL_SETTINGS][
CONF_API_ENDPOINT
]
user_input[CONF_PROXY_URL] = user_input[SECTION_ADVANCED_SETTINGS].get(
user_input[CONF_PROXY_URL] = user_input[SECTION_ADDITIONAL_SETTINGS].get(
CONF_PROXY_URL
)
bot_name = await self._validate_bot(
@@ -270,9 +270,7 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_PLATFORM: user_input[CONF_PLATFORM],
CONF_API_ENDPOINT: user_input[CONF_API_ENDPOINT],
CONF_API_KEY: user_input[CONF_API_KEY],
CONF_PROXY_URL: user_input[SECTION_ADVANCED_SETTINGS].get(
CONF_PROXY_URL
),
CONF_PROXY_URL: user_input[CONF_PROXY_URL],
},
options={ATTR_PARSER: PARSER_MD},
description_placeholders=description_placeholders,
@@ -383,10 +381,10 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
data={
CONF_PLATFORM: self._step_user_data[CONF_PLATFORM],
CONF_API_KEY: self._step_user_data[CONF_API_KEY],
CONF_API_ENDPOINT: self._step_user_data[SECTION_ADVANCED_SETTINGS][
CONF_API_ENDPOINT: self._step_user_data[SECTION_ADDITIONAL_SETTINGS][
CONF_API_ENDPOINT
],
CONF_PROXY_URL: self._step_user_data[SECTION_ADVANCED_SETTINGS].get(
CONF_PROXY_URL: self._step_user_data[SECTION_ADDITIONAL_SETTINGS].get(
CONF_PROXY_URL
),
CONF_URL: user_input.get(CONF_URL),
@@ -461,7 +459,7 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
STEP_RECONFIGURE_USER_DATA_SCHEMA,
{
**self._get_reconfigure_entry().data,
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_API_ENDPOINT: self._get_reconfigure_entry().data[
CONF_API_ENDPOINT
],
@@ -473,11 +471,11 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
),
description_placeholders=DESCRIPTION_PLACEHOLDERS,
)
user_input[CONF_PROXY_URL] = user_input[SECTION_ADVANCED_SETTINGS].get(
user_input[CONF_PROXY_URL] = user_input[SECTION_ADDITIONAL_SETTINGS].get(
CONF_PROXY_URL
)
user_input[CONF_API_ENDPOINT] = user_input[SECTION_ADVANCED_SETTINGS][
user_input[CONF_API_ENDPOINT] = user_input[SECTION_ADDITIONAL_SETTINGS][
CONF_API_ENDPOINT
]
@@ -528,7 +526,7 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
STEP_RECONFIGURE_USER_DATA_SCHEMA,
{
**user_input,
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_API_ENDPOINT: user_input[CONF_API_ENDPOINT],
CONF_PROXY_URL: user_input.get(CONF_PROXY_URL),
},
@@ -7,7 +7,7 @@ DOMAIN = "telegram_bot"
PLATFORM_BROADCAST = "broadcast"
PLATFORM_POLLING = "polling"
PLATFORM_WEBHOOKS = "webhooks"
SECTION_ADVANCED_SETTINGS = "advanced_settings"
SECTION_ADDITIONAL_SETTINGS = "additional_settings"
SUBENTRY_TYPE_ALLOWED_CHAT_IDS = "allowed_chat_ids"
CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids"
@@ -33,16 +33,16 @@
},
"description": "Reconfigure Telegram bot",
"sections": {
"advanced_settings": {
"additional_settings": {
"data": {
"api_endpoint": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::data::api_endpoint%]",
"proxy_url": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::data::proxy_url%]"
"api_endpoint": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::data::api_endpoint%]",
"proxy_url": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::data::proxy_url%]"
},
"data_description": {
"api_endpoint": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::data_description::api_endpoint%]",
"proxy_url": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::data_description::proxy_url%]"
"api_endpoint": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::data_description::api_endpoint%]",
"proxy_url": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::data_description::proxy_url%]"
},
"name": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::name%]"
"name": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::name%]"
}
},
"title": "Telegram bot setup"
@@ -58,7 +58,7 @@
},
"description": "To create a Telegram bot, follow these steps:\n\n1. Open Telegram and start a chat with [{botfather_username}]({botfather_url}).\n1. Send the command `/newbot`.\n1. Follow the instructions to create your bot and get your API token.",
"sections": {
"advanced_settings": {
"additional_settings": {
"data": {
"api_endpoint": "API endpoint",
"proxy_url": "Proxy URL"
@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["verisure"],
"requirements": ["vsure==2.7.1"]
"requirements": ["vsure==2.8.0"]
}
@@ -1038,7 +1038,6 @@ async def handle_subscribe_trigger(
vol.Optional("variables"): dict,
}
)
@decorators.require_admin
@decorators.async_response
async def handle_test_condition(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
@@ -1101,7 +1100,6 @@ async def handle_test_condition(
vol.Required("condition"): cv.CONDITION_SCHEMA,
}
)
@decorators.require_admin
@decorators.async_response
async def handle_subscribe_condition(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
+2 -2
View File
@@ -55,7 +55,7 @@
"migration_strategy_recommended": "This is the quickest option to migrate to a new adapter."
},
"menu_options": {
"migration_strategy_advanced": "Advanced migration",
"migration_strategy_advanced": "Migrate manually",
"migration_strategy_recommended": "Migrate automatically (recommended)"
},
"title": "Migrate to a new adapter"
@@ -74,7 +74,7 @@
"setup_strategy_recommended": "This is the quickest option to create a new network and get started."
},
"menu_options": {
"setup_strategy_advanced": "Advanced setup",
"setup_strategy_advanced": "Set up manually",
"setup_strategy_recommended": "Set up automatically (recommended)"
},
"title": "Set up Zigbee"
+1 -1
View File
@@ -648,7 +648,7 @@ exclude_lines = [
]
[tool.ruff]
required-version = ">=0.15.17"
required-version = ">=0.15.18"
[tool.ruff.lint]
select = [
+5 -2
View File
@@ -653,6 +653,9 @@ bleak-esphome==3.9.4
# homeassistant.components.bluetooth
bleak-retry-connector==4.6.1
# homeassistant.components.smlight
bleak-smlight==1.1.0
# homeassistant.components.bluetooth
bleak==3.0.2
@@ -1314,7 +1317,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==13.2.5
ical==13.3.0
# homeassistant.components.caldav
icalendar==6.3.1
@@ -3313,7 +3316,7 @@ volkszaehler==0.4.0
volvocarsapi==0.4.3
# homeassistant.components.verisure
vsure==2.7.1
vsure==2.8.0
# homeassistant.components.vasttrafik
vtjp==0.2.1
+1 -1
View File
@@ -1,6 +1,6 @@
# Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit
codespell==2.4.2
ruff==0.15.17
ruff==0.15.18
yamllint==1.38.0
zizmor==1.24.1
+12 -12
View File
@@ -275,9 +275,9 @@ async def test_subentry_options_thinking_budget_more_than_max(
},
)
assert options["type"] is FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["step_id"] == "additional"
# Configure advanced step
# Configure additional step
options = await hass.config_entries.subentries.async_configure(
options["flow_id"],
{"chat_model": "claude-sonnet-4-5"},
@@ -330,9 +330,9 @@ async def test_subentry_web_search_user_location(
},
)
assert options["type"] is FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["step_id"] == "additional"
# Configure advanced step
# Configure additional step
options = await hass.config_entries.subentries.async_configure(
options["flow_id"],
{
@@ -424,7 +424,7 @@ async def test_model_list(
},
)
assert options["type"] is FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["step_id"] == "additional"
assert options["data_schema"].schema["chat_model"].config["options"] == snapshot
@@ -447,9 +447,9 @@ async def test_invalid_model(
},
)
assert options["type"] is FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["step_id"] == "additional"
# Configure advanced step but with api error
# Configure additional step but with api error
with patch(
"homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.retrieve",
new_callable=AsyncMock,
@@ -877,12 +877,12 @@ async def test_ai_task_subentry_not_loaded(
assert result.get("reason") == "entry_not_loaded"
async def test_creating_ai_task_subentry_advanced(
async def test_creating_ai_task_subentry_additional(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_init_component,
) -> None:
"""Test creating an AI task subentry with advanced settings."""
"""Test creating an AI task subentry with additional settings."""
result = await hass.config_entries.subentries.async_init(
(mock_config_entry.entry_id, "ai_task_data"),
context={"source": config_entries.SOURCE_USER},
@@ -891,7 +891,7 @@ async def test_creating_ai_task_subentry_advanced(
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "init"
# Go to advanced settings
# Go to additional settings
result2 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
@@ -901,9 +901,9 @@ async def test_creating_ai_task_subentry_advanced(
)
assert result2.get("type") is FlowResultType.FORM
assert result2.get("step_id") == "advanced"
assert result2.get("step_id") == "additional"
# Configure advanced settings
# Configure additional settings
result3 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
@@ -8,7 +8,7 @@ import pytest
from homeassistant.components.autoskope.const import (
DEFAULT_HOST,
DOMAIN,
SECTION_ADVANCED_SETTINGS,
SECTION_ADDITIONAL_SETTINGS,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
@@ -20,7 +20,7 @@ from tests.common import MockConfigEntry
USER_INPUT = {
CONF_USERNAME: "test_user",
CONF_PASSWORD: "test_password",
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_HOST: DEFAULT_HOST,
},
}
@@ -102,7 +102,7 @@ async def test_flow_invalid_url(
{
CONF_USERNAME: "test_user",
CONF_PASSWORD: "test_password",
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_HOST: "not-a-valid-url",
},
},
@@ -151,7 +151,7 @@ async def test_custom_host(
{
CONF_USERNAME: "test_user",
CONF_PASSWORD: "test_password",
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_HOST: "https://custom.autoskope.server",
},
},
+6 -6
View File
@@ -8,7 +8,7 @@ import pytest
from homeassistant import config_entries
from homeassistant.components.dnsip.config_flow import DATA_SCHEMA
from homeassistant.components.dnsip.const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_HOSTNAME,
CONF_IPV4,
CONF_IPV6,
@@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None:
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOSTNAME: "home-assistant.io", CONF_ADVANCED_OPTIONS: {}},
{CONF_HOSTNAME: "home-assistant.io", CONF_ADDITIONAL_OPTIONS: {}},
)
await hass.async_block_till_done()
@@ -71,7 +71,7 @@ async def test_form(hass: HomeAssistant) -> None:
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_with_advanced_options(hass: HomeAssistant) -> None:
async def test_form_with_additional_options(hass: HomeAssistant) -> None:
"""Test we can submit the form with custom resolver and port options."""
result = await hass.config_entries.flow.async_init(
@@ -95,7 +95,7 @@ async def test_form_with_advanced_options(hass: HomeAssistant) -> None:
result["flow_id"],
{
CONF_HOSTNAME: "home-assistant.io",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_RESOLVER: "8.8.8.8",
CONF_RESOLVER_IPV6: "2620:119:53::53",
CONF_PORT: 53,
@@ -136,7 +136,7 @@ async def test_form_error(hass: HomeAssistant) -> None:
result["flow_id"],
{
CONF_HOSTNAME: "home-assistant.io",
CONF_ADVANCED_OPTIONS: {},
CONF_ADDITIONAL_OPTIONS: {},
},
)
await hass.async_block_till_done()
@@ -185,7 +185,7 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None:
result["flow_id"],
{
CONF_HOSTNAME: "home-assistant.io",
CONF_ADVANCED_OPTIONS: {},
CONF_ADDITIONAL_OPTIONS: {},
},
)
await hass.async_block_till_done()
@@ -217,6 +217,206 @@
'state': '88',
})
# ---
# name: test_sensor_entities_state[sensor.bedroom_valve_state_end_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.bedroom_valve_state_end_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'State end time',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'State end time',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'time_state_end',
'unique_id': 'aa:bb:cc:dd:ee:ff_60_time_state_end',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_entities_state[sensor.bedroom_valve_state_end_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve State end time',
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_valve_state_end_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor_entities_state[sensor.bedroom_valve_target_flow_level-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.bedroom_valve_target_flow_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Target flow level',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Target flow level',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'target_flow_level',
'unique_id': 'aa:bb:cc:dd:ee:ff_60_target_flow_level',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.bedroom_valve_target_flow_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve Target flow level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_valve_target_flow_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor_entities_state[sensor.bedroom_valve_ventilation_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'aut1',
'aut2',
'aut3',
'man1',
'man2',
'man3',
'empt',
'cnt1',
'cnt2',
'cnt3',
'-',
'man1x2',
'man2x2',
'man3x2',
'man1x3',
'man2x3',
'man3x3',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.bedroom_valve_ventilation_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Ventilation state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Ventilation state',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ventilation_state',
'unique_id': 'aa:bb:cc:dd:ee:ff_60_ventilation_state',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_entities_state[sensor.bedroom_valve_ventilation_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve Ventilation state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'aut1',
'aut2',
'aut3',
'man1',
'man2',
'man3',
'empt',
'cnt1',
'cnt2',
'cnt3',
'-',
'man1x2',
'man2x2',
'man3x2',
'man1x3',
'man2x3',
'man3x3',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_valve_ventilation_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'auto',
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -326,6 +526,206 @@
'state': '76',
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_state_end_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.hall_valve_state_end_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'State end time',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'State end time',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'time_state_end',
'unique_id': 'aa:bb:cc:dd:ee:ff_61_time_state_end',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_state_end_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve State end time',
}),
'context': <ANY>,
'entity_id': 'sensor.hall_valve_state_end_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_target_flow_level-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.hall_valve_target_flow_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Target flow level',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Target flow level',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'target_flow_level',
'unique_id': 'aa:bb:cc:dd:ee:ff_61_target_flow_level',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_target_flow_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve Target flow level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.hall_valve_target_flow_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_ventilation_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'aut1',
'aut2',
'aut3',
'man1',
'man2',
'man3',
'empt',
'cnt1',
'cnt2',
'cnt3',
'-',
'man1x2',
'man2x2',
'man3x2',
'man1x3',
'man2x3',
'man3x3',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.hall_valve_ventilation_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Ventilation state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Ventilation state',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ventilation_state',
'unique_id': 'aa:bb:cc:dd:ee:ff_61_ventilation_state',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_ventilation_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve Ventilation state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'aut1',
'aut2',
'aut3',
'man1',
'man2',
'man3',
'empt',
'cnt1',
'cnt2',
'cnt3',
'-',
'man1x2',
'man2x2',
'man3x2',
'man1x3',
'man2x3',
'man3x3',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.hall_valve_ventilation_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'auto',
})
# ---
# name: test_sensor_entities_state[sensor.kitchen_rh_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -1072,3 +1472,203 @@
'state': '92',
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_state_end_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.study_valve_state_end_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'State end time',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'State end time',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'time_state_end',
'unique_id': 'aa:bb:cc:dd:ee:ff_62_time_state_end',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_state_end_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve State end time',
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_state_end_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_target_flow_level-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.study_valve_target_flow_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Target flow level',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Target flow level',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'target_flow_level',
'unique_id': 'aa:bb:cc:dd:ee:ff_62_target_flow_level',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_target_flow_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve Target flow level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_target_flow_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_ventilation_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'aut1',
'aut2',
'aut3',
'man1',
'man2',
'man3',
'empt',
'cnt1',
'cnt2',
'cnt3',
'-',
'man1x2',
'man2x2',
'man3x2',
'man1x3',
'man2x3',
'man3x3',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.study_valve_ventilation_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Ventilation state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Ventilation state',
'platform': 'duco',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ventilation_state',
'unique_id': 'aa:bb:cc:dd:ee:ff_62_ventilation_state',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_ventilation_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve Ventilation state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'auto',
'aut1',
'aut2',
'aut3',
'man1',
'man2',
'man3',
'empt',
'cnt1',
'cnt2',
'cnt3',
'-',
'man1x2',
'man2x2',
'man3x2',
'man1x3',
'man2x3',
'man3x3',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_ventilation_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'auto',
})
# ---
+57
View File
@@ -1,5 +1,6 @@
"""Tests for the Duco sensor platform."""
from dataclasses import replace
import logging
from unittest.mock import AsyncMock
@@ -29,6 +30,62 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat
FILTER_REMAINING_ENTITY_ID = "sensor.living_filter_remaining"
@pytest.mark.parametrize(
"ventilation_node_type",
[
pytest.param(NodeType.BOX, id="box"),
pytest.param(NodeType.VLV, id="vlv"),
pytest.param(NodeType.VLVRH, id="vlvrh"),
pytest.param(NodeType.VLVVOC, id="vlvvoc"),
pytest.param(NodeType.VLVCO2, id="vlvco2"),
pytest.param(NodeType.VLVCO2RH, id="vlvco2rh"),
pytest.param(NodeType.EAV, id="eav"),
pytest.param(NodeType.EAVRH, id="eavrh"),
pytest.param(NodeType.EAVVOC, id="eavvoc"),
pytest.param(NodeType.EAVCO2, id="eavco2"),
],
)
async def test_ventilation_related_sensors_created_for_supported_node_types(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
mock_sensor_nodes: list[Node],
ventilation_node_type: NodeType,
) -> None:
"""Test ventilation-related sensors are created for supported node families."""
supported_node = replace(
mock_sensor_nodes[0],
general=replace(mock_sensor_nodes[0].general, node_type=ventilation_node_type),
ventilation=replace(
mock_sensor_nodes[0].ventilation,
flow_lvl_tgt=42,
time_state_end=1700000400,
),
)
mock_duco_client.async_get_nodes.return_value = [
supported_node,
*mock_sensor_nodes[1:],
]
await setup_platform_integration(hass, mock_config_entry, [Platform.SENSOR])
state = hass.states.get("sensor.living_ventilation_state")
assert state is not None
assert state.state == "auto"
state = hass.states.get("sensor.living_target_flow_level")
assert state is not None
assert state.state == "42"
state = hass.states.get("sensor.living_state_end_time")
assert state is not None
assert state.state == "2023-11-14T22:20:00+00:00"
assert hass.states.get("sensor.office_co2_ventilation_state") is None
assert hass.states.get("sensor.office_co2_target_flow_level") is None
assert hass.states.get("sensor.office_co2_state_end_time") is None
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
+7
View File
@@ -102,6 +102,13 @@ async def test_temperature(hass: HomeAssistant, router: Mock) -> None:
assert hass.states.get("sensor.freebox_server_r2_temperature_cpu_b").state == "56"
async def test_fan(hass: HomeAssistant, router: Mock) -> None:
"""Test fan speed sensors expose API names and values."""
await setup_platform(hass, SENSOR_DOMAIN)
assert hass.states.get("sensor.freebox_server_r2_ventilateur_1").state == "2130"
async def test_battery(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, router: Mock
) -> None:
@@ -10,9 +10,11 @@ from homeassistant.components.history_stats.config_flow import (
)
from homeassistant.components.history_stats.const import (
CONF_END,
CONF_MIN_STATE_DURATION,
CONF_START,
DEFAULT_NAME,
DOMAIN,
SECTION_ADDITIONAL_SETTINGS,
)
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -476,6 +478,46 @@ async def test_migration_1_2(
)
@pytest.mark.usefixtures("recorder_mock")
async def test_migration_1_3(
hass: HomeAssistant,
sensor_entity_entry: er.RegistryEntry,
) -> None:
"""Test migration from v1.3 renames advanced_settings to additional_settings."""
history_stats_config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
CONF_NAME: DEFAULT_NAME,
CONF_ENTITY_ID: sensor_entity_entry.entity_id,
CONF_STATE: ["on"],
CONF_TYPE: "count",
CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}",
CONF_END: "{{ utcnow() }}",
"advanced_settings": {CONF_MIN_STATE_DURATION: {"seconds": 30}},
},
title="My history stats",
version=1,
minor_version=3,
)
history_stats_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(history_stats_config_entry.entry_id)
await hass.async_block_till_done()
assert history_stats_config_entry.state is ConfigEntryState.LOADED
assert "advanced_settings" not in history_stats_config_entry.options
assert history_stats_config_entry.options[SECTION_ADDITIONAL_SETTINGS] == {
CONF_MIN_STATE_DURATION: {"seconds": 30}
}
assert history_stats_config_entry.version == 1
assert (
history_stats_config_entry.minor_version
== HistoryStatsConfigFlowHandler.MINOR_VERSION
)
@pytest.mark.usefixtures("recorder_mock")
async def test_migration_from_future_version(
hass: HomeAssistant,
@@ -1131,7 +1131,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'block_hulu',
'unique_id': 'xyz12_block_hulu',
'unit_of_measurement': None,
})
@@ -246,9 +246,9 @@ async def test_subentry_unsupported_model(
)
await hass.async_block_till_done()
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -300,9 +300,9 @@ async def test_subentry_reasoning_effort_list(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -354,9 +354,9 @@ async def test_subentry_reasoning_summary_visibility(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -403,7 +403,7 @@ async def test_subentry_reasoning_summary_options(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
@@ -450,7 +450,7 @@ async def test_subentry_reasoning_summary_default_sanitized_on_model_switch(
CONF_LLM_HASS_API: ["assist"],
},
)
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
@@ -515,9 +515,9 @@ async def test_subentry_service_tier_list(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -561,9 +561,9 @@ async def test_subentry_unsupported_reasoning_effort(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -1144,9 +1144,9 @@ async def test_subentry_web_search_user_location(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -1292,12 +1292,12 @@ async def test_ai_task_subentry_not_loaded(
assert result.get("reason") == "entry_not_loaded"
async def test_creating_ai_task_subentry_advanced(
async def test_creating_ai_task_subentry_additional(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_init_component,
) -> None:
"""Test creating an AI task subentry with advanced settings."""
"""Test creating an AI task subentry with additional settings."""
result = await hass.config_entries.subentries.async_init(
(mock_config_entry.entry_id, "ai_task_data"),
context={"source": config_entries.SOURCE_USER},
@@ -1306,7 +1306,7 @@ async def test_creating_ai_task_subentry_advanced(
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "init"
# Go to advanced settings
# Go to additional settings
result2 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
@@ -1316,9 +1316,9 @@ async def test_creating_ai_task_subentry_advanced(
)
assert result2.get("type") is FlowResultType.FORM
assert result2.get("step_id") == "advanced"
assert result2.get("step_id") == "additional"
# Configure advanced settings
# Configure additional settings
result3 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
+25
View File
@@ -4,6 +4,7 @@ from collections.abc import AsyncGenerator, Generator
from unittest.mock import AsyncMock, MagicMock, patch
from pysmlight.exceptions import SmlightAuthError
from pysmlight.models import BleFeatures
from pysmlight.sse import sseClient
from pysmlight.web import ActionWrapper, CmdWrapper, Firmware, Info, Sensors
import pytest
@@ -26,6 +27,29 @@ MOCK_USERNAME = "test-user"
MOCK_PASSWORD = "test-pass"
@pytest.fixture(autouse=True)
def mock_bluetooth_scanner() -> Generator[MagicMock]:
"""Mock bluetooth scanner."""
with patch(
"homeassistant.components.smlight.bluetooth.async_register_scanner"
) as mock_register:
yield mock_register
@pytest.fixture(autouse=True)
def mock_connect_scanner() -> Generator[MagicMock]:
"""Mock bleak_smlight connect_scanner."""
with patch(
"homeassistant.components.smlight.bluetooth.connect_scanner"
) as mock_connect:
client_data = MagicMock()
client_data.scanner = MagicMock()
client_data.client = MagicMock()
client_data.client.start = AsyncMock()
mock_connect.return_value = client_data
yield mock_connect
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
@@ -127,6 +151,7 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]:
MOCK_ULTIMA = Info(
MAC="AA:BB:CC:DD:EE:FF",
model="SLZB-Ultima3",
ble=BleFeatures(ble_enabled=True, proxy_enabled=True),
)
@@ -0,0 +1,89 @@
"""Tests for the SMLIGHT Bluetooth platform."""
from unittest.mock import ANY, MagicMock
from pysmlight import Info
from pysmlight.models import BleFeatures
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_ultima_client")
async def test_bluetooth_scanner_lifecycle(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connect_scanner: MagicMock,
mock_bluetooth_scanner: MagicMock,
) -> None:
"""Test setting up and unloading SMLIGHT Bluetooth scanner (lifecycle)."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_connect_scanner.assert_called_once_with(
source=mock_config_entry.unique_id,
name=mock_config_entry.title,
host=mock_config_entry.data[CONF_HOST],
port=5050,
)
client_data = mock_connect_scanner.return_value
client_data.client.start.assert_called_once()
mock_bluetooth_scanner.assert_called_once_with(
hass,
client_data.scanner,
source_domain="smlight",
source_model="SLZB-Ultima3",
source_config_entry_id=mock_config_entry.entry_id,
source_device_id=ANY,
)
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
client_data.client.stop.assert_called_once()
async def test_bluetooth_not_started_for_disabled_settings(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connect_scanner: MagicMock,
mock_smlight_client: MagicMock,
) -> None:
"""Test that bluetooth scanner is not started for SLZB device with disabled settings."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info(
MAC="AA:BB:CC:DD:EE:FF",
model="SLZB-MR3U",
u_device=True,
ble=BleFeatures(proxy_enabled=False),
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_connect_scanner.assert_not_called()
@pytest.mark.usefixtures("mock_smlight_client")
async def test_bluetooth_not_started_for_classic_device(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connect_scanner: MagicMock,
) -> None:
"""Test that bluetooth scanner is not started for classic (non-U) devices."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_connect_scanner.assert_not_called()
+15 -3
View File
@@ -132,7 +132,11 @@ async def test_zeroconf_flow(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "confirm_discovery"
progress = hass.config_entries.flow.async_progress()
progress = [
flow
for flow in hass.config_entries.flow.async_progress()
if flow["handler"] == DOMAIN
]
assert len(progress) == 1
assert progress[0]["flow_id"] == result["flow_id"]
assert progress[0]["context"]["confirm_only"] is True
@@ -169,7 +173,11 @@ async def test_zeroconf_flow_auth(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "confirm_discovery"
progress = hass.config_entries.flow.async_progress()
progress = [
flow
for flow in hass.config_entries.flow.async_progress()
if flow["handler"] == DOMAIN
]
assert len(progress) == 1
assert progress[0]["flow_id"] == result["flow_id"]
assert progress[0]["context"]["confirm_only"] is True
@@ -181,7 +189,11 @@ async def test_zeroconf_flow_auth(
assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "auth"
progress2 = hass.config_entries.flow.async_progress()
progress2 = [
flow
for flow in hass.config_entries.flow.async_progress()
if flow["handler"] == DOMAIN
]
assert len(progress2) == 1
assert progress2[0]["flow_id"] == result["flow_id"]
+5 -1
View File
@@ -72,7 +72,11 @@ async def test_async_setup_missing_credentials(
await setup_integration(hass, mock_config_entry_host)
progress = hass.config_entries.flow.async_progress()
progress = [
flow
for flow in hass.config_entries.flow.async_progress()
if flow["handler"] == DOMAIN and flow["context"].get("source") == "reauth"
]
assert len(progress) == 1
assert progress[0]["step_id"] == "reauth_confirm"
assert progress[0]["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff"
+26
View File
@@ -1099,6 +1099,32 @@ FLOOR_LAMP_SERVICE_INFO = BluetoothServiceInfoBleak(
tx_power=-127,
)
CANDLE_WARMER_LAMP_SERVICE_INFO = BluetoothServiceInfoBleak(
name="Candle Warmer Lamp",
manufacturer_data={
2409: b"\x90\xe5\xb1h\xda\xaa\n\xb0 \x00",
},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b'\x00\x00\x00\x00\x11"\xb8'},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="Candle Warmer Lamp",
manufacturer_data={
2409: b"\x90\xe5\xb1h\xda\xaa\n\xb0 \x00",
},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b'\x00\x00\x00\x00\x11"\xb8'
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Candle Warmer Lamp"),
time=0,
connectable=True,
tx_power=-127,
)
RGBICWW_STRIP_LIGHT_SERVICE_INFO = BluetoothServiceInfoBleak(
name="RGBICWW Strip Light",
manufacturer_data={
+80
View File
@@ -27,6 +27,7 @@ from . import (
AIR_PURIFIER_TABLE_US_SERVICE_INFO,
AIR_PURIFIER_US_SERVICE_INFO,
BULB_SERVICE_INFO,
CANDLE_WARMER_LAMP_SERVICE_INFO,
CEILING_LIGHT_SERVICE_INFO,
FLOOR_LAMP_SERVICE_INFO,
PERMANENT_OUTDOOR_LIGHT_SERVICE_INFO,
@@ -132,6 +133,15 @@ FLOOR_LAMP_PARAMETERS = (
],
)
CANDLE_WARMER_LAMP_PARAMETERS = (
COMMON_PARAMETERS,
[
TURN_ON_PARAMETERS,
TURN_OFF_PARAMETERS,
SET_BRIGHTNESS_PARAMETERS,
],
)
AIR_PURIFIER_LIGHT_PARAMETERS = (
COMMON_PARAMETERS,
[
@@ -469,6 +479,76 @@ async def test_floor_lamp_services_exception(
)
@pytest.mark.parametrize(*CANDLE_WARMER_LAMP_PARAMETERS)
async def test_candle_warmer_lamp_services(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
service: str,
service_data: dict,
mock_method: str,
expected_args: Any,
) -> None:
"""Test all SwitchBot candle warmer lamp services."""
inject_bluetooth_service_info(hass, CANDLE_WARMER_LAMP_SERVICE_INFO)
entry = mock_entry_encrypted_factory(sensor_type="candle_warmer_lamp")
entry.add_to_hass(hass)
entity_id = "light.test_name"
mocked_instance = AsyncMock(return_value=True)
with patch.multiple(
"homeassistant.components.switchbot.light.switchbot.SwitchbotCandleWarmerLamp",
**{mock_method: mocked_instance},
update=AsyncMock(return_value=None),
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
LIGHT_DOMAIN,
service,
{**service_data, ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mocked_instance.assert_awaited_once_with(*expected_args)
@pytest.mark.parametrize(*CANDLE_WARMER_LAMP_PARAMETERS)
async def test_candle_warmer_lamp_services_exception(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
service: str,
service_data: dict,
mock_method: str,
expected_args: Any,
) -> None:
"""Test all SwitchBot candle warmer lamp services with exception."""
inject_bluetooth_service_info(hass, CANDLE_WARMER_LAMP_SERVICE_INFO)
entry = mock_entry_encrypted_factory(sensor_type="candle_warmer_lamp")
entry.add_to_hass(hass)
entity_id = "light.test_name"
exception = SwitchbotOperationError("Operation failed")
error_message = "An error occurred while performing the action: Operation failed"
with patch.multiple(
"homeassistant.components.switchbot.light.switchbot.SwitchbotCandleWarmerLamp",
**{mock_method: AsyncMock(side_effect=exception)},
update=AsyncMock(return_value=None),
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
with pytest.raises(HomeAssistantError, match=error_message):
await hass.services.async_call(
LIGHT_DOMAIN,
service,
{**service_data, ATTR_ENTITY_ID: entity_id},
blocking=True,
)
@pytest.mark.parametrize(
("service_info", "sensor_type"),
[
@@ -20,7 +20,7 @@ from homeassistant.components.telegram_bot.const import (
PARSER_PLAIN_TEXT,
PLATFORM_BROADCAST,
PLATFORM_WEBHOOKS,
SECTION_ADVANCED_SETTINGS,
SECTION_ADDITIONAL_SETTINGS,
SUBENTRY_TYPE_ALLOWED_CHAT_IDS,
)
from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL
@@ -100,7 +100,7 @@ async def test_reconfigure_flow_broadcast(
result["flow_id"],
{
CONF_PLATFORM: PLATFORM_BROADCAST,
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_PROXY_URL: "invalid",
},
},
@@ -117,7 +117,7 @@ async def test_reconfigure_flow_broadcast(
result["flow_id"],
{
CONF_PLATFORM: PLATFORM_BROADCAST,
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_PROXY_URL: "https://test",
},
},
@@ -155,7 +155,7 @@ async def test_reconfigure_flow_webhooks(
result["flow_id"],
{
CONF_PLATFORM: PLATFORM_WEBHOOKS,
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_API_ENDPOINT: DEFAULT_API_ENDPOINT,
CONF_PROXY_URL: "https://test",
},
@@ -271,7 +271,7 @@ async def test_reconfigure_flow_logout_failed(
result["flow_id"],
{
CONF_PLATFORM: PLATFORM_BROADCAST,
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_API_ENDPOINT: "http://mock1",
},
},
@@ -289,7 +289,7 @@ async def test_reconfigure_flow_logout_failed(
result["flow_id"],
{
CONF_PLATFORM: PLATFORM_BROADCAST,
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_API_ENDPOINT: "http://mock2",
},
},
@@ -327,7 +327,7 @@ async def test_create_entry(
{
CONF_PLATFORM: PLATFORM_WEBHOOKS,
CONF_API_KEY: "mock api key",
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_PROXY_URL: "invalid",
},
},
@@ -350,7 +350,7 @@ async def test_create_entry(
{
CONF_PLATFORM: PLATFORM_WEBHOOKS,
CONF_API_KEY: "mock api key",
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_PROXY_URL: "https://proxy",
},
},
@@ -374,7 +374,7 @@ async def test_create_entry(
{
CONF_PLATFORM: PLATFORM_WEBHOOKS,
CONF_API_KEY: "mock api key",
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_PROXY_URL: "https://proxy",
},
},
@@ -446,7 +446,7 @@ async def test_create_webhook_entry(
{
CONF_PLATFORM: PLATFORM_WEBHOOKS,
CONF_API_KEY: "mock api key",
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_API_ENDPOINT: api_endpoint,
},
},
@@ -774,7 +774,7 @@ async def test_duplicate_entry(hass: HomeAssistant) -> None:
data = {
CONF_PLATFORM: PLATFORM_BROADCAST,
CONF_API_KEY: "mock api key",
SECTION_ADVANCED_SETTINGS: {
SECTION_ADDITIONAL_SETTINGS: {
CONF_API_ENDPOINT: "http://mock_api_endpoint",
},
}
@@ -67,7 +67,7 @@ from homeassistant.components.telegram_bot.const import (
PARSER_MD2,
PARSER_PLAIN_TEXT,
PLATFORM_BROADCAST,
SECTION_ADVANCED_SETTINGS,
SECTION_ADDITIONAL_SETTINGS,
SERVICE_ANSWER_CALLBACK_QUERY,
SERVICE_DELETE_MESSAGE,
SERVICE_EDIT_CAPTION,
@@ -1093,7 +1093,7 @@ async def test_send_message_no_chat_id_error(
data={
CONF_PLATFORM: PLATFORM_BROADCAST,
CONF_API_KEY: "mock api key",
SECTION_ADVANCED_SETTINGS: {},
SECTION_ADDITIONAL_SETTINGS: {},
},
options={ATTR_PARSER: PARSER_PLAIN_TEXT},
)
@@ -2918,6 +2918,32 @@ async def test_test_condition(
assert msg["result"]["result"] is False
async def test_test_condition_non_admin(
hass: HomeAssistant,
websocket_client: MockHAClientWebSocket,
hass_admin_user: MockUser,
) -> None:
"""Test testing a condition does not require admin."""
hass_admin_user.groups = []
hass.states.async_set("hello.world", "paulus")
await websocket_client.send_json_auto_id(
{
"type": "test_condition",
"condition": {
"condition": "state",
"entity_id": "hello.world",
"state": "paulus",
},
}
)
msg = await websocket_client.receive_json()
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
assert msg["result"]["result"] is True
@pytest.mark.parametrize(
("value_template", "expected_template_errors"),
[
@@ -3090,6 +3116,36 @@ async def test_subscribe_condition(
}
async def test_subscribe_condition_non_admin(
hass: HomeAssistant,
websocket_client: MockHAClientWebSocket,
hass_admin_user: MockUser,
) -> None:
"""Test subscribing to a condition does not require admin."""
hass_admin_user.groups = []
hass.states.async_set("hello.world", "paulus")
await websocket_client.send_json_auto_id(
{
"type": "subscribe_condition",
"condition": {
"condition": "state",
"entity_id": "hello.world",
"state": "paulus",
},
}
)
msg = await websocket_client.receive_json()
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
subscription_id = msg["id"]
msg = await websocket_client.receive_json()
assert msg == {"id": subscription_id, "type": "event", "event": {"result": True}}
@pytest.mark.parametrize(
("value_template", "expected_event"),
[