mirror of
https://github.com/home-assistant/core.git
synced 2025-11-27 11:38:01 +00:00
Compare commits
31 Commits
mqtt-json-
...
hassfest-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c2fc12b6a | ||
|
|
54fd55a1c6 | ||
|
|
cc64fa639d | ||
|
|
84140ba414 | ||
|
|
d1726b84c8 | ||
|
|
4724ecbc38 | ||
|
|
85afe87b5e | ||
|
|
5960179844 | ||
|
|
9f8f7d2fde | ||
|
|
4c22264b13 | ||
|
|
baf4382724 | ||
|
|
8263ea4a4a | ||
|
|
8412581be4 | ||
|
|
207c848438 | ||
|
|
2b61601fd7 | ||
|
|
ee506e6c14 | ||
|
|
8003a49571 | ||
|
|
e438b11afb | ||
|
|
64ba43703c | ||
|
|
1d214ae120 | ||
|
|
68d987f866 | ||
|
|
299cc5e40c | ||
|
|
2c3456177e | ||
|
|
1ef90180cc | ||
|
|
4c1364dfd1 | ||
|
|
09a44a6a30 | ||
|
|
63303bdcde | ||
|
|
59cd24f54b | ||
|
|
82b9fead39 | ||
|
|
a879e36e9b | ||
|
|
b12c458188 |
@@ -19,7 +19,6 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers.chat_session import ChatSession, async_get_chat_session
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.util import RE_SANITIZE_FILENAME, slugify
|
||||
|
||||
from .const import (
|
||||
@@ -249,7 +248,7 @@ async def async_generate_image(
|
||||
if IMAGE_EXPIRY_TIME > 0:
|
||||
async_call_later(hass, IMAGE_EXPIRY_TIME, partial(_purge_image, filename))
|
||||
|
||||
service_result["url"] = get_url(hass) + async_sign_path(
|
||||
service_result["url"] = async_sign_path(
|
||||
hass,
|
||||
f"/api/{DOMAIN}/images/{filename}",
|
||||
timedelta(seconds=IMAGE_EXPIRY_TIME or 1800),
|
||||
|
||||
@@ -497,16 +497,18 @@ class BayesianBinarySensor(BinarySensorEntity):
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Observation for entity '%s' returned None, it will not be used"
|
||||
" for Bayesian updating"
|
||||
" for updating Bayesian sensor '%s'"
|
||||
),
|
||||
observation.entity_id,
|
||||
self.entity_id,
|
||||
)
|
||||
continue
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Observation for template entity returned None rather than a valid"
|
||||
" boolean, it will not be used for Bayesian updating"
|
||||
" boolean, it will not be used for updating Bayesian sensor '%s'"
|
||||
),
|
||||
self.entity_id,
|
||||
)
|
||||
# the prior has been updated and is now the posterior
|
||||
return prior
|
||||
|
||||
@@ -18,8 +18,10 @@ async def async_get_config_entry_diagnostics(
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
device_info = await coordinator.client.get_system_info()
|
||||
command_list = await coordinator.client.get_command_list()
|
||||
|
||||
return {
|
||||
"remote_command_list": command_list,
|
||||
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
||||
"device_info": async_redact_data(device_info, TO_REDACT),
|
||||
}
|
||||
|
||||
@@ -2,28 +2,40 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from brother import Brother, SnmpError
|
||||
|
||||
from homeassistant.components.snmp import async_get_snmp_engine
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE, Platform
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import (
|
||||
CONF_COMMUNITY,
|
||||
DEFAULT_COMMUNITY,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from .coordinator import BrotherConfigEntry, BrotherDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool:
|
||||
"""Set up Brother from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
port = entry.data[SECTION_ADVANCED_SETTINGS][CONF_PORT]
|
||||
community = entry.data[SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY]
|
||||
printer_type = entry.data[CONF_TYPE]
|
||||
|
||||
snmp_engine = await async_get_snmp_engine(hass)
|
||||
try:
|
||||
brother = await Brother.create(
|
||||
host, printer_type=printer_type, snmp_engine=snmp_engine
|
||||
host, port, community, printer_type=printer_type, snmp_engine=snmp_engine
|
||||
)
|
||||
except (ConnectionError, SnmpError, TimeoutError) as error:
|
||||
raise ConfigEntryNotReady(
|
||||
@@ -48,3 +60,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> b
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool:
|
||||
"""Migrate an old entry."""
|
||||
if entry.version == 1 and entry.minor_version < 2:
|
||||
new_data = entry.data.copy()
|
||||
new_data[SECTION_ADVANCED_SETTINGS] = {
|
||||
CONF_PORT: DEFAULT_PORT,
|
||||
CONF_COMMUNITY: DEFAULT_COMMUNITY,
|
||||
}
|
||||
hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
|
||||
|
||||
_LOGGER.info(
|
||||
"Migration to configuration version %s.%s successful",
|
||||
entry.version,
|
||||
entry.minor_version,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -9,21 +9,65 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.snmp import async_get_snmp_engine
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import section
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.util.network import is_host_valid
|
||||
|
||||
from .const import DOMAIN, PRINTER_TYPES
|
||||
from .const import (
|
||||
CONF_COMMUNITY,
|
||||
DEFAULT_COMMUNITY,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
PRINTER_TYPES,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Optional(CONF_TYPE, default="laser"): vol.In(PRINTER_TYPES),
|
||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Required(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): str,
|
||||
},
|
||||
),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}
|
||||
)
|
||||
ZEROCONF_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_TYPE, default="laser"): vol.In(PRINTER_TYPES),
|
||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Required(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): str,
|
||||
},
|
||||
),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}
|
||||
)
|
||||
RECONFIGURE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Required(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): str,
|
||||
},
|
||||
),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}
|
||||
)
|
||||
RECONFIGURE_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
||||
|
||||
|
||||
async def validate_input(
|
||||
@@ -35,7 +79,12 @@ async def validate_input(
|
||||
|
||||
snmp_engine = await async_get_snmp_engine(hass)
|
||||
|
||||
brother = await Brother.create(user_input[CONF_HOST], snmp_engine=snmp_engine)
|
||||
brother = await Brother.create(
|
||||
user_input[CONF_HOST],
|
||||
user_input[SECTION_ADVANCED_SETTINGS][CONF_PORT],
|
||||
user_input[SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY],
|
||||
snmp_engine=snmp_engine,
|
||||
)
|
||||
await brother.async_update()
|
||||
|
||||
if expected_mac is not None and brother.serial.lower() != expected_mac:
|
||||
@@ -48,6 +97,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Brother Printer."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
@@ -126,13 +176,11 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title = f"{self.brother.model} {self.brother.serial}"
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data={CONF_HOST: self.host, CONF_TYPE: user_input[CONF_TYPE]},
|
||||
data={CONF_HOST: self.host, **user_input},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Optional(CONF_TYPE, default="laser"): vol.In(PRINTER_TYPES)}
|
||||
),
|
||||
data_schema=ZEROCONF_SCHEMA,
|
||||
description_placeholders={
|
||||
"serial_number": self.brother.serial,
|
||||
"model": self.brother.model,
|
||||
@@ -160,7 +208,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data_updates={CONF_HOST: user_input[CONF_HOST]},
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -10,3 +10,10 @@ DOMAIN: Final = "brother"
|
||||
PRINTER_TYPES: Final = ["laser", "ink"]
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
SECTION_ADVANCED_SETTINGS = "advanced_settings"
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
|
||||
DEFAULT_COMMUNITY = "public"
|
||||
DEFAULT_PORT = 161
|
||||
|
||||
@@ -8,7 +8,21 @@
|
||||
"type": "Type of the printer"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Brother printer to control."
|
||||
"host": "The hostname or IP address of the Brother printer to control.",
|
||||
"type": "Brother printer type: ink or laser."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
"name": "Advanced settings",
|
||||
"data": {
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"community": "SNMP Community"
|
||||
},
|
||||
"data_description": {
|
||||
"port": "The SNMP port of the Brother printer.",
|
||||
"community": "A simple password for devices to communicate to each other."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
@@ -16,6 +30,22 @@
|
||||
"title": "Discovered Brother Printer",
|
||||
"data": {
|
||||
"type": "[%key:component::brother::config::step::user::data::type%]"
|
||||
},
|
||||
"data_description": {
|
||||
"type": "[%key:component::brother::config::step::user::data_description::type%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
"name": "Advanced settings",
|
||||
"data": {
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"community": "SNMP Community"
|
||||
},
|
||||
"data_description": {
|
||||
"port": "The SNMP port of the Brother printer.",
|
||||
"community": "A simple password for devices to communicate to each other."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
@@ -25,6 +55,19 @@
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::brother::config::step::user::data_description::host%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
"name": "Advanced settings",
|
||||
"data": {
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"community": "SNMP Community"
|
||||
},
|
||||
"data_description": {
|
||||
"port": "The SNMP port of the Brother printer.",
|
||||
"community": "A simple password for devices to communicate to each other."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.1.0"],
|
||||
"requirements": ["hass-nabucasa==1.1.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/derivative",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "calculated"
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
"name": "Max connection upload throughput"
|
||||
},
|
||||
"cpu_temperature": {
|
||||
"name": "CPU Temperature"
|
||||
"name": "CPU temperature"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"dependencies": ["sensor", "switch"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic_thermostat",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_polling"
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import codecs
|
||||
from collections.abc import AsyncGenerator, AsyncIterator, Callable
|
||||
from dataclasses import replace
|
||||
from dataclasses import dataclass, replace
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from google.genai import Client
|
||||
from google.genai.errors import APIError, ClientError
|
||||
@@ -27,6 +28,7 @@ from google.genai.types import (
|
||||
PartUnionDict,
|
||||
SafetySetting,
|
||||
Schema,
|
||||
ThinkingConfig,
|
||||
Tool,
|
||||
ToolListUnion,
|
||||
)
|
||||
@@ -201,6 +203,30 @@ def _create_google_tool_response_content(
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PartDetails:
|
||||
"""Additional data for a content part."""
|
||||
|
||||
part_type: Literal["text", "thought", "function_call"]
|
||||
"""The part type for which this data is relevant for."""
|
||||
|
||||
index: int
|
||||
"""Start position or number of the tool."""
|
||||
|
||||
length: int = 0
|
||||
"""Length of the relevant data."""
|
||||
|
||||
thought_signature: str | None = None
|
||||
"""Base64 encoded thought signature, if available."""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ContentDetails:
|
||||
"""Native data for AssistantContent."""
|
||||
|
||||
part_details: list[PartDetails]
|
||||
|
||||
|
||||
def _convert_content(
|
||||
content: (
|
||||
conversation.UserContent
|
||||
@@ -209,32 +235,91 @@ def _convert_content(
|
||||
),
|
||||
) -> Content:
|
||||
"""Convert HA content to Google content."""
|
||||
if content.role != "assistant" or not content.tool_calls:
|
||||
role = "model" if content.role == "assistant" else content.role
|
||||
if content.role != "assistant":
|
||||
return Content(
|
||||
role=role,
|
||||
parts=[
|
||||
Part.from_text(text=content.content if content.content else ""),
|
||||
],
|
||||
role=content.role,
|
||||
parts=[Part.from_text(text=content.content if content.content else "")],
|
||||
)
|
||||
|
||||
# Handle the Assistant content with tool calls.
|
||||
assert type(content) is conversation.AssistantContent
|
||||
parts: list[Part] = []
|
||||
part_details: list[PartDetails] = (
|
||||
content.native.part_details
|
||||
if isinstance(content.native, ContentDetails)
|
||||
else []
|
||||
)
|
||||
details: PartDetails | None = None
|
||||
|
||||
if content.content:
|
||||
parts.append(Part.from_text(text=content.content))
|
||||
index = 0
|
||||
for details in part_details:
|
||||
if details.part_type == "text":
|
||||
if index < details.index:
|
||||
parts.append(
|
||||
Part.from_text(text=content.content[index : details.index])
|
||||
)
|
||||
index = details.index
|
||||
parts.append(
|
||||
Part.from_text(
|
||||
text=content.content[index : index + details.length],
|
||||
)
|
||||
)
|
||||
if details.thought_signature:
|
||||
parts[-1].thought_signature = base64.b64decode(
|
||||
details.thought_signature
|
||||
)
|
||||
index += details.length
|
||||
if index < len(content.content):
|
||||
parts.append(Part.from_text(text=content.content[index:]))
|
||||
|
||||
if content.thinking_content:
|
||||
index = 0
|
||||
for details in part_details:
|
||||
if details.part_type == "thought":
|
||||
if index < details.index:
|
||||
parts.append(
|
||||
Part.from_text(
|
||||
text=content.thinking_content[index : details.index]
|
||||
)
|
||||
)
|
||||
parts[-1].thought = True
|
||||
index = details.index
|
||||
parts.append(
|
||||
Part.from_text(
|
||||
text=content.thinking_content[index : index + details.length],
|
||||
)
|
||||
)
|
||||
parts[-1].thought = True
|
||||
if details.thought_signature:
|
||||
parts[-1].thought_signature = base64.b64decode(
|
||||
details.thought_signature
|
||||
)
|
||||
index += details.length
|
||||
if index < len(content.thinking_content):
|
||||
parts.append(Part.from_text(text=content.thinking_content[index:]))
|
||||
parts[-1].thought = True
|
||||
|
||||
if content.tool_calls:
|
||||
parts.extend(
|
||||
[
|
||||
for index, tool_call in enumerate(content.tool_calls):
|
||||
parts.append(
|
||||
Part.from_function_call(
|
||||
name=tool_call.tool_name,
|
||||
args=_escape_decode(tool_call.tool_args),
|
||||
)
|
||||
for tool_call in content.tool_calls
|
||||
]
|
||||
)
|
||||
)
|
||||
if details := next(
|
||||
(
|
||||
d
|
||||
for d in part_details
|
||||
if d.part_type == "function_call" and d.index == index
|
||||
),
|
||||
None,
|
||||
):
|
||||
if details.thought_signature:
|
||||
parts[-1].thought_signature = base64.b64decode(
|
||||
details.thought_signature
|
||||
)
|
||||
|
||||
return Content(role="model", parts=parts)
|
||||
|
||||
@@ -243,14 +328,20 @@ async def _transform_stream(
|
||||
result: AsyncIterator[GenerateContentResponse],
|
||||
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
||||
new_message = True
|
||||
part_details: list[PartDetails] = []
|
||||
try:
|
||||
async for response in result:
|
||||
LOGGER.debug("Received response chunk: %s", response)
|
||||
chunk: conversation.AssistantContentDeltaDict = {}
|
||||
|
||||
if new_message:
|
||||
chunk["role"] = "assistant"
|
||||
if part_details:
|
||||
yield {"native": ContentDetails(part_details=part_details)}
|
||||
part_details = []
|
||||
yield {"role": "assistant"}
|
||||
new_message = False
|
||||
content_index = 0
|
||||
thinking_content_index = 0
|
||||
tool_call_index = 0
|
||||
|
||||
# According to the API docs, this would mean no candidate is returned, so we can safely throw an error here.
|
||||
if response.prompt_feedback or not response.candidates:
|
||||
@@ -284,23 +375,62 @@ async def _transform_stream(
|
||||
else []
|
||||
)
|
||||
|
||||
content = "".join([part.text for part in response_parts if part.text])
|
||||
tool_calls = []
|
||||
for part in response_parts:
|
||||
if not part.function_call:
|
||||
continue
|
||||
tool_call = part.function_call
|
||||
tool_name = tool_call.name if tool_call.name else ""
|
||||
tool_args = _escape_decode(tool_call.args)
|
||||
tool_calls.append(
|
||||
llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
|
||||
)
|
||||
chunk: conversation.AssistantContentDeltaDict = {}
|
||||
|
||||
if tool_calls:
|
||||
chunk["tool_calls"] = tool_calls
|
||||
if part.text:
|
||||
if part.thought:
|
||||
chunk["thinking_content"] = part.text
|
||||
if part.thought_signature:
|
||||
part_details.append(
|
||||
PartDetails(
|
||||
part_type="thought",
|
||||
index=thinking_content_index,
|
||||
length=len(part.text),
|
||||
thought_signature=base64.b64encode(
|
||||
part.thought_signature
|
||||
).decode("utf-8"),
|
||||
)
|
||||
)
|
||||
thinking_content_index += len(part.text)
|
||||
else:
|
||||
chunk["content"] = part.text
|
||||
if part.thought_signature:
|
||||
part_details.append(
|
||||
PartDetails(
|
||||
part_type="text",
|
||||
index=content_index,
|
||||
length=len(part.text),
|
||||
thought_signature=base64.b64encode(
|
||||
part.thought_signature
|
||||
).decode("utf-8"),
|
||||
)
|
||||
)
|
||||
content_index += len(part.text)
|
||||
|
||||
if part.function_call:
|
||||
tool_call = part.function_call
|
||||
tool_name = tool_call.name if tool_call.name else ""
|
||||
tool_args = _escape_decode(tool_call.args)
|
||||
chunk["tool_calls"] = [
|
||||
llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
|
||||
]
|
||||
if part.thought_signature:
|
||||
part_details.append(
|
||||
PartDetails(
|
||||
part_type="function_call",
|
||||
index=tool_call_index,
|
||||
thought_signature=base64.b64encode(
|
||||
part.thought_signature
|
||||
).decode("utf-8"),
|
||||
)
|
||||
)
|
||||
|
||||
yield chunk
|
||||
|
||||
if part_details:
|
||||
yield {"native": ContentDetails(part_details=part_details)}
|
||||
|
||||
chunk["content"] = content
|
||||
yield chunk
|
||||
except (
|
||||
APIError,
|
||||
ValueError,
|
||||
@@ -522,6 +652,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
),
|
||||
),
|
||||
],
|
||||
thinking_config=ThinkingConfig(include_thoughts=True),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ def ws_expose_entity(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Expose an entity to an assistant."""
|
||||
entity_ids: str = msg["entity_ids"]
|
||||
entity_ids: list[str] = msg["entity_ids"]
|
||||
|
||||
if blocked := next(
|
||||
(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyHomee.const import AttributeChangedBy, AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import get_name_for_enum
|
||||
from .helpers import get_name_for_enum, setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -60,18 +60,29 @@ def get_supported_features(
|
||||
return supported_features
|
||||
|
||||
|
||||
async def add_alarm_control_panel_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee alarm control panel entities."""
|
||||
async_add_entities(
|
||||
HomeeAlarmPanel(attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type])
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in ALARM_DESCRIPTIONS and attribute.editable
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the alarm control panel component."""
|
||||
"""Add the homee platform for the alarm control panel component."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeAlarmPanel(attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type])
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in ALARM_DESCRIPTIONS and attribute.editable
|
||||
await setup_homee_platform(
|
||||
add_alarm_control_panel_entities, async_add_entities, config_entry
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The Homee binary sensor platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -152,23 +153,34 @@ BINARY_SENSOR_DESCRIPTIONS: dict[AttributeType, BinarySensorEntityDescription] =
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
async def add_binary_sensor_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add the Homee platform for the binary sensor component."""
|
||||
|
||||
async_add_devices(
|
||||
"""Add homee binary sensor entities."""
|
||||
async_add_entities(
|
||||
HomeeBinarySensor(
|
||||
attribute, config_entry, BINARY_SENSOR_DESCRIPTIONS[attribute.type]
|
||||
)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in BINARY_SENSOR_DESCRIPTIONS and not attribute.editable
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the homee platform for the binary sensor component."""
|
||||
|
||||
await setup_homee_platform(
|
||||
add_binary_sensor_entities, async_add_entities, config_entry
|
||||
)
|
||||
|
||||
|
||||
class HomeeBinarySensor(HomeeEntity, BinarySensorEntity):
|
||||
"""Representation of a Homee binary sensor."""
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The homee button platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
@@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -39,19 +40,28 @@ BUTTON_DESCRIPTIONS: dict[AttributeType, ButtonEntityDescription] = {
|
||||
}
|
||||
|
||||
|
||||
async def add_button_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee button entities."""
|
||||
async_add_entities(
|
||||
HomeeButton(attribute, config_entry, BUTTON_DESCRIPTIONS[attribute.type])
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in BUTTON_DESCRIPTIONS and attribute.editable
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the button component."""
|
||||
"""Add the homee platform for the button component."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeButton(attribute, config_entry, BUTTON_DESCRIPTIONS[attribute.type])
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in BUTTON_DESCRIPTIONS and attribute.editable
|
||||
)
|
||||
await setup_homee_platform(add_button_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeButton(HomeeEntity, ButtonEntity):
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from . import HomeeConfigEntry
|
||||
from .const import CLIMATE_PROFILES, DOMAIN, HOMEE_UNIT_TO_HA_UNIT, PRESET_MANUAL
|
||||
from .entity import HomeeNodeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -31,18 +32,27 @@ ROOM_THERMOSTATS = {
|
||||
}
|
||||
|
||||
|
||||
async def add_climate_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee climate entities."""
|
||||
async_add_entities(
|
||||
HomeeClimate(node, config_entry)
|
||||
for node in nodes
|
||||
if node.profile in CLIMATE_PROFILES
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the climate component."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeClimate(node, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
if node.profile in CLIMATE_PROFILES
|
||||
)
|
||||
await setup_homee_platform(add_climate_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeNodeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,18 +78,25 @@ def get_device_class(node: HomeeNode) -> CoverDeviceClass | None:
|
||||
return COVER_DEVICE_PROFILES.get(node.profile)
|
||||
|
||||
|
||||
async def add_cover_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee cover entities."""
|
||||
async_add_entities(
|
||||
HomeeCover(node, config_entry) for node in nodes if is_cover_node(node)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the homee platform for the cover integration."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeCover(node, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
if is_cover_node(node)
|
||||
)
|
||||
await setup_homee_platform(add_cover_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
def is_cover_node(node: HomeeNode) -> bool:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The homee event platform."""
|
||||
|
||||
from pyHomee.const import AttributeType, NodeProfile
|
||||
from pyHomee.model import HomeeAttribute
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.event import (
|
||||
EventDeviceClass,
|
||||
@@ -13,6 +13,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -49,6 +50,22 @@ EVENT_DESCRIPTIONS: dict[AttributeType, EventEntityDescription] = {
|
||||
}
|
||||
|
||||
|
||||
async def add_event_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee event entities."""
|
||||
async_add_entities(
|
||||
HomeeEvent(attribute, config_entry, EVENT_DESCRIPTIONS[attribute.type])
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in EVENT_DESCRIPTIONS
|
||||
and node.profile in REMOTE_PROFILES
|
||||
and not attribute.editable
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
@@ -56,14 +73,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add event entities for homee."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeEvent(attribute, config_entry, EVENT_DESCRIPTIONS[attribute.type])
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in EVENT_DESCRIPTIONS
|
||||
and node.profile in REMOTE_PROFILES
|
||||
and not attribute.editable
|
||||
)
|
||||
await setup_homee_platform(add_event_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeEvent(HomeeEntity, EventEntity):
|
||||
|
||||
@@ -19,22 +19,32 @@ from homeassistant.util.scaling import int_states_in_range
|
||||
from . import HomeeConfigEntry
|
||||
from .const import DOMAIN, PRESET_AUTO, PRESET_MANUAL, PRESET_SUMMER
|
||||
from .entity import HomeeNodeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def add_fan_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee fan entities."""
|
||||
async_add_entities(
|
||||
HomeeFan(node, config_entry)
|
||||
for node in nodes
|
||||
if node.profile == NodeProfile.VENTILATION_CONTROL
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Homee fan platform."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeFan(node, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
if node.profile == NodeProfile.VENTILATION_CONTROL
|
||||
)
|
||||
await setup_homee_platform(add_fan_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeFan(HomeeNodeEntity, FanEntity):
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
"""Helper functions for the homee custom component."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.model import HomeeNode
|
||||
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def setup_homee_platform(
|
||||
add_platform_entities: Callable[
|
||||
[HomeeConfigEntry, AddConfigEntryEntitiesCallback, list[HomeeNode]],
|
||||
Coroutine[Any, Any, None],
|
||||
],
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
config_entry: HomeeConfigEntry,
|
||||
) -> None:
|
||||
"""Set up a homee platform."""
|
||||
await add_platform_entities(
|
||||
config_entry, async_add_entities, config_entry.runtime_data.nodes
|
||||
)
|
||||
|
||||
async def add_device(node: HomeeNode, add: bool) -> None:
|
||||
"""Dynamically add entities."""
|
||||
if add:
|
||||
await add_platform_entities(config_entry, async_add_entities, [node])
|
||||
|
||||
config_entry.async_on_unload(
|
||||
config_entry.runtime_data.add_nodes_listener(add_device)
|
||||
)
|
||||
|
||||
|
||||
def get_name_for_enum(att_class: type[IntEnum], att_id: int) -> str | None:
|
||||
"""Return the enum item name for a given integer."""
|
||||
try:
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.util.color import (
|
||||
from . import HomeeConfigEntry
|
||||
from .const import LIGHT_PROFILES
|
||||
from .entity import HomeeNodeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
LIGHT_ATTRIBUTES = [
|
||||
AttributeType.COLOR,
|
||||
@@ -85,19 +86,28 @@ def decimal_to_rgb_list(color: float) -> list[int]:
|
||||
]
|
||||
|
||||
|
||||
async def add_light_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee light entities."""
|
||||
async_add_entities(
|
||||
HomeeLight(node, light, config_entry)
|
||||
for node in nodes
|
||||
for light in get_light_attribute_sets(node)
|
||||
if is_light_node(node)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the light entity."""
|
||||
"""Add the homee platform for the light entity."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeLight(node, light, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for light in get_light_attribute_sets(node)
|
||||
if is_light_node(node)
|
||||
)
|
||||
await setup_homee_platform(add_light_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeLight(HomeeNodeEntity, LightEntity):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeChangedBy, AttributeType
|
||||
from pyHomee.model import HomeeNode
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -10,24 +11,33 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import get_name_for_enum
|
||||
from .helpers import get_name_for_enum, setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def add_lock_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee lock entities."""
|
||||
async_add_entities(
|
||||
HomeeLock(attribute, config_entry)
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if (attribute.type == AttributeType.LOCK_STATE and attribute.editable)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the lock component."""
|
||||
"""Add the homee platform for the lock component."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeLock(attribute, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if (attribute.type == AttributeType.LOCK_STATE and attribute.editable)
|
||||
)
|
||||
await setup_homee_platform(add_lock_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeLock(HomeeEntity, LockEntity):
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
@@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from . import HomeeConfigEntry
|
||||
from .const import HOMEE_UNIT_TO_HA_UNIT
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -136,19 +137,28 @@ NUMBER_DESCRIPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
async def add_number_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee number entities."""
|
||||
async_add_entities(
|
||||
HomeeNumber(attribute, config_entry, NUMBER_DESCRIPTIONS[attribute.type])
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in NUMBER_DESCRIPTIONS and attribute.data != "fixed_value"
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the number component."""
|
||||
"""Add the homee platform for the number component."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeNumber(attribute, config_entry, NUMBER_DESCRIPTIONS[attribute.type])
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in NUMBER_DESCRIPTIONS and attribute.data != "fixed_value"
|
||||
)
|
||||
await setup_homee_platform(add_number_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeNumber(HomeeEntity, NumberEntity):
|
||||
|
||||
@@ -54,7 +54,7 @@ rules:
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The Homee select platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -10,6 +10,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -27,19 +28,28 @@ SELECT_DESCRIPTIONS: dict[AttributeType, SelectEntityDescription] = {
|
||||
}
|
||||
|
||||
|
||||
async def add_select_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee select entities."""
|
||||
async_add_entities(
|
||||
HomeeSelect(attribute, config_entry, SELECT_DESCRIPTIONS[attribute.type])
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in SELECT_DESCRIPTIONS and attribute.editable
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the select component."""
|
||||
"""Add the homee platform for the select component."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeSelect(attribute, config_entry, SELECT_DESCRIPTIONS[attribute.type])
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in SELECT_DESCRIPTIONS and attribute.editable
|
||||
)
|
||||
await setup_homee_platform(add_select_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeSelect(HomeeEntity, SelectEntity):
|
||||
|
||||
@@ -35,7 +35,7 @@ from .const import (
|
||||
WINDOW_MAP_REVERSED,
|
||||
)
|
||||
from .entity import HomeeEntity, HomeeNodeEntity
|
||||
from .helpers import get_name_for_enum
|
||||
from .helpers import get_name_for_enum, setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -304,16 +304,16 @@ def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the homee platform for the sensor components."""
|
||||
ent_reg = er.async_get(hass)
|
||||
devices: list[HomeeSensor | HomeeNodeSensor] = []
|
||||
|
||||
def add_deprecated_entity(
|
||||
attribute: HomeeAttribute, description: HomeeSensorEntityDescription
|
||||
) -> None:
|
||||
) -> list[HomeeSensor]:
|
||||
"""Add deprecated entities."""
|
||||
deprecated_entities: list[HomeeSensor] = []
|
||||
entity_uid = f"{config_entry.runtime_data.settings.uid}-{attribute.node_id}-{attribute.id}"
|
||||
if entity_id := ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, entity_uid):
|
||||
entity_entry = ent_reg.async_get(entity_id)
|
||||
@@ -325,7 +325,9 @@ async def async_setup_entry(
|
||||
f"deprecated_entity_{entity_uid}",
|
||||
)
|
||||
elif entity_entry:
|
||||
devices.append(HomeeSensor(attribute, config_entry, description))
|
||||
deprecated_entities.append(
|
||||
HomeeSensor(attribute, config_entry, description)
|
||||
)
|
||||
if entity_used_in(hass, entity_id):
|
||||
async_create_issue(
|
||||
hass,
|
||||
@@ -342,27 +344,42 @@ async def async_setup_entry(
|
||||
"entity": entity_id,
|
||||
},
|
||||
)
|
||||
return deprecated_entities
|
||||
|
||||
for node in config_entry.runtime_data.nodes:
|
||||
# Node properties that are sensors.
|
||||
devices.extend(
|
||||
HomeeNodeSensor(node, config_entry, description)
|
||||
for description in NODE_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
async def add_sensor_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee sensor entities."""
|
||||
entities: list[HomeeSensor | HomeeNodeSensor] = []
|
||||
|
||||
# Node attributes that are sensors.
|
||||
for attribute in node.attributes:
|
||||
if attribute.type == AttributeType.CURRENT_VALVE_POSITION:
|
||||
add_deprecated_entity(attribute, SENSOR_DESCRIPTIONS[attribute.type])
|
||||
elif attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable:
|
||||
devices.append(
|
||||
HomeeSensor(
|
||||
attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]
|
||||
for node in nodes:
|
||||
# Node properties that are sensors.
|
||||
entities.extend(
|
||||
HomeeNodeSensor(node, config_entry, description)
|
||||
for description in NODE_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
# Node attributes that are sensors.
|
||||
for attribute in node.attributes:
|
||||
if attribute.type == AttributeType.CURRENT_VALVE_POSITION:
|
||||
entities.extend(
|
||||
add_deprecated_entity(
|
||||
attribute, SENSOR_DESCRIPTIONS[attribute.type]
|
||||
)
|
||||
)
|
||||
elif attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable:
|
||||
entities.append(
|
||||
HomeeSensor(
|
||||
attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
await setup_homee_platform(add_sensor_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeSensor(HomeeEntity, SensorEntity):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeNode
|
||||
|
||||
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -10,23 +11,33 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def add_siren_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee siren entities."""
|
||||
async_add_entities(
|
||||
HomeeSiren(attribute, config_entry)
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type == AttributeType.SIREN
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add siren entities for homee."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeSiren(attribute, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type == AttributeType.SIREN
|
||||
)
|
||||
await setup_homee_platform(add_siren_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeSiren(HomeeEntity, SirenEntity):
|
||||
|
||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeType, NodeProfile
|
||||
from pyHomee.model import HomeeAttribute
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
@@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from . import HomeeConfigEntry
|
||||
from .const import CLIMATE_PROFILES, LIGHT_PROFILES
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -65,27 +66,35 @@ SWITCH_DESCRIPTIONS: dict[AttributeType, HomeeSwitchEntityDescription] = {
|
||||
}
|
||||
|
||||
|
||||
async def add_switch_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee switch entities."""
|
||||
async_add_entities(
|
||||
HomeeSwitch(attribute, config_entry, SWITCH_DESCRIPTIONS[attribute.type])
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if (attribute.type in SWITCH_DESCRIPTIONS and attribute.editable)
|
||||
and not (
|
||||
attribute.type == AttributeType.ON_OFF and node.profile in LIGHT_PROFILES
|
||||
)
|
||||
and not (
|
||||
attribute.type == AttributeType.MANUAL_OPERATION
|
||||
and node.profile in CLIMATE_PROFILES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform for the Homee component."""
|
||||
|
||||
for node in config_entry.runtime_data.nodes:
|
||||
async_add_devices(
|
||||
HomeeSwitch(attribute, config_entry, SWITCH_DESCRIPTIONS[attribute.type])
|
||||
for attribute in node.attributes
|
||||
if (attribute.type in SWITCH_DESCRIPTIONS and attribute.editable)
|
||||
and not (
|
||||
attribute.type == AttributeType.ON_OFF
|
||||
and node.profile in LIGHT_PROFILES
|
||||
)
|
||||
and not (
|
||||
attribute.type == AttributeType.MANUAL_OPERATION
|
||||
and node.profile in CLIMATE_PROFILES
|
||||
)
|
||||
)
|
||||
await setup_homee_platform(add_switch_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeSwitch(HomeeEntity, SwitchEntity):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The Homee valve platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.valve import (
|
||||
ValveDeviceClass,
|
||||
@@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -25,19 +26,28 @@ VALVE_DESCRIPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
async def add_valve_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee valve entities."""
|
||||
async_add_entities(
|
||||
HomeeValve(attribute, config_entry, VALVE_DESCRIPTIONS[attribute.type])
|
||||
for node in nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in VALVE_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the valve component."""
|
||||
"""Add the homee platform for the valve component."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeValve(attribute, config_entry, VALVE_DESCRIPTIONS[attribute.type])
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in VALVE_DESCRIPTIONS
|
||||
)
|
||||
await setup_homee_platform(add_valve_entities, async_add_entities, config_entry)
|
||||
|
||||
|
||||
class HomeeValve(HomeeEntity, ValveEntity):
|
||||
|
||||
@@ -20,7 +20,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up numbers for device."""
|
||||
if entry.runtime_data.data.device.supports_state():
|
||||
if entry.runtime_data.data.device.supports_led_brightness():
|
||||
async_add_entities([HWEnergyNumberEntity(entry.runtime_data)])
|
||||
|
||||
|
||||
|
||||
@@ -36,12 +36,13 @@ async def async_setup_entry(
|
||||
"""Set up Automower message event entities.
|
||||
|
||||
Entities are created dynamically based on messages received from the API,
|
||||
but only for mowers that support message events.
|
||||
but only for mowers that support message events after the WebSocket connection
|
||||
is ready.
|
||||
"""
|
||||
coordinator = config_entry.runtime_data
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
restored_mowers = {
|
||||
restored_mowers: set[str] = {
|
||||
entry.unique_id.removesuffix("_message")
|
||||
for entry in er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
@@ -49,14 +50,20 @@ async def async_setup_entry(
|
||||
if entry.domain == EVENT_DOMAIN
|
||||
}
|
||||
|
||||
async_add_entities(
|
||||
AutomowerMessageEventEntity(mower_id, coordinator)
|
||||
for mower_id in restored_mowers
|
||||
if mower_id in coordinator.data
|
||||
)
|
||||
@callback
|
||||
def _on_ws_ready() -> None:
|
||||
async_add_entities(
|
||||
AutomowerMessageEventEntity(mower_id, coordinator, websocket_alive=True)
|
||||
for mower_id in restored_mowers
|
||||
if mower_id in coordinator.data
|
||||
)
|
||||
coordinator.api.unregister_ws_ready_callback(_on_ws_ready)
|
||||
|
||||
coordinator.api.register_ws_ready_callback(_on_ws_ready)
|
||||
|
||||
@callback
|
||||
def _handle_message(msg: SingleMessageData) -> None:
|
||||
"""Add entity dynamically if a new mower sends messages."""
|
||||
if msg.id in restored_mowers:
|
||||
return
|
||||
|
||||
@@ -78,11 +85,17 @@ class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
|
||||
self,
|
||||
mower_id: str,
|
||||
coordinator: AutomowerDataUpdateCoordinator,
|
||||
*,
|
||||
websocket_alive: bool | None = None,
|
||||
) -> None:
|
||||
"""Initialize Automower message event entity."""
|
||||
super().__init__(mower_id, coordinator)
|
||||
self._attr_unique_id = f"{mower_id}_message"
|
||||
self.websocket_alive: bool = coordinator.websocket_alive
|
||||
self.websocket_alive: bool = (
|
||||
websocket_alive
|
||||
if websocket_alive is not None
|
||||
else coordinator.websocket_alive
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -52,8 +52,10 @@ async def async_get_config_entry_diagnostics(
|
||||
try:
|
||||
CONFIG_SCHEMA(raw_config)
|
||||
except vol.Invalid as ex:
|
||||
diag["configuration_error"] = str(ex)
|
||||
diag["yaml_configuration_error"] = str(ex)
|
||||
else:
|
||||
diag["configuration_error"] = None
|
||||
diag["yaml_configuration_error"] = None
|
||||
|
||||
diag["config_store"] = knx_module.config_store.data
|
||||
|
||||
return diag
|
||||
|
||||
@@ -285,13 +285,19 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
|
||||
group_address_switch_green_state=conf.get_state_and_passive(
|
||||
CONF_COLOR, CONF_GA_GREEN_SWITCH
|
||||
),
|
||||
group_address_brightness_green=conf.get_write(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_brightness_green=conf.get_write(
|
||||
CONF_COLOR, CONF_GA_GREEN_BRIGHTNESS
|
||||
),
|
||||
group_address_brightness_green_state=conf.get_state_and_passive(
|
||||
CONF_COLOR, CONF_GA_GREEN_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_blue=conf.get_write(CONF_GA_BLUE_SWITCH),
|
||||
group_address_switch_blue_state=conf.get_state_and_passive(CONF_GA_BLUE_SWITCH),
|
||||
group_address_brightness_blue=conf.get_write(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_switch_blue=conf.get_write(CONF_COLOR, CONF_GA_BLUE_SWITCH),
|
||||
group_address_switch_blue_state=conf.get_state_and_passive(
|
||||
CONF_COLOR, CONF_GA_BLUE_SWITCH
|
||||
),
|
||||
group_address_brightness_blue=conf.get_write(
|
||||
CONF_COLOR, CONF_GA_BLUE_BRIGHTNESS
|
||||
),
|
||||
group_address_brightness_blue_state=conf.get_state_and_passive(
|
||||
CONF_COLOR, CONF_GA_BLUE_BRIGHTNESS
|
||||
),
|
||||
|
||||
@@ -240,19 +240,19 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
"section_blue": KNXSectionFlat(),
|
||||
vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
vol.Optional(CONF_GA_BLUE_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
"section_white": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector(
|
||||
vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
"section_white": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_WHITE_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
},
|
||||
),
|
||||
GroupSelectOption(
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"food_dispensed_today": {
|
||||
"default": "mdi:counter"
|
||||
},
|
||||
"hopper_status": {
|
||||
"default": "mdi:filter",
|
||||
"state": {
|
||||
|
||||
@@ -163,6 +163,17 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = {
|
||||
),
|
||||
],
|
||||
FeederRobot: [
|
||||
RobotSensorEntityDescription[FeederRobot](
|
||||
key="food_dispensed_today",
|
||||
translation_key="food_dispensed_today",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
last_reset_fn=dt_util.start_of_local_day,
|
||||
value_fn=(
|
||||
lambda robot: (
|
||||
robot.get_food_dispensed_since(dt_util.start_of_local_day())
|
||||
)
|
||||
),
|
||||
),
|
||||
RobotSensorEntityDescription[FeederRobot](
|
||||
key="food_level",
|
||||
translation_key="food_level",
|
||||
@@ -181,6 +192,12 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = {
|
||||
)
|
||||
),
|
||||
),
|
||||
RobotSensorEntityDescription[FeederRobot](
|
||||
key="next_feeding",
|
||||
translation_key="next_feeding",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda robot: robot.next_feeding,
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"food_dispensed_today": {
|
||||
"name": "Food dispensed today",
|
||||
"unit_of_measurement": "cups"
|
||||
},
|
||||
"food_level": {
|
||||
"name": "Food level"
|
||||
},
|
||||
@@ -82,6 +86,9 @@
|
||||
"litter_level": {
|
||||
"name": "Litter level"
|
||||
},
|
||||
"next_feeding": {
|
||||
"name": "Next feeding"
|
||||
},
|
||||
"pet_weight": {
|
||||
"name": "Pet weight"
|
||||
},
|
||||
|
||||
@@ -83,7 +83,9 @@ class MetWeatherData:
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.get_default_time_zone()
|
||||
self.daily_forecast = self._weather_data.get_forecast(time_zone, False, 0)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(
|
||||
time_zone, True, range_stop=49
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -270,6 +270,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
@@ -307,6 +308,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
device_class=SensorDeviceClass.WATER,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
suggested_display_precision=0,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
@@ -618,6 +620,8 @@ async def async_setup_entry(
|
||||
"state_elapsed_time": MieleTimeSensor,
|
||||
"state_remaining_time": MieleTimeSensor,
|
||||
"state_start_time": MieleTimeSensor,
|
||||
"current_energy_consumption": MieleConsumptionSensor,
|
||||
"current_water_consumption": MieleConsumptionSensor,
|
||||
}.get(definition.description.key, MieleSensor)
|
||||
|
||||
def _is_entity_registered(unique_id: str) -> bool:
|
||||
@@ -924,3 +928,58 @@ class MieleTimeSensor(MieleRestorableSensor):
|
||||
# otherwise, cache value and return it
|
||||
else:
|
||||
self._last_value = current_value
|
||||
|
||||
|
||||
class MieleConsumptionSensor(MieleRestorableSensor):
|
||||
"""Representation of consumption sensors keeping state from cache."""
|
||||
|
||||
_is_reporting: bool = False
|
||||
|
||||
def _update_last_value(self) -> None:
|
||||
"""Update the last value of the sensor."""
|
||||
current_value = self.entity_description.value_fn(self.device)
|
||||
current_status = StateStatus(self.device.state_status)
|
||||
last_value = (
|
||||
float(cast(str, self._last_value))
|
||||
if self._last_value is not None and self._last_value != STATE_UNKNOWN
|
||||
else 0
|
||||
)
|
||||
|
||||
# force unknown when appliance is not able to report consumption
|
||||
if current_status in (
|
||||
StateStatus.ON,
|
||||
StateStatus.OFF,
|
||||
StateStatus.PROGRAMMED,
|
||||
StateStatus.WAITING_TO_START,
|
||||
StateStatus.IDLE,
|
||||
StateStatus.SERVICE,
|
||||
):
|
||||
self._is_reporting = False
|
||||
self._last_value = None
|
||||
|
||||
# appliance might report the last value for consumption of previous cycle and it will report 0
|
||||
# only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless
|
||||
# we already saw a valid value in this cycle from cache
|
||||
elif (
|
||||
current_status in (StateStatus.IN_USE, StateStatus.PAUSE)
|
||||
and not self._is_reporting
|
||||
and last_value > 0
|
||||
):
|
||||
self._last_value = current_value
|
||||
self._is_reporting = True
|
||||
|
||||
elif (
|
||||
current_status in (StateStatus.IN_USE, StateStatus.PAUSE)
|
||||
and not self._is_reporting
|
||||
and current_value is not None
|
||||
and cast(int, current_value) > 0
|
||||
):
|
||||
self._last_value = 0
|
||||
|
||||
# keep value when program ends
|
||||
elif current_status == StateStatus.PROGRAM_ENDED:
|
||||
pass
|
||||
|
||||
else:
|
||||
self._last_value = current_value
|
||||
self._is_reporting = True
|
||||
|
||||
@@ -1445,7 +1445,7 @@ class MqttEntity(
|
||||
},
|
||||
translation_key="deprecated_object_id",
|
||||
)
|
||||
else:
|
||||
elif CONF_DEFAULT_ENTITY_ID not in self._config:
|
||||
if CONF_ORIGIN in self._config:
|
||||
origin_name = self._config[CONF_ORIGIN][CONF_NAME]
|
||||
url = self._config[CONF_ORIGIN].get(CONF_URL)
|
||||
|
||||
@@ -43,6 +43,8 @@ LOG_NAME = "Tag"
|
||||
|
||||
TAG = "tag"
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
|
||||
@@ -146,20 +146,20 @@ class NtfyEventEntity(NtfyBaseEntity, EventEntity):
|
||||
)
|
||||
self._attr_available = False
|
||||
finally:
|
||||
if self._ws is None or self._ws.done():
|
||||
self._ws = self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
target=self.ntfy.subscribe(
|
||||
topics=[self.topic],
|
||||
callback=self._async_handle_event,
|
||||
title=self.subentry.data.get(CONF_TITLE),
|
||||
message=self.subentry.data.get(CONF_MESSAGE),
|
||||
priority=self.subentry.data.get(CONF_PRIORITY),
|
||||
tags=self.subentry.data.get(CONF_TAGS),
|
||||
),
|
||||
name="ntfy_websocket",
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
if self._ws is None or self._ws.done():
|
||||
self._ws = self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
target=self.ntfy.subscribe(
|
||||
topics=[self.topic],
|
||||
callback=self._async_handle_event,
|
||||
title=self.subentry.data.get(CONF_TITLE),
|
||||
message=self.subentry.data.get(CONF_MESSAGE),
|
||||
priority=self.subentry.data.get(CONF_PRIORITY),
|
||||
tags=self.subentry.data.get(CONF_TAGS),
|
||||
),
|
||||
name="ntfy_websocket",
|
||||
)
|
||||
await asyncio.sleep(RECONNECT_INTERVAL)
|
||||
|
||||
@property
|
||||
|
||||
@@ -35,7 +35,6 @@ from .const import ( # noqa: F401
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
ATTR_STEP_VALIDATION,
|
||||
ATTR_VALUE,
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
@@ -184,7 +183,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Representation of a Number entity."""
|
||||
|
||||
_entity_component_unrecorded_attributes = frozenset(
|
||||
{ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_STEP_VALIDATION, ATTR_MODE}
|
||||
{ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_MODE}
|
||||
)
|
||||
|
||||
entity_description: NumberEntityDescription
|
||||
|
||||
@@ -57,7 +57,6 @@ ATTR_VALUE = "value"
|
||||
ATTR_MIN = "min"
|
||||
ATTR_MAX = "max"
|
||||
ATTR_STEP = "step"
|
||||
ATTR_STEP_VALIDATION = "step_validation"
|
||||
|
||||
DEFAULT_MIN_VALUE = 0.0
|
||||
DEFAULT_MAX_VALUE = 100.0
|
||||
@@ -328,6 +327,7 @@ class NumberDeviceClass(StrEnum):
|
||||
- `Pa`, `hPa`, `kPa`
|
||||
- `inHg`
|
||||
- `psi`
|
||||
- `inH₂O`
|
||||
"""
|
||||
|
||||
REACTIVE_ENERGY = "reactive_energy"
|
||||
|
||||
@@ -181,7 +181,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_BEARING)
|
||||
|
||||
@property
|
||||
def visibility(self) -> float | str | None:
|
||||
def native_visibility(self) -> float | None:
|
||||
"""Return visibility."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_VISIBILITY_DISTANCE)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"protocol": "Protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"protocol": "Streaming protocol to use for the camera entities. RTSP supports 4K streams (h265 encoding) while RTMP and FLV do not. FLV is the least demanding on the camera."
|
||||
"protocol": "Streaming protocol to use for the camera entities. RTSP supports 4K streams (H.265 encoding) while RTMP and FLV do not. FLV is the least demanding on the camera."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,7 @@ class SensorDeviceClass(StrEnum):
|
||||
- `Pa`, `hPa`, `kPa`
|
||||
- `inHg`
|
||||
- `psi`
|
||||
- `inH₂O`
|
||||
"""
|
||||
|
||||
REACTIVE_ENERGY = "reactive_energy"
|
||||
|
||||
@@ -97,6 +97,7 @@ PLATFORMS_BY_TYPE = {
|
||||
SupportedModels.STRIP_LIGHT_3.value: [Platform.LIGHT, Platform.SENSOR],
|
||||
SupportedModels.RGBICWW_FLOOR_LAMP.value: [Platform.LIGHT, Platform.SENSOR],
|
||||
SupportedModels.RGBICWW_STRIP_LIGHT.value: [Platform.LIGHT, Platform.SENSOR],
|
||||
SupportedModels.PLUG_MINI_EU.value: [Platform.SWITCH, Platform.SENSOR],
|
||||
}
|
||||
CLASS_BY_DEVICE = {
|
||||
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
|
||||
@@ -127,6 +128,7 @@ CLASS_BY_DEVICE = {
|
||||
SupportedModels.STRIP_LIGHT_3.value: switchbot.SwitchbotStripLight3,
|
||||
SupportedModels.RGBICWW_FLOOR_LAMP.value: switchbot.SwitchbotRgbicLight,
|
||||
SupportedModels.RGBICWW_STRIP_LIGHT.value: switchbot.SwitchbotRgbicLight,
|
||||
SupportedModels.PLUG_MINI_EU.value: switchbot.SwitchbotRelaySwitch,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,12 +11,15 @@ from switchbot import (
|
||||
SwitchbotApiError,
|
||||
SwitchbotAuthenticationError,
|
||||
SwitchbotModel,
|
||||
fetch_cloud_devices,
|
||||
parse_advertisement_data,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfoBleak,
|
||||
async_current_scanners,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
@@ -87,6 +90,8 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Initialize the config flow."""
|
||||
self._discovered_adv: SwitchBotAdvertisement | None = None
|
||||
self._discovered_advs: dict[str, SwitchBotAdvertisement] = {}
|
||||
self._cloud_username: str | None = None
|
||||
self._cloud_password: str | None = None
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
@@ -176,9 +181,17 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the SwitchBot API auth step."""
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
assert self._discovered_adv is not None
|
||||
description_placeholders = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
# If we have saved credentials from cloud login, try them first
|
||||
if user_input is None and self._cloud_username and self._cloud_password:
|
||||
user_input = {
|
||||
CONF_USERNAME: self._cloud_username,
|
||||
CONF_PASSWORD: self._cloud_password,
|
||||
}
|
||||
|
||||
if user_input is not None:
|
||||
model: SwitchbotModel = self._discovered_adv.data["modelName"]
|
||||
cls = ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS[model]
|
||||
@@ -200,6 +213,9 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
||||
errors = {"base": "auth_failed"}
|
||||
description_placeholders = {"error_detail": str(ex)}
|
||||
# Clear saved credentials if auth failed
|
||||
self._cloud_username = None
|
||||
self._cloud_password = None
|
||||
else:
|
||||
return await self.async_step_encrypted_key(key_details)
|
||||
|
||||
@@ -239,7 +255,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the encryption key step."""
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
assert self._discovered_adv is not None
|
||||
if user_input is not None:
|
||||
model: SwitchbotModel = self._discovered_adv.data["modelName"]
|
||||
@@ -308,7 +324,73 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the user step to pick discovered device."""
|
||||
"""Handle the user step to choose cloud login or direct discovery."""
|
||||
# Check if all scanners are in active mode
|
||||
# If so, skip the menu and go directly to device selection
|
||||
scanners = async_current_scanners(self.hass)
|
||||
if scanners and all(
|
||||
scanner.current_mode == BluetoothScanningMode.ACTIVE for scanner in scanners
|
||||
):
|
||||
# All scanners are active, skip the menu
|
||||
return await self.async_step_select_device()
|
||||
|
||||
return self.async_show_menu(
|
||||
step_id="user",
|
||||
menu_options=["cloud_login", "select_device"],
|
||||
)
|
||||
|
||||
async def async_step_cloud_login(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the cloud login step."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await fetch_cloud_devices(
|
||||
async_get_clientsession(self.hass),
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
except (SwitchbotApiError, SwitchbotAccountConnectionError) as ex:
|
||||
_LOGGER.debug(
|
||||
"Failed to connect to SwitchBot API: %s", ex, exc_info=True
|
||||
)
|
||||
raise AbortFlow(
|
||||
"api_error", description_placeholders={"error_detail": str(ex)}
|
||||
) from ex
|
||||
except SwitchbotAuthenticationError as ex:
|
||||
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
||||
errors = {"base": "auth_failed"}
|
||||
description_placeholders = {"error_detail": str(ex)}
|
||||
else:
|
||||
# Save credentials temporarily for the duration of this flow
|
||||
# to avoid re-prompting if encrypted device auth is needed
|
||||
# These will be discarded when the flow completes
|
||||
self._cloud_username = user_input[CONF_USERNAME]
|
||||
self._cloud_password = user_input[CONF_PASSWORD]
|
||||
return await self.async_step_select_device()
|
||||
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
step_id="cloud_login",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_select_device(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the step to pick discovered device."""
|
||||
errors: dict[str, str] = {}
|
||||
device_adv: SwitchBotAdvertisement | None = None
|
||||
if user_input is not None:
|
||||
@@ -333,7 +415,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_confirm()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
step_id="select_device",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
|
||||
@@ -53,6 +53,7 @@ class SupportedModels(StrEnum):
|
||||
STRIP_LIGHT_3 = "strip_light_3"
|
||||
RGBICWW_STRIP_LIGHT = "rgbicww_strip_light"
|
||||
RGBICWW_FLOOR_LAMP = "rgbicww_floor_lamp"
|
||||
PLUG_MINI_EU = "plug_mini_eu"
|
||||
|
||||
|
||||
CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||
@@ -85,6 +86,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||
SwitchbotModel.STRIP_LIGHT_3: SupportedModels.STRIP_LIGHT_3,
|
||||
SwitchbotModel.RGBICWW_STRIP_LIGHT: SupportedModels.RGBICWW_STRIP_LIGHT,
|
||||
SwitchbotModel.RGBICWW_FLOOR_LAMP: SupportedModels.RGBICWW_FLOOR_LAMP,
|
||||
SwitchbotModel.PLUG_MINI_EU: SupportedModels.PLUG_MINI_EU,
|
||||
}
|
||||
|
||||
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||
@@ -118,6 +120,7 @@ ENCRYPTED_MODELS = {
|
||||
SwitchbotModel.STRIP_LIGHT_3,
|
||||
SwitchbotModel.RGBICWW_STRIP_LIGHT,
|
||||
SwitchbotModel.RGBICWW_FLOOR_LAMP,
|
||||
SwitchbotModel.PLUG_MINI_EU,
|
||||
}
|
||||
|
||||
ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
|
||||
@@ -136,6 +139,7 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
|
||||
SwitchbotModel.STRIP_LIGHT_3: switchbot.SwitchbotStripLight3,
|
||||
SwitchbotModel.RGBICWW_STRIP_LIGHT: switchbot.SwitchbotRgbicLight,
|
||||
SwitchbotModel.RGBICWW_FLOOR_LAMP: switchbot.SwitchbotRgbicLight,
|
||||
SwitchbotModel.PLUG_MINI_EU: switchbot.SwitchbotRelaySwitch,
|
||||
}
|
||||
|
||||
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
"flow_title": "{name} ({address})",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "One or more of your Bluetooth adapters is using passive scanning, which may not discover all SwitchBot devices. Would you like to sign in to your SwitchBot account to download device information and automate discovery? If you're not sure, we recommend signing in.",
|
||||
"menu_options": {
|
||||
"cloud_login": "Sign in to SwitchBot account",
|
||||
"select_device": "Continue without signing in"
|
||||
}
|
||||
},
|
||||
"cloud_login": {
|
||||
"description": "Please provide your SwitchBot app username and password. This data won't be saved and is only used to retrieve device model information to automate discovery. Usernames and passwords are case-sensitive.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::switchbot::config::step::encrypted_auth::data_description::username%]",
|
||||
"password": "[%key:component::switchbot::config::step::encrypted_auth::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"address": "MAC address"
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.FAN,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
@@ -57,6 +58,7 @@ class SwitchbotDevices:
|
||||
locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
fans: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
lights: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
humidifiers: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -255,6 +257,19 @@ async def make_device_data(
|
||||
)
|
||||
devices_data.lights.append((device, coordinator))
|
||||
|
||||
if isinstance(device, Device) and device.device_type == "Humidifier2":
|
||||
coordinator = await coordinator_for_device(
|
||||
hass, entry, api, device, coordinators_by_id
|
||||
)
|
||||
devices_data.humidifiers.append((device, coordinator))
|
||||
|
||||
if isinstance(device, Device) and device.device_type == "Humidifier":
|
||||
coordinator = await coordinator_for_device(
|
||||
hass, entry, api, device, coordinators_by_id
|
||||
)
|
||||
devices_data.humidifiers.append((device, coordinator))
|
||||
devices_data.sensors.append((device, coordinator))
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up SwitchBot via API from a config entry."""
|
||||
|
||||
@@ -20,6 +20,12 @@ VACUUM_FAN_SPEED_MAX = "max"
|
||||
AFTER_COMMAND_REFRESH = 5
|
||||
COVER_ENTITY_AFTER_COMMAND_REFRESH = 10
|
||||
|
||||
HUMIDITY_LEVELS = {
|
||||
34: 101, # Low humidity mode
|
||||
67: 102, # Medium humidity mode
|
||||
100: 103, # High humidity mode
|
||||
}
|
||||
|
||||
|
||||
class AirPurifierMode(Enum):
|
||||
"""Air Purifier Modes."""
|
||||
@@ -33,3 +39,21 @@ class AirPurifierMode(Enum):
|
||||
def get_modes(cls) -> list[str]:
|
||||
"""Return a list of available air purifier modes as lowercase strings."""
|
||||
return [mode.name.lower() for mode in cls]
|
||||
|
||||
|
||||
class Humidifier2Mode(Enum):
|
||||
"""Enumerates the available modes for a SwitchBot humidifier2."""
|
||||
|
||||
HIGH = 1
|
||||
MEDIUM = 2
|
||||
LOW = 3
|
||||
QUIET = 4
|
||||
TARGET_HUMIDITY = 5
|
||||
SLEEP = 6
|
||||
AUTO = 7
|
||||
DRYING_FILTER = 8
|
||||
|
||||
@classmethod
|
||||
def get_modes(cls) -> list[str]:
|
||||
"""Return a list of available humidifier2 modes as lowercase strings."""
|
||||
return [mode.name.lower() for mode in cls]
|
||||
|
||||
155
homeassistant/components/switchbot_cloud/humidifier.py
Normal file
155
homeassistant/components/switchbot_cloud/humidifier.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""Support for Switchbot humidifier."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import CommonCommands, HumidifierCommands, HumidifierV2Commands
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
MODE_AUTO,
|
||||
MODE_NORMAL,
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntity,
|
||||
HumidifierEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from .const import AFTER_COMMAND_REFRESH, DOMAIN, HUMIDITY_LEVELS, Humidifier2Mode
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Switchbot based on a config entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
SwitchBotHumidifier(data.api, device, coordinator)
|
||||
if device.device_type == "Humidifier"
|
||||
else SwitchBotEvaporativeHumidifier(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.humidifiers
|
||||
)
|
||||
|
||||
|
||||
class SwitchBotHumidifier(SwitchBotCloudEntity, HumidifierEntity):
|
||||
"""Representation of a Switchbot humidifier."""
|
||||
|
||||
_attr_supported_features = HumidifierEntityFeature.MODES
|
||||
_attr_device_class = HumidifierDeviceClass.HUMIDIFIER
|
||||
_attr_available_modes = [MODE_NORMAL, MODE_AUTO]
|
||||
_attr_min_humidity = 1
|
||||
_attr_translation_key = "humidifier"
|
||||
_attr_name = None
|
||||
_attr_target_humidity = 50
|
||||
|
||||
def _set_attributes(self) -> None:
|
||||
"""Set attributes from coordinator data."""
|
||||
if coord_data := self.coordinator.data:
|
||||
self._attr_is_on = coord_data.get("power") == STATE_ON
|
||||
self._attr_mode = MODE_AUTO if coord_data.get("auto") else MODE_NORMAL
|
||||
self._attr_current_humidity = coord_data.get("humidity")
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
self.target_humidity, parameters = self._map_humidity_to_supported_level(
|
||||
humidity
|
||||
)
|
||||
await self.send_api_command(
|
||||
HumidifierCommands.SET_MODE, parameters=str(parameters)
|
||||
)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_mode(self, mode: str) -> None:
|
||||
"""Set new target humidity."""
|
||||
if mode == MODE_AUTO:
|
||||
await self.send_api_command(HumidifierCommands.SET_MODE, parameters=mode)
|
||||
else:
|
||||
await self.send_api_command(
|
||||
HumidifierCommands.SET_MODE, parameters=str(102)
|
||||
)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
await self.send_api_command(CommonCommands.ON)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
await self.send_api_command(CommonCommands.OFF)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
def _map_humidity_to_supported_level(self, humidity: int) -> tuple[int, int]:
|
||||
"""Map any humidity to the closest supported level and its parameter."""
|
||||
if humidity <= 34:
|
||||
return 34, HUMIDITY_LEVELS[34]
|
||||
if humidity <= 67:
|
||||
return 67, HUMIDITY_LEVELS[67]
|
||||
return 100, HUMIDITY_LEVELS[100]
|
||||
|
||||
|
||||
class SwitchBotEvaporativeHumidifier(SwitchBotCloudEntity, HumidifierEntity):
|
||||
"""Representation of a Switchbot humidifier v2."""
|
||||
|
||||
_attr_supported_features = HumidifierEntityFeature.MODES
|
||||
_attr_device_class = HumidifierDeviceClass.HUMIDIFIER
|
||||
_attr_available_modes = Humidifier2Mode.get_modes()
|
||||
_attr_translation_key = "evaporative_humidifier"
|
||||
_attr_name = None
|
||||
_attr_target_humidity = 50
|
||||
|
||||
def _set_attributes(self) -> None:
|
||||
"""Set attributes from coordinator data."""
|
||||
if coord_data := self.coordinator.data:
|
||||
self._attr_is_on = coord_data.get("power") == STATE_ON
|
||||
self._attr_mode = (
|
||||
Humidifier2Mode(coord_data.get("mode")).name.lower()
|
||||
if coord_data.get("mode") is not None
|
||||
else None
|
||||
)
|
||||
self._attr_current_humidity = (
|
||||
coord_data.get("humidity")
|
||||
if coord_data.get("humidity") != 127
|
||||
else None
|
||||
)
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
assert self.coordinator.data is not None
|
||||
self._attr_target_humidity = humidity
|
||||
params = {"mode": self.coordinator.data["mode"], "humidity": humidity}
|
||||
await self.send_api_command(HumidifierV2Commands.SET_MODE, parameters=params)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_mode(self, mode: str) -> None:
|
||||
"""Set new target mode."""
|
||||
assert self.coordinator.data is not None
|
||||
params = {"mode": Humidifier2Mode[mode.upper()].value}
|
||||
await self.send_api_command(HumidifierV2Commands.SET_MODE, parameters=params)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
await self.send_api_command(CommonCommands.ON)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
await self.send_api_command(CommonCommands.OFF)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -34,6 +34,22 @@
|
||||
"10": "mdi:brightness-7"
|
||||
}
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"evaporative_humidifier": {
|
||||
"state_attributes": {
|
||||
"mode": {
|
||||
"state": {
|
||||
"high": "mdi:water-plus",
|
||||
"medium": "mdi:water",
|
||||
"low": "mdi:water-outline",
|
||||
"quiet": "mdi:volume-off",
|
||||
"target_humidity": "mdi:target",
|
||||
"drying_filter": "mdi:water-remove"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
"Motion Sensor": (BATTERY_DESCRIPTION,),
|
||||
"Contact Sensor": (BATTERY_DESCRIPTION,),
|
||||
"Water Detector": (BATTERY_DESCRIPTION,),
|
||||
"Humidifier": (TEMPERATURE_DESCRIPTION,),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,22 @@
|
||||
"light_level": {
|
||||
"name": "Light level"
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"evaporative_humidifier": {
|
||||
"state_attributes": {
|
||||
"mode": {
|
||||
"state": {
|
||||
"high": "[%key:common::state::high%]",
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"quiet": "Quiet",
|
||||
"target_humidity": "Target humidity",
|
||||
"drying_filter": "Drying filter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,6 +752,9 @@
|
||||
},
|
||||
"vehicle_state_valet_mode": {
|
||||
"default": "mdi:speedometer-slow"
|
||||
},
|
||||
"guest_mode_enabled": {
|
||||
"default": "mdi:account-group"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1084,6 +1084,9 @@
|
||||
},
|
||||
"vehicle_state_valet_mode": {
|
||||
"name": "Valet mode"
|
||||
},
|
||||
"guest_mode_enabled": {
|
||||
"name": "Guest mode"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from itertools import chain
|
||||
from typing import Any
|
||||
|
||||
from tesla_fleet_api.const import AutoSeat, Scope
|
||||
@@ -38,6 +37,7 @@ PARALLEL_UPDATES = 0
|
||||
class TeslemetrySwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes Teslemetry Switch entity."""
|
||||
|
||||
polling: bool = False
|
||||
on_func: Callable[[Vehicle], Awaitable[dict[str, Any]]]
|
||||
off_func: Callable[[Vehicle], Awaitable[dict[str, Any]]]
|
||||
scopes: list[Scope]
|
||||
@@ -53,6 +53,7 @@ class TeslemetrySwitchEntityDescription(SwitchEntityDescription):
|
||||
VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="vehicle_state_sentry_mode",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_SentryMode(
|
||||
lambda value: callback(None if value is None else value != "Off")
|
||||
),
|
||||
@@ -62,6 +63,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="vehicle_state_valet_mode",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, value: vehicle.listen_ValetModeEnabled(
|
||||
value
|
||||
),
|
||||
@@ -72,6 +74,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="climate_state_auto_seat_climate_left",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_AutoSeatClimateLeft(
|
||||
callback
|
||||
),
|
||||
@@ -85,6 +88,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="climate_state_auto_seat_climate_right",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle,
|
||||
callback: vehicle.listen_AutoSeatClimateRight(callback),
|
||||
on_func=lambda api: api.remote_auto_seat_climate_request(
|
||||
@@ -97,6 +101,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="climate_state_auto_steering_wheel_heat",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle,
|
||||
callback: vehicle.listen_HvacSteeringWheelHeatAuto(callback),
|
||||
on_func=lambda api: api.remote_auto_steering_wheel_heat_climate_request(
|
||||
@@ -109,6 +114,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="climate_state_defrost_mode",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_DefrostMode(
|
||||
lambda value: callback(None if value is None else value != "Off")
|
||||
),
|
||||
@@ -120,6 +126,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="charge_state_charging_state",
|
||||
polling=True,
|
||||
unique_id="charge_state_user_charge_enable_request",
|
||||
value_func=lambda state: state in {"Starting", "Charging"},
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_DetailedChargeState(
|
||||
@@ -131,6 +138,17 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
off_func=lambda api: api.charge_stop(),
|
||||
scopes=[Scope.VEHICLE_CMDS, Scope.VEHICLE_CHARGING_CMDS],
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="guest_mode_enabled",
|
||||
polling=False,
|
||||
unique_id="guest_mode_enabled",
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_GuestModeEnabled(
|
||||
callback
|
||||
),
|
||||
on_func=lambda api: api.guest_mode(True),
|
||||
off_func=lambda api: api.guest_mode(False),
|
||||
scopes=[Scope.VEHICLE_CMDS],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -141,35 +159,40 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Teslemetry Switch platform from a config entry."""
|
||||
|
||||
async_add_entities(
|
||||
chain(
|
||||
(
|
||||
TeslemetryVehiclePollingVehicleSwitchEntity(
|
||||
vehicle, description, entry.runtime_data.scopes
|
||||
entities: list[SwitchEntity] = []
|
||||
|
||||
for vehicle in entry.runtime_data.vehicles:
|
||||
for description in VEHICLE_DESCRIPTIONS:
|
||||
if vehicle.poll or vehicle.firmware < description.streaming_firmware:
|
||||
if description.polling:
|
||||
entities.append(
|
||||
TeslemetryVehiclePollingVehicleSwitchEntity(
|
||||
vehicle, description, entry.runtime_data.scopes
|
||||
)
|
||||
)
|
||||
else:
|
||||
entities.append(
|
||||
TeslemetryStreamingVehicleSwitchEntity(
|
||||
vehicle, description, entry.runtime_data.scopes
|
||||
)
|
||||
)
|
||||
if vehicle.poll or vehicle.firmware < description.streaming_firmware
|
||||
else TeslemetryStreamingVehicleSwitchEntity(
|
||||
vehicle, description, entry.runtime_data.scopes
|
||||
)
|
||||
for vehicle in entry.runtime_data.vehicles
|
||||
for description in VEHICLE_DESCRIPTIONS
|
||||
),
|
||||
(
|
||||
TeslemetryChargeFromGridSwitchEntity(
|
||||
energysite,
|
||||
entry.runtime_data.scopes,
|
||||
)
|
||||
for energysite in entry.runtime_data.energysites
|
||||
if energysite.info_coordinator.data.get("components_battery")
|
||||
and energysite.info_coordinator.data.get("components_solar")
|
||||
),
|
||||
(
|
||||
TeslemetryStormModeSwitchEntity(energysite, entry.runtime_data.scopes)
|
||||
for energysite in entry.runtime_data.energysites
|
||||
if energysite.info_coordinator.data.get("components_storm_mode_capable")
|
||||
),
|
||||
|
||||
entities.extend(
|
||||
TeslemetryChargeFromGridSwitchEntity(
|
||||
energysite,
|
||||
entry.runtime_data.scopes,
|
||||
)
|
||||
for energysite in entry.runtime_data.energysites
|
||||
if energysite.info_coordinator.data.get("components_battery")
|
||||
and energysite.info_coordinator.data.get("components_solar")
|
||||
)
|
||||
entities.extend(
|
||||
TeslemetryStormModeSwitchEntity(energysite, entry.runtime_data.scopes)
|
||||
for energysite in entry.runtime_data.energysites
|
||||
if energysite.info_coordinator.data.get("components_storm_mode_capable")
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class TeslemetryVehicleSwitchEntity(TeslemetryRootEntity, SwitchEntity):
|
||||
|
||||
@@ -75,6 +75,7 @@ class Platform(StrEnum):
|
||||
SWITCH = "switch"
|
||||
TEXT = "text"
|
||||
TIME = "time"
|
||||
TAG = "tag"
|
||||
TODO = "todo"
|
||||
TTS = "tts"
|
||||
UPDATE = "update"
|
||||
@@ -749,6 +750,7 @@ class UnitOfPressure(StrEnum):
|
||||
MBAR = "mbar"
|
||||
MMHG = "mmHg"
|
||||
INHG = "inHg"
|
||||
INH2O = "inH₂O"
|
||||
PSI = "psi"
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0
|
||||
go2rtc-client==0.2.1
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==5.6.2
|
||||
hass-nabucasa==1.1.0
|
||||
hass-nabucasa==1.1.1
|
||||
hassil==3.2.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250903.3
|
||||
|
||||
@@ -82,6 +82,7 @@ _STONE_TO_G = _POUND_TO_G * 14 # 14 pounds to a stone
|
||||
# Pressure conversion constants
|
||||
_STANDARD_GRAVITY = 9.80665
|
||||
_MERCURY_DENSITY = 13.5951
|
||||
_INH2O_TO_PA = 249.0889083333348 # 1 inH₂O = 249.0889083333348 Pa at 4°C
|
||||
|
||||
# Volume conversion constants
|
||||
_L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³
|
||||
@@ -435,6 +436,7 @@ class PressureConverter(BaseUnitConverter):
|
||||
UnitOfPressure.MBAR: 1 / 100,
|
||||
UnitOfPressure.INHG: 1
|
||||
/ (_IN_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY),
|
||||
UnitOfPressure.INH2O: 1 / _INH2O_TO_PA,
|
||||
UnitOfPressure.PSI: 1 / 6894.757,
|
||||
UnitOfPressure.MMHG: 1
|
||||
/ (_MM_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY),
|
||||
@@ -447,6 +449,7 @@ class PressureConverter(BaseUnitConverter):
|
||||
UnitOfPressure.CBAR,
|
||||
UnitOfPressure.MBAR,
|
||||
UnitOfPressure.INHG,
|
||||
UnitOfPressure.INH2O,
|
||||
UnitOfPressure.PSI,
|
||||
UnitOfPressure.MMHG,
|
||||
}
|
||||
|
||||
@@ -296,6 +296,7 @@ METRIC_SYSTEM = UnitSystem(
|
||||
# Convert non-metric pressure
|
||||
("pressure", UnitOfPressure.PSI): UnitOfPressure.KPA,
|
||||
("pressure", UnitOfPressure.INHG): UnitOfPressure.HPA,
|
||||
("pressure", UnitOfPressure.INH2O): UnitOfPressure.KPA,
|
||||
# Convert non-metric speeds except knots to km/h
|
||||
("speed", UnitOfSpeed.FEET_PER_SECOND): UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
("speed", UnitOfSpeed.INCHES_PER_SECOND): UnitOfSpeed.MILLIMETERS_PER_SECOND,
|
||||
@@ -379,6 +380,7 @@ US_CUSTOMARY_SYSTEM = UnitSystem(
|
||||
("pressure", UnitOfPressure.HPA): UnitOfPressure.PSI,
|
||||
("pressure", UnitOfPressure.KPA): UnitOfPressure.PSI,
|
||||
("pressure", UnitOfPressure.MMHG): UnitOfPressure.INHG,
|
||||
("pressure", UnitOfPressure.INH2O): UnitOfPressure.PSI,
|
||||
# Convert non-USCS speeds, except knots, to mph
|
||||
("speed", UnitOfSpeed.METERS_PER_SECOND): UnitOfSpeed.MILES_PER_HOUR,
|
||||
("speed", UnitOfSpeed.MILLIMETERS_PER_SECOND): UnitOfSpeed.INCHES_PER_SECOND,
|
||||
|
||||
@@ -47,7 +47,7 @@ dependencies = [
|
||||
"fnv-hash-fast==1.5.0",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==1.1.0",
|
||||
"hass-nabucasa==1.1.1",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.28.1",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -22,7 +22,7 @@ certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.6
|
||||
fnv-hash-fast==1.5.0
|
||||
hass-nabucasa==1.1.0
|
||||
hass-nabucasa==1.1.1
|
||||
httpx==0.28.1
|
||||
home-assistant-bluetooth==1.13.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -1140,7 +1140,7 @@ habiticalib==0.4.5
|
||||
habluetooth==5.6.2
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.1.0
|
||||
hass-nabucasa==1.1.1
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.1
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-c homeassistant/package_constraints.txt
|
||||
-r requirements_test_pre_commit.txt
|
||||
astroid==3.3.11
|
||||
coverage==7.10.0
|
||||
coverage==7.10.6
|
||||
freezegun==1.5.2
|
||||
go2rtc-client==0.2.1
|
||||
license-expression==30.4.3
|
||||
@@ -21,7 +21,7 @@ pylint-per-file-ignores==1.4.0
|
||||
pipdeptree==2.26.1
|
||||
pytest-asyncio==1.1.0
|
||||
pytest-aiohttp==1.1.0
|
||||
pytest-cov==6.2.1
|
||||
pytest-cov==7.0.0
|
||||
pytest-freezer==0.4.9
|
||||
pytest-github-actions-annotate-failures==0.3.0
|
||||
pytest-socket==0.7.0
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -1001,7 +1001,7 @@ habiticalib==0.4.5
|
||||
habluetooth==5.6.2
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.1.0
|
||||
hass-nabucasa==1.1.1
|
||||
|
||||
# homeassistant.components.assist_satellite
|
||||
# homeassistant.components.conversation
|
||||
|
||||
@@ -342,6 +342,15 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No
|
||||
"manifest", "Domain collides with built-in core integration"
|
||||
)
|
||||
|
||||
if (
|
||||
integration.manifest.get("integration_type") == "entity"
|
||||
and integration.domain not in Platform
|
||||
):
|
||||
integration.add_error(
|
||||
"manifest",
|
||||
"Integration should be added to Platform constant in homeassistant/const.py",
|
||||
)
|
||||
|
||||
if domain in NO_IOT_CLASS and "iot_class" in integration.manifest:
|
||||
integration.add_error("manifest", "Domain should not have an IoT Class")
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ async def test_generate_image(
|
||||
assert "image_data" not in result
|
||||
assert result["media_source_id"].startswith("media-source://ai_task/images/")
|
||||
assert result["media_source_id"].endswith("_test_task.png")
|
||||
assert result["url"].startswith("http://10.10.10.10:8123/api/ai_task/images/")
|
||||
assert result["url"].startswith("/api/ai_task/images/")
|
||||
assert result["url"].count("_test_task.png?authSig=") == 1
|
||||
assert result["mime_type"] == "image/png"
|
||||
assert result["model"] == "mock_model"
|
||||
|
||||
@@ -37,5 +37,7 @@
|
||||
'region': 'XEU',
|
||||
'serial': 'serial_number',
|
||||
}),
|
||||
'remote_command_list': list([
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -69,6 +69,7 @@ async def test_entry_diagnostics(
|
||||
patch("pybravia.BraviaClient.get_playing_info", return_value={}),
|
||||
patch("pybravia.BraviaClient.get_app_list", return_value=[]),
|
||||
patch("pybravia.BraviaClient.get_content_list_all", return_value=[]),
|
||||
patch("pybravia.BraviaClient.get_command_list", return_value=[]),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
|
||||
|
||||
@@ -7,8 +7,12 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from brother import BrotherSensors
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.brother.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE
|
||||
from homeassistant.components.brother.const import (
|
||||
CONF_COMMUNITY,
|
||||
DOMAIN,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -122,5 +126,10 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
domain=DOMAIN,
|
||||
title="HL-L2340DW 0123456789",
|
||||
unique_id="0123456789",
|
||||
data={CONF_HOST: "localhost", CONF_TYPE: "laser"},
|
||||
data={
|
||||
CONF_HOST: "localhost",
|
||||
CONF_TYPE: "laser",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
}),
|
||||
'firmware': '1.2.3',
|
||||
'info': dict({
|
||||
'advanced_settings': dict({
|
||||
'community': 'public',
|
||||
'port': 161,
|
||||
}),
|
||||
'host': 'localhost',
|
||||
'type': 'laser',
|
||||
}),
|
||||
|
||||
@@ -6,9 +6,13 @@ from unittest.mock import AsyncMock, patch
|
||||
from brother import SnmpError, UnsupportedModelError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.brother.const import DOMAIN
|
||||
from homeassistant.components.brother.const import (
|
||||
CONF_COMMUNITY,
|
||||
DOMAIN,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
@@ -17,7 +21,11 @@ from . import init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONFIG = {CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"}
|
||||
CONFIG = {
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_TYPE: "laser",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry", "mock_unload_entry")
|
||||
|
||||
@@ -37,16 +45,21 @@ async def test_create_entry(
|
||||
hass: HomeAssistant, host: str, mock_brother_client: AsyncMock
|
||||
) -> None:
|
||||
"""Test that the user step works with printer hostname/IPv4/IPv6."""
|
||||
config = CONFIG.copy()
|
||||
config[CONF_HOST] = host
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: host, CONF_TYPE: "laser"},
|
||||
data=config,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "HL-L2340DW 0123456789"
|
||||
assert result["data"][CONF_HOST] == host
|
||||
assert result["data"][CONF_TYPE] == "laser"
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_PORT] == 161
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY] == "public"
|
||||
|
||||
|
||||
async def test_invalid_hostname(hass: HomeAssistant) -> None:
|
||||
@@ -54,7 +67,11 @@ async def test_invalid_hostname(hass: HomeAssistant) -> None:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: "invalid/hostname", CONF_TYPE: "laser"},
|
||||
data={
|
||||
CONF_HOST: "invalid/hostname",
|
||||
CONF_TYPE: "laser",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["errors"] == {CONF_HOST: "wrong_host"}
|
||||
@@ -241,13 +258,19 @@ async def test_zeroconf_confirm_create_entry(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_TYPE: "laser"}
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_TYPE: "laser",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "HL-L2340DW 0123456789"
|
||||
assert result["data"][CONF_HOST] == "127.0.0.1"
|
||||
assert result["data"][CONF_TYPE] == "laser"
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_PORT] == 161
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY] == "public"
|
||||
|
||||
|
||||
async def test_reconfigure_successful(
|
||||
@@ -265,7 +288,10 @@ async def test_reconfigure_successful(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "10.10.10.10"},
|
||||
user_input={
|
||||
CONF_HOST: "10.10.10.10",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
@@ -273,6 +299,7 @@ async def test_reconfigure_successful(
|
||||
assert mock_config_entry.data == {
|
||||
CONF_HOST: "10.10.10.10",
|
||||
CONF_TYPE: "laser",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +330,10 @@ async def test_reconfigure_not_successful(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "10.10.10.10"},
|
||||
user_input={
|
||||
CONF_HOST: "10.10.10.10",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -314,7 +344,10 @@ async def test_reconfigure_not_successful(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "10.10.10.10"},
|
||||
user_input={
|
||||
CONF_HOST: "10.10.10.10",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
@@ -322,6 +355,7 @@ async def test_reconfigure_not_successful(
|
||||
assert mock_config_entry.data == {
|
||||
CONF_HOST: "10.10.10.10",
|
||||
CONF_TYPE: "laser",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
}
|
||||
|
||||
|
||||
@@ -340,7 +374,10 @@ async def test_reconfigure_invalid_hostname(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "invalid/hostname"},
|
||||
user_input={
|
||||
CONF_HOST: "invalid/hostname",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -365,7 +402,10 @@ async def test_reconfigure_not_the_same_device(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "10.10.10.10"},
|
||||
user_input={
|
||||
CONF_HOST: "10.10.10.10",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
@@ -5,8 +5,13 @@ from unittest.mock import AsyncMock, patch
|
||||
from brother import SnmpError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.brother.const import DOMAIN
|
||||
from homeassistant.components.brother.const import (
|
||||
CONF_COMMUNITY,
|
||||
DOMAIN,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import init_integration
|
||||
@@ -68,3 +73,26 @@ async def test_unload_entry(
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_migrate_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_brother_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test entry migration to minor_version=2."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="HL-L2340DW 0123456789",
|
||||
unique_id="0123456789",
|
||||
data={CONF_HOST: "localhost", CONF_TYPE: "laser"},
|
||||
minor_version=1,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.minor_version == 2
|
||||
assert config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_PORT] == 161
|
||||
assert config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY] == "public"
|
||||
|
||||
@@ -855,7 +855,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'CPU Temperature',
|
||||
'original_name': 'CPU temperature',
|
||||
'platform': 'fritz',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -869,7 +869,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Mock Title CPU Temperature',
|
||||
'friendly_name': 'Mock Title CPU temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# serializer version: 1
|
||||
# name: test_function_call
|
||||
list([
|
||||
Content(
|
||||
parts=[
|
||||
Part(
|
||||
text='Please call the test function'
|
||||
),
|
||||
],
|
||||
role='user'
|
||||
),
|
||||
Content(
|
||||
parts=[
|
||||
Part(
|
||||
text='Hi there!',
|
||||
thought_signature=b'_thought_signature_2'
|
||||
),
|
||||
Part(
|
||||
text='The user asked me to call a function',
|
||||
thought=True,
|
||||
thought_signature=b'_thought_signature_1'
|
||||
),
|
||||
Part(
|
||||
function_call=FunctionCall(
|
||||
args={
|
||||
'param1': [
|
||||
'test_value',
|
||||
"param1's value",
|
||||
],
|
||||
'param2': 2.7
|
||||
},
|
||||
name='test_tool'
|
||||
),
|
||||
thought_signature=b'_thought_signature_3'
|
||||
),
|
||||
],
|
||||
role='model'
|
||||
),
|
||||
Content(
|
||||
parts=[
|
||||
Part(
|
||||
function_response=FunctionResponse(
|
||||
name='test_tool',
|
||||
response={
|
||||
'result': 'Test response'
|
||||
}
|
||||
)
|
||||
),
|
||||
],
|
||||
role='user'
|
||||
),
|
||||
Content(
|
||||
parts=[
|
||||
Part(
|
||||
text="I've called the ",
|
||||
thought_signature=b'_thought_signature_4'
|
||||
),
|
||||
Part(
|
||||
text='test function with the provided parameters.',
|
||||
thought_signature=b'_thought_signature_5'
|
||||
),
|
||||
],
|
||||
role='model'
|
||||
),
|
||||
])
|
||||
# ---
|
||||
@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch
|
||||
from freezegun import freeze_time
|
||||
from google.genai.types import GenerateContentResponse
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.components.conversation import UserContent
|
||||
@@ -80,6 +81,7 @@ async def test_function_call(
|
||||
mock_config_entry_with_assist: MockConfigEntry,
|
||||
mock_chat_log: MockChatLog, # noqa: F811
|
||||
mock_send_message_stream: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test function calling."""
|
||||
agent_id = "conversation.google_ai_conversation"
|
||||
@@ -93,9 +95,15 @@ async def test_function_call(
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "The user asked me to call a function",
|
||||
"thought": True,
|
||||
"thought_signature": b"_thought_signature_1",
|
||||
},
|
||||
{
|
||||
"text": "Hi there!",
|
||||
}
|
||||
"thought_signature": b"_thought_signature_2",
|
||||
},
|
||||
],
|
||||
"role": "model",
|
||||
}
|
||||
@@ -118,6 +126,7 @@ async def test_function_call(
|
||||
"param2": 2.7,
|
||||
},
|
||||
},
|
||||
"thought_signature": b"_thought_signature_3",
|
||||
}
|
||||
],
|
||||
"role": "model",
|
||||
@@ -136,6 +145,7 @@ async def test_function_call(
|
||||
"parts": [
|
||||
{
|
||||
"text": "I've called the ",
|
||||
"thought_signature": b"_thought_signature_4",
|
||||
}
|
||||
],
|
||||
"role": "model",
|
||||
@@ -150,6 +160,25 @@ async def test_function_call(
|
||||
"parts": [
|
||||
{
|
||||
"text": "test function with the provided parameters.",
|
||||
"thought_signature": b"_thought_signature_5",
|
||||
}
|
||||
],
|
||||
"role": "model",
|
||||
},
|
||||
"finish_reason": "STOP",
|
||||
}
|
||||
],
|
||||
),
|
||||
],
|
||||
# Follow-up response
|
||||
[
|
||||
GenerateContentResponse(
|
||||
candidates=[
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "You are welcome!",
|
||||
}
|
||||
],
|
||||
"role": "model",
|
||||
@@ -205,6 +234,22 @@ async def test_function_call(
|
||||
"video_metadata": None,
|
||||
}
|
||||
|
||||
# Test history conversion for multi-turn conversation
|
||||
with patch(
|
||||
"google.genai.chats.AsyncChats.create", return_value=AsyncMock()
|
||||
) as mock_create:
|
||||
mock_create.return_value.send_message_stream = mock_send_message_stream
|
||||
await conversation.async_converse(
|
||||
hass,
|
||||
"Thank you!",
|
||||
mock_chat_log.conversation_id,
|
||||
context,
|
||||
agent_id=agent_id,
|
||||
device_id="test_device",
|
||||
)
|
||||
|
||||
assert mock_create.call_args[1].get("history") == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
@pytest.mark.usefixtures("mock_ulid_tools")
|
||||
|
||||
@@ -208,6 +208,7 @@ async def test_tts_service_speak(
|
||||
threshold=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
),
|
||||
],
|
||||
thinking_config=types.ThinkingConfig(include_thoughts=True),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -276,5 +277,6 @@ async def test_tts_service_speak_error(
|
||||
threshold=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
),
|
||||
],
|
||||
thinking_config=types.ThinkingConfig(include_thoughts=True),
|
||||
),
|
||||
)
|
||||
|
||||
176
tests/components/homee/fixtures/add_device.json
Normal file
176
tests/components/homee/fixtures/add_device.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Added Device",
|
||||
"profile": 4010,
|
||||
"image": "default",
|
||||
"favorite": 0,
|
||||
"order": 20,
|
||||
"protocol": 1,
|
||||
"routing": 0,
|
||||
"state": 1,
|
||||
"state_changed": 1709379826,
|
||||
"added": 1676199446,
|
||||
"history": 1,
|
||||
"cube_type": 1,
|
||||
"note": "",
|
||||
"services": 5,
|
||||
"phonetic_name": "",
|
||||
"owner": 2,
|
||||
"security": 0,
|
||||
"attributes": [
|
||||
{
|
||||
"id": 21,
|
||||
"node_id": 3,
|
||||
"instance": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 200000,
|
||||
"current_value": 555.591,
|
||||
"target_value": 555.591,
|
||||
"last_value": 555.586,
|
||||
"unit": "kWh",
|
||||
"step_value": 1.0,
|
||||
"editable": 0,
|
||||
"type": 4,
|
||||
"state": 1,
|
||||
"last_changed": 1694175270,
|
||||
"changed_by": 1,
|
||||
"changed_by_id": 0,
|
||||
"based_on": 1,
|
||||
"data": "",
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"node_id": 3,
|
||||
"instance": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"current_value": 0.0,
|
||||
"target_value": 0.0,
|
||||
"last_value": 0.0,
|
||||
"unit": "",
|
||||
"step_value": 1.0,
|
||||
"editable": 0,
|
||||
"type": 17,
|
||||
"state": 1,
|
||||
"last_changed": 1691668428,
|
||||
"changed_by": 1,
|
||||
"changed_by_id": 0,
|
||||
"based_on": 1,
|
||||
"data": "",
|
||||
"name": "",
|
||||
"options": {
|
||||
"automations": ["reset"],
|
||||
"history": {
|
||||
"day": 182,
|
||||
"week": 26,
|
||||
"month": 6,
|
||||
"stepped": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"node_id": 3,
|
||||
"instance": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"current_value": 100.0,
|
||||
"target_value": 100.0,
|
||||
"last_value": 100.0,
|
||||
"unit": "%",
|
||||
"step_value": 0.5,
|
||||
"editable": 1,
|
||||
"type": 349,
|
||||
"state": 1,
|
||||
"last_changed": 1624446307,
|
||||
"changed_by": 1,
|
||||
"changed_by_id": 0,
|
||||
"based_on": 1,
|
||||
"data": "",
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"node_id": 3,
|
||||
"instance": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"current_value": 0.0,
|
||||
"target_value": 0.0,
|
||||
"last_value": 0.0,
|
||||
"unit": "",
|
||||
"step_value": 1.0,
|
||||
"editable": 1,
|
||||
"type": 346,
|
||||
"state": 1,
|
||||
"last_changed": 1624806728,
|
||||
"changed_by": 1,
|
||||
"changed_by_id": 0,
|
||||
"based_on": 1,
|
||||
"data": "",
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"node_id": 3,
|
||||
"instance": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"current_value": 0.0,
|
||||
"target_value": 0.0,
|
||||
"last_value": 0.0,
|
||||
"unit": "n/a",
|
||||
"step_value": 1.0,
|
||||
"editable": 1,
|
||||
"type": 13,
|
||||
"state": 1,
|
||||
"last_changed": 1736003985,
|
||||
"changed_by": 1,
|
||||
"changed_by_id": 0,
|
||||
"based_on": 1,
|
||||
"data": "",
|
||||
"name": "",
|
||||
"options": {
|
||||
"automations": ["toggle"],
|
||||
"history": {
|
||||
"day": 35,
|
||||
"week": 5,
|
||||
"month": 1,
|
||||
"stepped": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"node_id": 3,
|
||||
"instance": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"current_value": 1.0,
|
||||
"target_value": 0.0,
|
||||
"last_value": 0.0,
|
||||
"unit": "n/a",
|
||||
"step_value": 1.0,
|
||||
"editable": 1,
|
||||
"type": 1,
|
||||
"state": 1,
|
||||
"last_changed": 1736743294,
|
||||
"changed_by": 1,
|
||||
"changed_by_id": 0,
|
||||
"based_on": 1,
|
||||
"data": "",
|
||||
"name": "",
|
||||
"options": {
|
||||
"can_observe": [300],
|
||||
"automations": ["toggle"],
|
||||
"history": {
|
||||
"day": 35,
|
||||
"week": 5,
|
||||
"month": 1,
|
||||
"stepped": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,3 +27,26 @@ async def test_sensor_snapshot(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_add_device(
|
||||
hass: HomeAssistant,
|
||||
mock_homee: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test adding a device."""
|
||||
mock_homee.nodes = [build_mock_node("binary_sensors.json")]
|
||||
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
|
||||
with patch("homeassistant.components.homee.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Add a new device
|
||||
added_node = build_mock_node("add_device.json")
|
||||
mock_homee.nodes.append(added_node)
|
||||
mock_homee.get_node_by_id.return_value = mock_homee.nodes[1]
|
||||
await mock_homee.add_nodes_listener.call_args_list[0][0][0](added_node, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
'rate_limit': 0,
|
||||
'state_updater': True,
|
||||
}),
|
||||
'configuration_error': "extra keys not allowed @ data['knx']['wrong_key']",
|
||||
'config_store': dict({
|
||||
'entities': dict({
|
||||
}),
|
||||
}),
|
||||
'configuration_yaml': dict({
|
||||
'wrong_key': dict({
|
||||
}),
|
||||
@@ -19,6 +22,7 @@
|
||||
'current_address': '0.0.0',
|
||||
'version': '0.0.0',
|
||||
}),
|
||||
'yaml_configuration_error': "extra keys not allowed @ data['knx']['wrong_key']",
|
||||
})
|
||||
# ---
|
||||
# name: test_diagnostic_redact[hass_config0]
|
||||
@@ -35,13 +39,17 @@
|
||||
'state_updater': True,
|
||||
'user_password': '**REDACTED**',
|
||||
}),
|
||||
'configuration_error': None,
|
||||
'config_store': dict({
|
||||
'entities': dict({
|
||||
}),
|
||||
}),
|
||||
'configuration_yaml': None,
|
||||
'project_info': None,
|
||||
'xknx': dict({
|
||||
'current_address': '0.0.0',
|
||||
'version': '0.0.0',
|
||||
}),
|
||||
'yaml_configuration_error': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_diagnostics[hass_config0]
|
||||
@@ -54,13 +62,17 @@
|
||||
'rate_limit': 0,
|
||||
'state_updater': True,
|
||||
}),
|
||||
'configuration_error': None,
|
||||
'config_store': dict({
|
||||
'entities': dict({
|
||||
}),
|
||||
}),
|
||||
'configuration_yaml': None,
|
||||
'project_info': None,
|
||||
'xknx': dict({
|
||||
'current_address': '0.0.0',
|
||||
'version': '0.0.0',
|
||||
}),
|
||||
'yaml_configuration_error': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_diagnostics_project[hass_config0]
|
||||
@@ -73,7 +85,50 @@
|
||||
'rate_limit': 0,
|
||||
'state_updater': True,
|
||||
}),
|
||||
'configuration_error': None,
|
||||
'config_store': dict({
|
||||
'entities': dict({
|
||||
'light': dict({
|
||||
'knx_es_01J85ZKTFHSZNG4X9DYBE592TF': dict({
|
||||
'entity': dict({
|
||||
'device_info': None,
|
||||
'entity_category': 'config',
|
||||
'name': 'test',
|
||||
}),
|
||||
'knx': dict({
|
||||
'color_temp_max': 6000,
|
||||
'color_temp_min': 2700,
|
||||
'ga_switch': dict({
|
||||
'passive': list([
|
||||
]),
|
||||
'state': '1/0/21',
|
||||
'write': '1/1/21',
|
||||
}),
|
||||
'sync_state': True,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'switch': dict({
|
||||
'knx_es_9d97829f47f1a2a3176a7c5b4216070c': dict({
|
||||
'entity': dict({
|
||||
'device_info': 'knx_vdev_4c80a564f5fe5da701ed293966d6384d',
|
||||
'entity_category': None,
|
||||
'name': 'test',
|
||||
}),
|
||||
'knx': dict({
|
||||
'ga_switch': dict({
|
||||
'passive': list([
|
||||
]),
|
||||
'state': '1/0/45',
|
||||
'write': '1/1/45',
|
||||
}),
|
||||
'invert': False,
|
||||
'respond_to_read': False,
|
||||
'sync_state': True,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'configuration_yaml': None,
|
||||
'project_info': dict({
|
||||
'created_by': 'ETS5',
|
||||
@@ -91,5 +146,6 @@
|
||||
'current_address': '0.0.0',
|
||||
'version': '0.0.0',
|
||||
}),
|
||||
'yaml_configuration_error': None,
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -574,26 +574,6 @@
|
||||
'required': False,
|
||||
'type': 'knx_section_flat',
|
||||
}),
|
||||
dict({
|
||||
'name': 'ga_blue_brightness',
|
||||
'options': dict({
|
||||
'passive': True,
|
||||
'state': dict({
|
||||
'required': False,
|
||||
}),
|
||||
'validDPTs': list([
|
||||
dict({
|
||||
'main': 5,
|
||||
'sub': 1,
|
||||
}),
|
||||
]),
|
||||
'write': dict({
|
||||
'required': True,
|
||||
}),
|
||||
}),
|
||||
'required': True,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
dict({
|
||||
'name': 'ga_blue_switch',
|
||||
'optional': True,
|
||||
@@ -616,14 +596,7 @@
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
dict({
|
||||
'collapsible': False,
|
||||
'name': 'section_white',
|
||||
'required': False,
|
||||
'type': 'knx_section_flat',
|
||||
}),
|
||||
dict({
|
||||
'name': 'ga_white_brightness',
|
||||
'optional': True,
|
||||
'name': 'ga_blue_brightness',
|
||||
'options': dict({
|
||||
'passive': True,
|
||||
'state': dict({
|
||||
@@ -639,9 +612,15 @@
|
||||
'required': True,
|
||||
}),
|
||||
}),
|
||||
'required': False,
|
||||
'required': True,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
dict({
|
||||
'collapsible': False,
|
||||
'name': 'section_white',
|
||||
'required': False,
|
||||
'type': 'knx_section_flat',
|
||||
}),
|
||||
dict({
|
||||
'name': 'ga_white_switch',
|
||||
'optional': True,
|
||||
@@ -663,6 +642,27 @@
|
||||
'required': False,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
dict({
|
||||
'name': 'ga_white_brightness',
|
||||
'optional': True,
|
||||
'options': dict({
|
||||
'passive': True,
|
||||
'state': dict({
|
||||
'required': False,
|
||||
}),
|
||||
'validDPTs': list([
|
||||
dict({
|
||||
'main': 5,
|
||||
'sub': 1,
|
||||
}),
|
||||
]),
|
||||
'write': dict({
|
||||
'required': True,
|
||||
}),
|
||||
}),
|
||||
'required': False,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
]),
|
||||
'translation_key': 'individual_addresses',
|
||||
'type': 'knx_group_select_option',
|
||||
|
||||
@@ -120,9 +120,13 @@ async def test_diagnostics_project(
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics."""
|
||||
await knx.setup_integration()
|
||||
await knx.setup_integration(
|
||||
config_store_fixture="config_store_light_switch.json",
|
||||
state_updater=False,
|
||||
)
|
||||
knx.xknx.version = "0.0.0"
|
||||
# snapshot will contain project specific fields in `project_info`
|
||||
# and UI configuration in `config_store`
|
||||
assert (
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
|
||||
== snapshot
|
||||
|
||||
@@ -128,6 +128,25 @@ FEEDER_ROBOT_DATA = {
|
||||
"mealInsertSize": 1,
|
||||
},
|
||||
"updated_at": "2022-09-08T15:07:00.000000+00:00",
|
||||
"active_schedule": {
|
||||
"id": "1",
|
||||
"name": "Feeding",
|
||||
"meals": [
|
||||
{
|
||||
"id": "1",
|
||||
"days": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
"hour": 6,
|
||||
"name": "Breakfast",
|
||||
"skip": None,
|
||||
"minute": 30,
|
||||
"paused": False,
|
||||
"portions": 3,
|
||||
"mealNumber": 1,
|
||||
"scheduleId": None,
|
||||
}
|
||||
],
|
||||
"created_at": "2021-12-17T07:07:31.047747+00:00",
|
||||
},
|
||||
},
|
||||
"feeding_snack": [
|
||||
{"timestamp": "2022-09-04T03:03:00.000000+00:00", "amount": 0.125},
|
||||
|
||||
@@ -104,6 +104,7 @@ async def test_litter_robot_sensor(
|
||||
assert sensor.attributes["state_class"] == SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2022-09-08 19:00:00+00:00")
|
||||
async def test_feeder_robot_sensor(
|
||||
hass: HomeAssistant, mock_account_with_feederrobot: MagicMock
|
||||
) -> None:
|
||||
@@ -117,6 +118,16 @@ async def test_feeder_robot_sensor(
|
||||
assert sensor.state == "2022-09-08T18:00:00+00:00"
|
||||
assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP
|
||||
|
||||
sensor = hass.states.get("sensor.test_next_feeding")
|
||||
assert sensor.state == "2022-09-09T12:30:00+00:00"
|
||||
assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP
|
||||
|
||||
sensor = hass.states.get("sensor.test_food_dispensed_today")
|
||||
assert sensor.state == "0.375"
|
||||
assert sensor.attributes["last_reset"] == "2022-09-08T00:00:00-07:00"
|
||||
assert sensor.attributes["state_class"] == SensorStateClass.TOTAL
|
||||
assert sensor.attributes["unit_of_measurement"] == "cups"
|
||||
|
||||
|
||||
async def test_pet_weight_sensor(
|
||||
hass: HomeAssistant, mock_account_with_pet: MagicMock
|
||||
|
||||
@@ -3904,7 +3904,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
@@ -3932,7 +3932,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states[platforms0][sensor.washing_machine_energy_forecast-entry]
|
||||
@@ -4501,7 +4501,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WATER: 'water'>,
|
||||
@@ -4529,7 +4529,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states[platforms0][sensor.washing_machine_water_forecast-entry]
|
||||
@@ -6050,7 +6050,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
@@ -6078,7 +6078,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_energy_forecast-entry]
|
||||
@@ -6647,7 +6647,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WATER: 'water'>,
|
||||
@@ -6675,7 +6675,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_water_forecast-entry]
|
||||
|
||||
@@ -315,6 +315,13 @@ async def test_laundry_wash_scenario(
|
||||
check_sensor_state(hass, "sensor.washing_machine_remaining_time", "unknown", step)
|
||||
# OFF -> elapsed forced to unknown (some devices continue reporting last value of last cycle)
|
||||
check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "unknown", step)
|
||||
# consumption sensors have to report "unknown" when the device is not working
|
||||
check_sensor_state(
|
||||
hass, "sensor.washing_machine_energy_consumption", "unknown", step
|
||||
)
|
||||
check_sensor_state(
|
||||
hass, "sensor.washing_machine_water_consumption", "unknown", step
|
||||
)
|
||||
|
||||
# Simulate program started
|
||||
device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 5
|
||||
@@ -337,10 +344,41 @@ async def test_laundry_wash_scenario(
|
||||
device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 12
|
||||
device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_raw"] = 1200
|
||||
device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_localized"] = "1200"
|
||||
device_fixture["DummyWasher"]["state"]["ecoFeedback"] = {
|
||||
"currentEnergyConsumption": {
|
||||
"value": 0.9,
|
||||
"unit": "kWh",
|
||||
},
|
||||
"currentWaterConsumption": {
|
||||
"value": 52,
|
||||
"unit": "l",
|
||||
},
|
||||
}
|
||||
|
||||
freezer.tick(timedelta(seconds=130))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# at this point, appliance is working, but it started reporting a value from last cycle, so it is forced to 0
|
||||
check_sensor_state(hass, "sensor.washing_machine_energy_consumption", "0", step)
|
||||
check_sensor_state(hass, "sensor.washing_machine_water_consumption", "0", step)
|
||||
|
||||
# intermediate step, only to report new consumption values
|
||||
device_fixture["DummyWasher"]["state"]["ecoFeedback"] = {
|
||||
"currentEnergyConsumption": {
|
||||
"value": 0.0,
|
||||
"unit": "kWh",
|
||||
},
|
||||
"currentWaterConsumption": {
|
||||
"value": 0,
|
||||
"unit": "l",
|
||||
},
|
||||
}
|
||||
|
||||
freezer.tick(timedelta(seconds=130))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
step += 1
|
||||
|
||||
check_sensor_state(hass, "sensor.washing_machine", "in_use", step)
|
||||
@@ -351,6 +389,28 @@ async def test_laundry_wash_scenario(
|
||||
# IN_USE -> elapsed, remaining time from API (normal case)
|
||||
check_sensor_state(hass, "sensor.washing_machine_remaining_time", "105", step)
|
||||
check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "12", step)
|
||||
check_sensor_state(hass, "sensor.washing_machine_energy_consumption", "0.0", step)
|
||||
check_sensor_state(hass, "sensor.washing_machine_water_consumption", "0", step)
|
||||
|
||||
# intermediate step, only to report new consumption values
|
||||
device_fixture["DummyWasher"]["state"]["ecoFeedback"] = {
|
||||
"currentEnergyConsumption": {
|
||||
"value": 0.1,
|
||||
"unit": "kWh",
|
||||
},
|
||||
"currentWaterConsumption": {
|
||||
"value": 7,
|
||||
"unit": "l",
|
||||
},
|
||||
}
|
||||
|
||||
freezer.tick(timedelta(seconds=130))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# at this point, it starts reporting value from API
|
||||
check_sensor_state(hass, "sensor.washing_machine_energy_consumption", "0.1", step)
|
||||
check_sensor_state(hass, "sensor.washing_machine_water_consumption", "7", step)
|
||||
|
||||
# Simulate rinse hold phase
|
||||
device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 11
|
||||
@@ -389,6 +449,7 @@ async def test_laundry_wash_scenario(
|
||||
device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 0
|
||||
device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0
|
||||
device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 0
|
||||
device_fixture["DummyWasher"]["state"]["ecoFeedback"] = None
|
||||
|
||||
freezer.tick(timedelta(seconds=130))
|
||||
async_fire_time_changed(hass)
|
||||
@@ -406,6 +467,9 @@ async def test_laundry_wash_scenario(
|
||||
check_sensor_state(hass, "sensor.washing_machine_remaining_time", "0", step)
|
||||
# PROGRAM_ENDED -> elapsed time kept from last program (some devices immediately go to 0)
|
||||
check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "109", step)
|
||||
# consumption values now are reporting last known value, API might start reporting null object
|
||||
check_sensor_state(hass, "sensor.washing_machine_energy_consumption", "0.1", step)
|
||||
check_sensor_state(hass, "sensor.washing_machine_water_consumption", "7", step)
|
||||
|
||||
# Simulate when door is opened after program ended
|
||||
device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 3
|
||||
|
||||
@@ -1331,7 +1331,7 @@ async def test_discover_alarm_control_panel(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("topic", "config", "entity_id", "name", "domain"),
|
||||
("topic", "config", "entity_id", "name", "domain", "deprecation_warning"),
|
||||
[
|
||||
(
|
||||
"homeassistant/alarm_control_panel/object/bla/config",
|
||||
@@ -1339,6 +1339,7 @@ async def test_discover_alarm_control_panel(
|
||||
"alarm_control_panel.hello_id",
|
||||
"Hello World 1",
|
||||
"alarm_control_panel",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/binary_sensor/object/bla/config",
|
||||
@@ -1346,6 +1347,7 @@ async def test_discover_alarm_control_panel(
|
||||
"binary_sensor.hello_id",
|
||||
"Hello World 2",
|
||||
"binary_sensor",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/button/object/bla/config",
|
||||
@@ -1353,6 +1355,7 @@ async def test_discover_alarm_control_panel(
|
||||
"button.hello_id",
|
||||
"Hello World button",
|
||||
"button",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/camera/object/bla/config",
|
||||
@@ -1360,6 +1363,7 @@ async def test_discover_alarm_control_panel(
|
||||
"camera.hello_id",
|
||||
"Hello World 3",
|
||||
"camera",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/climate/object/bla/config",
|
||||
@@ -1367,6 +1371,7 @@ async def test_discover_alarm_control_panel(
|
||||
"climate.hello_id",
|
||||
"Hello World 4",
|
||||
"climate",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/cover/object/bla/config",
|
||||
@@ -1374,6 +1379,7 @@ async def test_discover_alarm_control_panel(
|
||||
"cover.hello_id",
|
||||
"Hello World 5",
|
||||
"cover",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/fan/object/bla/config",
|
||||
@@ -1381,6 +1387,7 @@ async def test_discover_alarm_control_panel(
|
||||
"fan.hello_id",
|
||||
"Hello World 6",
|
||||
"fan",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/humidifier/object/bla/config",
|
||||
@@ -1388,6 +1395,7 @@ async def test_discover_alarm_control_panel(
|
||||
"humidifier.hello_id",
|
||||
"Hello World 7",
|
||||
"humidifier",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/number/object/bla/config",
|
||||
@@ -1395,6 +1403,7 @@ async def test_discover_alarm_control_panel(
|
||||
"number.hello_id",
|
||||
"Hello World 8",
|
||||
"number",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/scene/object/bla/config",
|
||||
@@ -1402,6 +1411,7 @@ async def test_discover_alarm_control_panel(
|
||||
"scene.hello_id",
|
||||
"Hello World 9",
|
||||
"scene",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/select/object/bla/config",
|
||||
@@ -1409,6 +1419,7 @@ async def test_discover_alarm_control_panel(
|
||||
"select.hello_id",
|
||||
"Hello World 10",
|
||||
"select",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/sensor/object/bla/config",
|
||||
@@ -1416,6 +1427,7 @@ async def test_discover_alarm_control_panel(
|
||||
"sensor.hello_id",
|
||||
"Hello World 11",
|
||||
"sensor",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/switch/object/bla/config",
|
||||
@@ -1423,6 +1435,7 @@ async def test_discover_alarm_control_panel(
|
||||
"switch.hello_id",
|
||||
"Hello World 12",
|
||||
"switch",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/light/object/bla/config",
|
||||
@@ -1430,6 +1443,7 @@ async def test_discover_alarm_control_panel(
|
||||
"light.hello_id",
|
||||
"Hello World 13",
|
||||
"light",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/light/object/bla/config",
|
||||
@@ -1437,6 +1451,7 @@ async def test_discover_alarm_control_panel(
|
||||
"light.hello_id",
|
||||
"Hello World 14",
|
||||
"light",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/light/object/bla/config",
|
||||
@@ -1444,6 +1459,7 @@ async def test_discover_alarm_control_panel(
|
||||
"light.hello_id",
|
||||
"Hello World 15",
|
||||
"light",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/vacuum/object/bla/config",
|
||||
@@ -1451,6 +1467,7 @@ async def test_discover_alarm_control_panel(
|
||||
"vacuum.hello_id",
|
||||
"Hello World 16",
|
||||
"vacuum",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/valve/object/bla/config",
|
||||
@@ -1458,6 +1475,7 @@ async def test_discover_alarm_control_panel(
|
||||
"valve.hello_id",
|
||||
"Hello World 17",
|
||||
"valve",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/lock/object/bla/config",
|
||||
@@ -1465,6 +1483,7 @@ async def test_discover_alarm_control_panel(
|
||||
"lock.hello_id",
|
||||
"Hello World 18",
|
||||
"lock",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/device_tracker/object/bla/config",
|
||||
@@ -1472,6 +1491,7 @@ async def test_discover_alarm_control_panel(
|
||||
"device_tracker.hello_id",
|
||||
"Hello World 19",
|
||||
"device_tracker",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/binary_sensor/object/bla/config",
|
||||
@@ -1480,6 +1500,7 @@ async def test_discover_alarm_control_panel(
|
||||
"binary_sensor.hello_id",
|
||||
"Hello World 2",
|
||||
"binary_sensor",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/button/object/bla/config",
|
||||
@@ -1489,6 +1510,7 @@ async def test_discover_alarm_control_panel(
|
||||
"button.hello_id",
|
||||
"Hello World button",
|
||||
"button",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/alarm_control_panel/object/bla/config",
|
||||
@@ -1497,6 +1519,7 @@ async def test_discover_alarm_control_panel(
|
||||
"alarm_control_panel.hello_id",
|
||||
"Hello World 1",
|
||||
"alarm_control_panel",
|
||||
False,
|
||||
),
|
||||
(
|
||||
"homeassistant/binary_sensor/object/bla/config",
|
||||
@@ -1505,6 +1528,7 @@ async def test_discover_alarm_control_panel(
|
||||
"binary_sensor.hello_id",
|
||||
"Hello World 2",
|
||||
"binary_sensor",
|
||||
False,
|
||||
),
|
||||
(
|
||||
"homeassistant/button/object/bla/config",
|
||||
@@ -1514,17 +1538,31 @@ async def test_discover_alarm_control_panel(
|
||||
"button.hello_id",
|
||||
"Hello World button",
|
||||
"button",
|
||||
False,
|
||||
),
|
||||
(
|
||||
"homeassistant/button/object/bla/config",
|
||||
'{ "name": "Hello World button", "def_ent_id": "button.hello_id", '
|
||||
'"obj_id": "hello_id_old", '
|
||||
'"o": {"name": "X2mqtt", "url": "https://example.com/x2mqtt"}, '
|
||||
'"command_topic": "test-topic" }',
|
||||
"button.hello_id",
|
||||
"Hello World button",
|
||||
"button",
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_discovery_with_object_id(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
topic: str,
|
||||
config: str,
|
||||
entity_id: str,
|
||||
name: str,
|
||||
domain: str,
|
||||
deprecation_warning: bool,
|
||||
) -> None:
|
||||
"""Test discovering an MQTT entity with object_id."""
|
||||
await mqtt_mock_entry()
|
||||
@@ -1537,6 +1575,11 @@ async def test_discovery_with_object_id(
|
||||
assert state.name == name
|
||||
assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered
|
||||
|
||||
assert (
|
||||
f"The configuration for entity {domain}.hello_id uses the deprecated option `object_id`"
|
||||
in caplog.text
|
||||
) is deprecation_warning
|
||||
|
||||
|
||||
async def test_discovery_with_default_entity_id_for_previous_deleted_entity(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
'pressure_unit': <UnitOfPressure.HPA: 'hPa'>,
|
||||
'temperature': 6.8,
|
||||
'temperature_unit': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
'visibility': 10.0,
|
||||
'visibility_unit': <UnitOfLength.KILOMETERS: 'km'>,
|
||||
'wind_bearing': 199,
|
||||
'wind_gust_speed': 42.52,
|
||||
@@ -136,6 +137,7 @@
|
||||
'supported_features': <WeatherEntityFeature: 2>,
|
||||
'temperature': 6.8,
|
||||
'temperature_unit': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
'visibility': 10.0,
|
||||
'visibility_unit': <UnitOfLength.KILOMETERS: 'km'>,
|
||||
'wind_bearing': 199,
|
||||
'wind_gust_speed': 42.52,
|
||||
@@ -200,6 +202,7 @@
|
||||
'supported_features': <WeatherEntityFeature: 3>,
|
||||
'temperature': 6.8,
|
||||
'temperature_unit': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
'visibility': 10.0,
|
||||
'visibility_unit': <UnitOfLength.KILOMETERS: 'km'>,
|
||||
'wind_bearing': 199,
|
||||
'wind_gust_speed': 42.52,
|
||||
|
||||
826
tests/components/plugwise/snapshots/test_climate.ambr
Normal file
826
tests/components/plugwise/snapshots/test_climate.ambr
Normal file
@@ -0,0 +1,826 @@
|
||||
# serializer version: 1
|
||||
# name: test_adam_2_climate_snapshot[platforms0-False-m_adam_heating][climate.bathroom-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'asleep',
|
||||
'vacation',
|
||||
'home',
|
||||
'away',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.bathroom',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 401>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': 'f871b8c4d63549319221e294e4f88074-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_2_climate_snapshot[platforms0-False-m_adam_heating][climate.bathroom-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 17.9,
|
||||
'friendly_name': 'Bathroom',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_mode': 'home',
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'asleep',
|
||||
'vacation',
|
||||
'home',
|
||||
'away',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 401>,
|
||||
'target_temp_step': 0.1,
|
||||
'temperature': 15.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.bathroom',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_2_climate_snapshot[platforms0-False-m_adam_heating][climate.living_room-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 1.0,
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'asleep',
|
||||
'vacation',
|
||||
'home',
|
||||
'away',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.living_room',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 401>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': 'f2bf9048bef64cc5b6d5110154e33c81-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_2_climate_snapshot[platforms0-False-m_adam_heating][climate.living_room-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 19.1,
|
||||
'friendly_name': 'Living room',
|
||||
'hvac_action': <HVACAction.PREHEATING: 'preheating'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 1.0,
|
||||
'preset_mode': 'home',
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'asleep',
|
||||
'vacation',
|
||||
'home',
|
||||
'away',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 401>,
|
||||
'target_temp_step': 0.1,
|
||||
'temperature': 20.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.living_room',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.badkamer-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.badkamer',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': '08963fec7c53423ca5680aa4cb502c63-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.badkamer-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 18.9,
|
||||
'friendly_name': 'Badkamer',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_mode': 'away',
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'target_temp_step': 0.1,
|
||||
'temperature': 14.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.badkamer',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.bios-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.bios',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': '12493538af164a409c6a1c79e38afe1c-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.bios-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 16.5,
|
||||
'friendly_name': 'Bios',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_mode': 'away',
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'target_temp_step': 0.1,
|
||||
'temperature': 13.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.bios',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.garage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.garage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': '446ac08dd04d4eff8ac57489757b7314-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.garage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 15.6,
|
||||
'friendly_name': 'Garage',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_mode': 'no_frost',
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'target_temp_step': 0.1,
|
||||
'temperature': 5.5,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.garage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.jessie-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.jessie',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': '82fa13f017d240daa0d0ea1775420f24-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.jessie-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 17.2,
|
||||
'friendly_name': 'Jessie',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_mode': 'asleep',
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'target_temp_step': 0.1,
|
||||
'temperature': 15.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.jessie',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.woonkamer-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.woonkamer',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': 'c50f167537524366a5af7aa3942feb1e-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_adam_climate_snapshot[platforms0][climate.woonkamer-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 20.9,
|
||||
'friendly_name': 'Woonkamer',
|
||||
'hvac_action': <HVACAction.HEATING: 'heating'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 0.0,
|
||||
'preset_mode': 'home',
|
||||
'preset_modes': list([
|
||||
'home',
|
||||
'asleep',
|
||||
'away',
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'target_temp_step': 0.1,
|
||||
'temperature': 21.5,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.woonkamer',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
# name: test_anna_2_climate_snapshot[platforms0-True-m_anna_heatpump_cooling][climate.anna-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 4.0,
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'home',
|
||||
'away',
|
||||
'asleep',
|
||||
'vacation',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.anna',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 18>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': '3cb70739631c4d17a86b8b12e8a5161b-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_anna_2_climate_snapshot[platforms0-True-m_anna_heatpump_cooling][climate.anna-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 26.3,
|
||||
'friendly_name': 'Anna',
|
||||
'hvac_action': <HVACAction.COOLING: 'cooling'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 4.0,
|
||||
'preset_mode': 'home',
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'home',
|
||||
'away',
|
||||
'asleep',
|
||||
'vacation',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 18>,
|
||||
'target_temp_high': 30.0,
|
||||
'target_temp_low': 20.5,
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.anna',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
# name: test_anna_3_climate_snapshot[platforms0-True-m_anna_heatpump_idle][climate.anna-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 4.0,
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'home',
|
||||
'away',
|
||||
'asleep',
|
||||
'vacation',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.anna',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 18>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': '3cb70739631c4d17a86b8b12e8a5161b-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_anna_3_climate_snapshot[platforms0-True-m_anna_heatpump_idle][climate.anna-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 23.0,
|
||||
'friendly_name': 'Anna',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 4.0,
|
||||
'preset_mode': 'home',
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'home',
|
||||
'away',
|
||||
'asleep',
|
||||
'vacation',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 18>,
|
||||
'target_temp_high': 30.0,
|
||||
'target_temp_low': 20.5,
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.anna',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
# name: test_anna_climate_snapshot[platforms0-True-anna_heatpump_heating][climate.anna-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 4.0,
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'home',
|
||||
'away',
|
||||
'asleep',
|
||||
'vacation',
|
||||
]),
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.anna',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'plugwise',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 18>,
|
||||
'translation_key': 'plugwise',
|
||||
'unique_id': '3cb70739631c4d17a86b8b12e8a5161b-climate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_anna_climate_snapshot[platforms0-True-anna_heatpump_heating][climate.anna-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 19.3,
|
||||
'friendly_name': 'Anna',
|
||||
'hvac_action': <HVACAction.HEATING: 'heating'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 4.0,
|
||||
'preset_mode': 'home',
|
||||
'preset_modes': list([
|
||||
'no_frost',
|
||||
'home',
|
||||
'away',
|
||||
'asleep',
|
||||
'vacation',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 18>,
|
||||
'target_temp_high': 30.0,
|
||||
'target_temp_low': 20.5,
|
||||
'target_temp_step': 0.1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.anna',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
@@ -6,180 +6,46 @@ from unittest.mock import MagicMock, patch
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from plugwise.exceptions import PlugwiseError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_PRESET_MODES,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TARGET_TEMP_STEP,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
PRESET_AWAY,
|
||||
PRESET_HOME,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
HA_PLUGWISE_SMILE_ASYNC_UPDATE = (
|
||||
"homeassistant.components.plugwise.coordinator.Smile.async_update"
|
||||
)
|
||||
|
||||
|
||||
async def test_adam_climate_entity_attributes(
|
||||
hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test creation of adam climate device environment."""
|
||||
state = hass.states.get("climate.woonkamer")
|
||||
assert state
|
||||
assert state.state == HVACMode.AUTO
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.AUTO, HVACMode.HEAT]
|
||||
assert ATTR_PRESET_MODES in state.attributes
|
||||
assert "no_frost" in state.attributes[ATTR_PRESET_MODES]
|
||||
assert PRESET_HOME in state.attributes[ATTR_PRESET_MODES]
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_HOME
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20.9
|
||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 17
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 21.5
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 0.0
|
||||
assert state.attributes[ATTR_MAX_TEMP] == 35.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 0.1
|
||||
|
||||
state = hass.states.get("climate.jessie")
|
||||
assert state
|
||||
assert state.state == HVACMode.AUTO
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.AUTO, HVACMode.HEAT]
|
||||
assert ATTR_PRESET_MODES in state.attributes
|
||||
assert "no_frost" in state.attributes[ATTR_PRESET_MODES]
|
||||
assert PRESET_HOME in state.attributes[ATTR_PRESET_MODES]
|
||||
assert state.attributes[ATTR_PRESET_MODE] == "asleep"
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 17.2
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 15.0
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 0.0
|
||||
assert state.attributes[ATTR_MAX_TEMP] == 35.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 0.1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [False], indirect=True)
|
||||
async def test_adam_2_climate_entity_attributes(
|
||||
@pytest.mark.parametrize("platforms", [(CLIMATE_DOMAIN,)])
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_adam_climate_snapshot(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_adam_heat_cool: MagicMock,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_smile_adam: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_platform: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test creation of adam climate device environment."""
|
||||
state = hass.states.get("climate.living_room")
|
||||
assert state
|
||||
assert state.state == HVACMode.HEAT
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.PREHEATING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.HEAT,
|
||||
]
|
||||
|
||||
state = hass.states.get("climate.bathroom")
|
||||
assert state
|
||||
assert state.state == HVACMode.AUTO
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.HEAT,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [True], indirect=True)
|
||||
async def test_adam_3_climate_entity_attributes(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_adam_heat_cool: MagicMock,
|
||||
init_integration: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test creation of adam climate device environment."""
|
||||
state = hass.states.get("climate.living_room")
|
||||
assert state
|
||||
assert state.state == HVACMode.COOL
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.COOL,
|
||||
]
|
||||
data = mock_smile_adam_heat_cool.async_update.return_value
|
||||
data["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "heating"
|
||||
data["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = HVACAction.HEATING
|
||||
data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["cooling_state"] = False
|
||||
data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["heating_state"] = True
|
||||
with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data):
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("climate.living_room")
|
||||
assert state
|
||||
assert state.state == HVACMode.HEAT
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.HEAT,
|
||||
]
|
||||
|
||||
data = mock_smile_adam_heat_cool.async_update.return_value
|
||||
data["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "cooling"
|
||||
data["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = HVACAction.COOLING
|
||||
data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["cooling_state"] = True
|
||||
data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["heating_state"] = False
|
||||
with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data):
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("climate.living_room")
|
||||
assert state
|
||||
assert state.state == HVACMode.COOL
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.COOL,
|
||||
]
|
||||
|
||||
|
||||
async def test_adam_climate_adjust_negative_testing(
|
||||
hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test PlugwiseError exception."""
|
||||
mock_smile_adam.set_temperature.side_effect = PlugwiseError
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: "climate.woonkamer", ATTR_TEMPERATURE: 25},
|
||||
blocking=True,
|
||||
)
|
||||
"""Test Adam climate snapshot."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||
|
||||
|
||||
async def test_adam_climate_entity_climate_changes(
|
||||
@@ -257,6 +123,95 @@ async def test_adam_climate_entity_climate_changes(
|
||||
)
|
||||
|
||||
|
||||
async def test_adam_climate_adjust_negative_testing(
|
||||
hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test PlugwiseError exception."""
|
||||
mock_smile_adam.set_temperature.side_effect = PlugwiseError
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: "climate.woonkamer", ATTR_TEMPERATURE: 25},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [False], indirect=True)
|
||||
@pytest.mark.parametrize("platforms", [(CLIMATE_DOMAIN,)])
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_adam_2_climate_snapshot(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_adam_heat_cool: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_platform: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test Adam 2 climate snapshot."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [True], indirect=True)
|
||||
async def test_adam_3_climate_entity_attributes(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_adam_heat_cool: MagicMock,
|
||||
init_integration: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test creation of adam climate device environment."""
|
||||
state = hass.states.get("climate.living_room")
|
||||
assert state
|
||||
assert state.state == HVACMode.COOL
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.COOL,
|
||||
]
|
||||
data = mock_smile_adam_heat_cool.async_update.return_value
|
||||
data["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "heating"
|
||||
data["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = HVACAction.HEATING
|
||||
data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["cooling_state"] = False
|
||||
data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["heating_state"] = True
|
||||
with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data):
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("climate.living_room")
|
||||
assert state
|
||||
assert state.state == HVACMode.HEAT
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.HEAT,
|
||||
]
|
||||
|
||||
data = mock_smile_adam_heat_cool.async_update.return_value
|
||||
data["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "cooling"
|
||||
data["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = HVACAction.COOLING
|
||||
data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["cooling_state"] = True
|
||||
data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["heating_state"] = False
|
||||
with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data):
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("climate.living_room")
|
||||
assert state
|
||||
assert state.state == HVACMode.COOL
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.COOL,
|
||||
]
|
||||
|
||||
|
||||
async def test_adam_climate_off_mode_change(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_adam_jip: MagicMock,
|
||||
@@ -313,68 +268,17 @@ async def test_adam_climate_off_mode_change(
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [True], indirect=True)
|
||||
async def test_anna_climate_entity_attributes(
|
||||
@pytest.mark.parametrize("platforms", [(CLIMATE_DOMAIN,)])
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_anna_climate_snapshot(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_anna: MagicMock,
|
||||
init_integration: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_platform: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test creation of anna climate device environment."""
|
||||
state = hass.states.get("climate.anna")
|
||||
assert state
|
||||
assert state.state == HVACMode.AUTO
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.AUTO, HVACMode.HEAT_COOL]
|
||||
|
||||
assert "no_frost" in state.attributes[ATTR_PRESET_MODES]
|
||||
assert PRESET_HOME in state.attributes[ATTR_PRESET_MODES]
|
||||
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 19.3
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_HOME
|
||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 18
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 30
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 20.5
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 4
|
||||
assert state.attributes[ATTR_MAX_TEMP] == 30
|
||||
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 0.1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_anna_heatpump_cooling"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [True], indirect=True)
|
||||
async def test_anna_2_climate_entity_attributes(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_anna: MagicMock,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test creation of anna climate device environment."""
|
||||
state = hass.states.get("climate.anna")
|
||||
assert state
|
||||
assert state.state == HVACMode.AUTO
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.AUTO,
|
||||
HVACMode.HEAT_COOL,
|
||||
]
|
||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 18
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 30
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 20.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_anna_heatpump_idle"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [True], indirect=True)
|
||||
async def test_anna_3_climate_entity_attributes(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_anna: MagicMock,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test creation of anna climate device environment."""
|
||||
state = hass.states.get("climate.anna")
|
||||
assert state
|
||||
assert state.state == HVACMode.AUTO
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVACMode.AUTO,
|
||||
HVACMode.HEAT_COOL,
|
||||
]
|
||||
"""Test Anna climate snapshot."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True)
|
||||
@@ -446,3 +350,33 @@ async def test_anna_climate_entity_climate_changes(
|
||||
state = hass.states.get("climate.anna")
|
||||
assert state.state == HVACMode.HEAT_COOL
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT_COOL]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_anna_heatpump_cooling"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [True], indirect=True)
|
||||
@pytest.mark.parametrize("platforms", [(CLIMATE_DOMAIN,)])
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_anna_2_climate_snapshot(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_anna: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_platform: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test Anna 2 climate snapshot."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["m_anna_heatpump_idle"], indirect=True)
|
||||
@pytest.mark.parametrize("cooling_present", [True], indirect=True)
|
||||
@pytest.mark.parametrize("platforms", [(CLIMATE_DOMAIN,)])
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_anna_3_climate_snapshot(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_anna: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_platform: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test Anna 3 climate snapshot."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||
|
||||
@@ -4957,14 +4957,14 @@ async def async_record_states(
|
||||
PRESSURE_SENSOR_ATTRIBUTES,
|
||||
"psi",
|
||||
"bar",
|
||||
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||
"Pa, bar, cbar, hPa, inHg, inH₂O, kPa, mbar, mmHg, psi",
|
||||
),
|
||||
(
|
||||
METRIC_SYSTEM,
|
||||
PRESSURE_SENSOR_ATTRIBUTES,
|
||||
"Pa",
|
||||
"bar",
|
||||
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||
"Pa, bar, cbar, hPa, inHg, inH₂O, kPa, mbar, mmHg, psi",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -5175,14 +5175,14 @@ async def test_validate_statistics_unit_ignore_device_class(
|
||||
PRESSURE_SENSOR_ATTRIBUTES,
|
||||
"psi",
|
||||
"bar",
|
||||
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||
"Pa, bar, cbar, hPa, inHg, inH₂O, kPa, mbar, mmHg, psi",
|
||||
),
|
||||
(
|
||||
METRIC_SYSTEM,
|
||||
PRESSURE_SENSOR_ATTRIBUTES,
|
||||
"Pa",
|
||||
"bar",
|
||||
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||
"Pa, bar, cbar, hPa, inHg, inH₂O, kPa, mbar, mmHg, psi",
|
||||
),
|
||||
(
|
||||
METRIC_SYSTEM,
|
||||
|
||||
@@ -1056,3 +1056,27 @@ RGBICWW_FLOOR_LAMP_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
connectable=True,
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
PLUG_MINI_EU_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
name="Plug Mini (EU)",
|
||||
manufacturer_data={
|
||||
2409: b"\x94\xa9\x90T\x85^?\xa1\x00\x00\x04\xe6\x00\x00\x00\x00",
|
||||
},
|
||||
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"?\x00\x00\x00"},
|
||||
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||
address="AA:BB:CC:DD:EE:FF",
|
||||
rssi=-60,
|
||||
source="local",
|
||||
advertisement=generate_advertisement_data(
|
||||
local_name="Plug Mini (EU)",
|
||||
manufacturer_data={
|
||||
2409: b"\x94\xa9\x90T\x85^?\xa1\x00\x00\x04\xe6\x00\x00\x00\x00",
|
||||
},
|
||||
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"?\x00\x00\x00"},
|
||||
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||
),
|
||||
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Plug Mini (EU)"),
|
||||
time=0,
|
||||
connectable=True,
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""Test the switchbot config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from switchbot import SwitchbotAccountConnectionError, SwitchbotAuthenticationError
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.switchbot.const import (
|
||||
CONF_ENCRYPTION_KEY,
|
||||
CONF_KEY_ID,
|
||||
@@ -41,6 +44,30 @@ from tests.common import MockConfigEntry
|
||||
DOMAIN = "switchbot"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_scanners_all_active() -> Generator[None]:
|
||||
"""Mock all scanners as active mode."""
|
||||
mock_scanner = Mock()
|
||||
mock_scanner.current_mode = BluetoothScanningMode.ACTIVE
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_current_scanners",
|
||||
return_value=[mock_scanner],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_scanners_all_passive() -> Generator[None]:
|
||||
"""Mock all scanners as passive mode."""
|
||||
mock_scanner = Mock()
|
||||
mock_scanner.current_mode = BluetoothScanningMode.PASSIVE
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_current_scanners",
|
||||
return_value=[mock_scanner],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_bluetooth_discovery(hass: HomeAssistant) -> None:
|
||||
"""Test discovery via bluetooth with a valid device."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -248,15 +275,23 @@ async def test_async_step_bluetooth_not_connectable(hass: HomeAssistant) -> None
|
||||
assert result["reason"] == "not_supported"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_wohand(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
@@ -279,6 +314,7 @@ async def test_user_setup_wohand(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_wohand_already_configured(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
entry = MockConfigEntry(
|
||||
@@ -292,29 +328,46 @@ async def test_user_setup_wohand_already_configured(hass: HomeAssistant) -> None
|
||||
unique_id="aabbccddeeff",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_wohand_replaces_ignored(hass: HomeAssistant) -> None:
|
||||
"""Test setting up a switchbot replaces an ignored entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, unique_id="aabbccddeeff", source=SOURCE_IGNORE
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
@@ -336,15 +389,23 @@ async def test_user_setup_wohand_replaces_ignored(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_wocurtain(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOCURTAIN_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
@@ -367,9 +428,16 @@ async def test_user_setup_wocurtain(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with valid address."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[
|
||||
@@ -379,11 +447,12 @@ async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None:
|
||||
WOHAND_SERVICE_INFO_NOT_CONNECTABLE,
|
||||
],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "select_device"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch_async_setup_entry() as mock_setup_entry:
|
||||
@@ -403,9 +472,16 @@ async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form and valid address and a bot with a password."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[
|
||||
@@ -414,11 +490,12 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) ->
|
||||
WOHAND_SERVICE_INFO_NOT_CONNECTABLE,
|
||||
],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "select_device"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@@ -447,15 +524,23 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) ->
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a bot with a password."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_ENCRYPTED_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "password"
|
||||
@@ -479,15 +564,23 @@ async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_woencrypted_key(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a lock."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOLOCK_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -545,15 +638,23 @@ async def test_user_setup_woencrypted_key(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_woencrypted_auth(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a lock."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOLOCK_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -618,17 +719,25 @@ async def test_user_setup_woencrypted_auth(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_woencrypted_auth_switchbot_api_down(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test the user initiated form for a lock when the switchbot api is down."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOLOCK_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -658,9 +767,16 @@ async def test_user_setup_woencrypted_auth_switchbot_api_down(
|
||||
assert result["description_placeholders"] == {"error_detail": "Switchbot API down"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a lock."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[
|
||||
@@ -668,11 +784,12 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None:
|
||||
WOHAND_SERVICE_ALT_ADDRESS_INFO,
|
||||
],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "select_device"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
@@ -719,14 +836,22 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_wosensor(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOSENSORTH_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
@@ -749,19 +874,236 @@ async def test_user_setup_wosensor(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_cloud_login(hass: HomeAssistant) -> None:
|
||||
"""Test the cloud login flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "cloud_login"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
|
||||
# Test successful cloud login
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.fetch_cloud_devices",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_SERVICE_INFO],
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "testpass",
|
||||
},
|
||||
)
|
||||
|
||||
# Should proceed to device selection with single device, so go to confirm
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
# Confirm device setup
|
||||
with patch_async_setup_entry():
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Bot EEFF"
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "AA:BB:CC:DD:EE:FF",
|
||||
CONF_SENSOR_TYPE: "bot",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_cloud_login_auth_failed(hass: HomeAssistant) -> None:
|
||||
"""Test the cloud login flow with authentication failure."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "cloud_login"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
|
||||
# Test authentication failure
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.fetch_cloud_devices",
|
||||
side_effect=SwitchbotAuthenticationError("Invalid credentials"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "wrongpass",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
assert result["errors"] == {"base": "auth_failed"}
|
||||
assert "Invalid credentials" in result["description_placeholders"]["error_detail"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_cloud_login_api_error(hass: HomeAssistant) -> None:
|
||||
"""Test the cloud login flow with API error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "cloud_login"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
|
||||
# Test API connection error
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.fetch_cloud_devices",
|
||||
side_effect=SwitchbotAccountConnectionError("API is down"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "testpass",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "api_error"
|
||||
assert result["description_placeholders"] == {"error_detail": "API is down"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_cloud_login_then_encrypted_device(hass: HomeAssistant) -> None:
|
||||
"""Test cloud login followed by encrypted device setup using saved credentials."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "cloud_login"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.fetch_cloud_devices",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOLOCK_SERVICE_INFO],
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "testpass",
|
||||
},
|
||||
)
|
||||
|
||||
# Should go to encrypted device choice menu
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
|
||||
# Choose encrypted auth
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "encrypted_auth"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_auth"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
None,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_auth"
|
||||
|
||||
with (
|
||||
patch_async_setup_entry() as mock_setup_entry,
|
||||
patch(
|
||||
"switchbot.SwitchbotLock.async_retrieve_encryption_key",
|
||||
return_value={
|
||||
CONF_KEY_ID: "ff",
|
||||
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
||||
},
|
||||
),
|
||||
patch("switchbot.SwitchbotLock.verify_encryption_key", return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "testpass",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Lock EEFF"
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||
CONF_KEY_ID: "ff",
|
||||
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
||||
CONF_SENSOR_TYPE: "lock",
|
||||
}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_no_devices(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_async_step_user_takes_precedence_over_discovery(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
@@ -774,13 +1116,20 @@ async def test_async_step_user_takes_precedence_over_discovery(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOCURTAIN_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
@@ -928,15 +1277,23 @@ async def test_options_flow_lock_pro(hass: HomeAssistant) -> None:
|
||||
assert entry.options[CONF_LOCK_NIGHTLATCH] is True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_worelay_switch_1pm_key(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a relay switch 1pm."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -976,15 +1333,23 @@ async def test_user_setup_worelay_switch_1pm_key(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_worelay_switch_1pm_auth(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a relay switch 1pm."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -1048,17 +1413,25 @@ async def test_user_setup_worelay_switch_1pm_auth(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_passive")
|
||||
async def test_user_setup_worelay_switch_1pm_auth_switchbot_api_down(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test the user initiated form for a relay switch 1pm when the switchbot api is down."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -1086,3 +1459,128 @@ async def test_user_setup_worelay_switch_1pm_auth_switchbot_api_down(
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "api_error"
|
||||
assert result["description_placeholders"] == {"error_detail": "Switchbot API down"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_scanners_all_active")
|
||||
async def test_user_skip_menu_when_all_scanners_active(hass: HomeAssistant) -> None:
|
||||
"""Test that menu is skipped when all scanners are in active mode."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_SERVICE_INFO],
|
||||
),
|
||||
patch_async_setup_entry() as mock_setup_entry,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
# Should skip menu and go directly to select_device -> confirm
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Bot EEFF"
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "AA:BB:CC:DD:EE:FF",
|
||||
CONF_SENSOR_TYPE: "bot",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_show_menu_when_passive_scanner_present(hass: HomeAssistant) -> None:
|
||||
"""Test that menu is shown when any scanner is in passive mode."""
|
||||
mock_scanner_active = Mock()
|
||||
mock_scanner_active.current_mode = BluetoothScanningMode.ACTIVE
|
||||
mock_scanner_passive = Mock()
|
||||
mock_scanner_passive.current_mode = BluetoothScanningMode.PASSIVE
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_current_scanners",
|
||||
return_value=[mock_scanner_active, mock_scanner_passive],
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_SERVICE_INFO],
|
||||
),
|
||||
patch_async_setup_entry() as mock_setup_entry,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
# Should show menu since not all scanners are active
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
assert set(result["menu_options"]) == {"cloud_login", "select_device"}
|
||||
|
||||
# Choose select_device from menu
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"next_step_id": "select_device"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
# Confirm the device
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Bot EEFF"
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "AA:BB:CC:DD:EE:FF",
|
||||
CONF_SENSOR_TYPE: "bot",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_show_menu_when_no_scanners(hass: HomeAssistant) -> None:
|
||||
"""Test that menu is shown when no scanners are available."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_current_scanners",
|
||||
return_value=[],
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_SERVICE_INFO],
|
||||
),
|
||||
patch_async_setup_entry() as mock_setup_entry,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
# Should show menu when no scanners are available
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
assert set(result["menu_options"]) == {"cloud_login", "select_device"}
|
||||
|
||||
# Choose select_device from menu
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"next_step_id": "select_device"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
# Confirm the device
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Bot EEFF"
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "AA:BB:CC:DD:EE:FF",
|
||||
CONF_SENSOR_TYPE: "bot",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
@@ -28,6 +28,7 @@ from . import (
|
||||
HUB3_SERVICE_INFO,
|
||||
HUBMINI_MATTER_SERVICE_INFO,
|
||||
LEAK_SERVICE_INFO,
|
||||
PLUG_MINI_EU_SERVICE_INFO,
|
||||
REMOTE_SERVICE_INFO,
|
||||
WOHAND_SERVICE_INFO,
|
||||
WOHUB2_SERVICE_INFO,
|
||||
@@ -542,3 +543,77 @@ async def test_evaporative_humidifier_sensor(hass: HomeAssistant) -> None:
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_plug_mini_eu_sensor(hass: HomeAssistant) -> None:
|
||||
"""Test setting up creates the plug mini eu sensor."""
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
inject_bluetooth_service_info(hass, PLUG_MINI_EU_SERVICE_INFO)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.switch.switchbot.SwitchbotRelaySwitch.get_basic_info",
|
||||
new=AsyncMock(
|
||||
return_value={
|
||||
"power": 500,
|
||||
"current": 0.5,
|
||||
"voltage": 230,
|
||||
"energy": 0.4,
|
||||
}
|
||||
),
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||
CONF_NAME: "test-name",
|
||||
CONF_SENSOR_TYPE: "plug_mini_eu",
|
||||
CONF_KEY_ID: "ff",
|
||||
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
||||
},
|
||||
unique_id="aabbccddeeaa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all("sensor")) == 5
|
||||
|
||||
power_sensor = hass.states.get("sensor.test_name_power")
|
||||
power_sensor_attrs = power_sensor.attributes
|
||||
assert power_sensor.state == "500"
|
||||
assert power_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Power"
|
||||
assert power_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "W"
|
||||
assert power_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
|
||||
|
||||
voltage_sensor = hass.states.get("sensor.test_name_voltage")
|
||||
voltage_sensor_attrs = voltage_sensor.attributes
|
||||
assert voltage_sensor.state == "230"
|
||||
assert voltage_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Voltage"
|
||||
assert voltage_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "V"
|
||||
assert voltage_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
|
||||
|
||||
current_sensor = hass.states.get("sensor.test_name_current")
|
||||
current_sensor_attrs = current_sensor.attributes
|
||||
assert current_sensor.state == "0.5"
|
||||
assert current_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Current"
|
||||
assert current_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "A"
|
||||
assert current_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
|
||||
|
||||
energy_sensor = hass.states.get("sensor.test_name_energy")
|
||||
energy_sensor_attrs = energy_sensor.attributes
|
||||
assert energy_sensor.state == "0.4"
|
||||
assert energy_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Energy"
|
||||
assert energy_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "kWh"
|
||||
assert energy_sensor_attrs[ATTR_STATE_CLASS] == "total_increasing"
|
||||
|
||||
rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal")
|
||||
rssi_sensor_attrs = rssi_sensor.attributes
|
||||
assert rssi_sensor.state == "-60"
|
||||
assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Bluetooth signal"
|
||||
assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm"
|
||||
assert rssi_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user