mirror of
https://github.com/home-assistant/core.git
synced 2026-06-30 10:35:54 +02:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d36c6f7f8 | |||
| e0ee456bfa | |||
| ebeb98dd83 | |||
| 49cff5f980 | |||
| c24186e571 | |||
| c9ea8baf61 | |||
| f21426dfa6 | |||
| a450999646 | |||
| 5b8ff19d8d | |||
| 25d505bcf3 | |||
| 91cb829881 | |||
| 4fdc4e6219 | |||
| ca7ae00c7e | |||
| ff460901b7 | |||
| 30512f08a8 | |||
| 9dd1a59d50 | |||
| 91aded4474 | |||
| 543eab3354 | |||
| 47b331a869 | |||
| 696dd45803 | |||
| f92239877f | |||
| 45ceb13937 | |||
| c5aeee8097 | |||
| bfc750b608 |
@@ -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,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -648,7 +648,7 @@ exclude_lines = [
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
required-version = ">=0.15.17"
|
||||
required-version = ">=0.15.18"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
|
||||
Generated
+5
-2
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user