mirror of
https://github.com/home-assistant/core.git
synced 2025-09-21 10:59:32 +00:00
Compare commits
2 Commits
hassfest-e
...
mqtt-json-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e2e907963a | ||
![]() |
104ff0f1e1 |
@@ -19,6 +19,7 @@ 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 (
|
||||
@@ -248,7 +249,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"] = async_sign_path(
|
||||
service_result["url"] = get_url(hass) + async_sign_path(
|
||||
hass,
|
||||
f"/api/{DOMAIN}/images/{filename}",
|
||||
timedelta(seconds=IMAGE_EXPIRY_TIME or 1800),
|
||||
|
@@ -497,18 +497,16 @@ class BayesianBinarySensor(BinarySensorEntity):
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Observation for entity '%s' returned None, it will not be used"
|
||||
" for updating Bayesian sensor '%s'"
|
||||
" for Bayesian updating"
|
||||
),
|
||||
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 updating Bayesian sensor '%s'"
|
||||
" boolean, it will not be used for Bayesian updating"
|
||||
),
|
||||
self.entity_id,
|
||||
)
|
||||
# the prior has been updated and is now the posterior
|
||||
return prior
|
||||
|
@@ -18,10 +18,8 @@ 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,40 +2,28 @@
|
||||
|
||||
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_PORT, CONF_TYPE, Platform
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import (
|
||||
CONF_COMMUNITY,
|
||||
DEFAULT_COMMUNITY,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
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, port, community, printer_type=printer_type, snmp_engine=snmp_engine
|
||||
host, printer_type=printer_type, snmp_engine=snmp_engine
|
||||
)
|
||||
except (ConnectionError, SnmpError, TimeoutError) as error:
|
||||
raise ConfigEntryNotReady(
|
||||
@@ -60,22 +48,3 @@ 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,65 +9,21 @@ 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_PORT, CONF_TYPE
|
||||
from homeassistant.const import CONF_HOST, 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 (
|
||||
CONF_COMMUNITY,
|
||||
DEFAULT_COMMUNITY,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
PRINTER_TYPES,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from .const import DOMAIN, PRINTER_TYPES
|
||||
|
||||
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(
|
||||
@@ -79,12 +35,7 @@ async def validate_input(
|
||||
|
||||
snmp_engine = await async_get_snmp_engine(hass)
|
||||
|
||||
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,
|
||||
)
|
||||
brother = await Brother.create(user_input[CONF_HOST], snmp_engine=snmp_engine)
|
||||
await brother.async_update()
|
||||
|
||||
if expected_mac is not None and brother.serial.lower() != expected_mac:
|
||||
@@ -97,7 +48,6 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Brother Printer."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
@@ -176,11 +126,13 @@ 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, **user_input},
|
||||
data={CONF_HOST: self.host, CONF_TYPE: user_input[CONF_TYPE]},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
data_schema=ZEROCONF_SCHEMA,
|
||||
data_schema=vol.Schema(
|
||||
{vol.Optional(CONF_TYPE, default="laser"): vol.In(PRINTER_TYPES)}
|
||||
),
|
||||
description_placeholders={
|
||||
"serial_number": self.brother.serial,
|
||||
"model": self.brother.model,
|
||||
@@ -208,7 +160,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data_updates=user_input,
|
||||
data_updates={CONF_HOST: user_input[CONF_HOST]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
@@ -10,10 +10,3 @@ 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,21 +8,7 @@
|
||||
"type": "Type of the printer"
|
||||
},
|
||||
"data_description": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
"host": "The hostname or IP address of the Brother printer to control."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
@@ -30,22 +16,6 @@
|
||||
"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": {
|
||||
@@ -55,19 +25,6 @@
|
||||
},
|
||||
"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.1"],
|
||||
"requirements": ["hass-nabucasa==1.1.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -6,6 +6,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/derivative",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal"
|
||||
"iot_class": "calculated"
|
||||
}
|
||||
|
@@ -176,7 +176,7 @@
|
||||
"name": "Max connection upload throughput"
|
||||
},
|
||||
"cpu_temperature": {
|
||||
"name": "CPU temperature"
|
||||
"name": "CPU Temperature"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -6,6 +6,5 @@
|
||||
"dependencies": ["sensor", "switch"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic_thermostat",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal"
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
@@ -3,13 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import codecs
|
||||
from collections.abc import AsyncGenerator, AsyncIterator, Callable
|
||||
from dataclasses import dataclass, replace
|
||||
from dataclasses import replace
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from google.genai import Client
|
||||
from google.genai.errors import APIError, ClientError
|
||||
@@ -28,7 +27,6 @@ from google.genai.types import (
|
||||
PartUnionDict,
|
||||
SafetySetting,
|
||||
Schema,
|
||||
ThinkingConfig,
|
||||
Tool,
|
||||
ToolListUnion,
|
||||
)
|
||||
@@ -203,30 +201,6 @@ 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
|
||||
@@ -235,91 +209,32 @@ def _convert_content(
|
||||
),
|
||||
) -> Content:
|
||||
"""Convert HA content to Google content."""
|
||||
if content.role != "assistant":
|
||||
if content.role != "assistant" or not content.tool_calls:
|
||||
role = "model" if content.role == "assistant" else content.role
|
||||
return Content(
|
||||
role=content.role,
|
||||
parts=[Part.from_text(text=content.content if content.content else "")],
|
||||
role=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:
|
||||
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
|
||||
parts.append(Part.from_text(text=content.content))
|
||||
|
||||
if content.tool_calls:
|
||||
for index, tool_call in enumerate(content.tool_calls):
|
||||
parts.append(
|
||||
parts.extend(
|
||||
[
|
||||
Part.from_function_call(
|
||||
name=tool_call.tool_name,
|
||||
args=_escape_decode(tool_call.tool_args),
|
||||
)
|
||||
)
|
||||
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
|
||||
)
|
||||
for tool_call in content.tool_calls
|
||||
]
|
||||
)
|
||||
|
||||
return Content(role="model", parts=parts)
|
||||
|
||||
@@ -328,20 +243,14 @@ 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:
|
||||
if part_details:
|
||||
yield {"native": ContentDetails(part_details=part_details)}
|
||||
part_details = []
|
||||
yield {"role": "assistant"}
|
||||
chunk["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:
|
||||
@@ -375,62 +284,23 @@ async def _transform_stream(
|
||||
else []
|
||||
)
|
||||
|
||||
content = "".join([part.text for part in response_parts if part.text])
|
||||
tool_calls = []
|
||||
for part in response_parts:
|
||||
chunk: conversation.AssistantContentDeltaDict = {}
|
||||
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)
|
||||
)
|
||||
|
||||
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)}
|
||||
if tool_calls:
|
||||
chunk["tool_calls"] = tool_calls
|
||||
|
||||
chunk["content"] = content
|
||||
yield chunk
|
||||
except (
|
||||
APIError,
|
||||
ValueError,
|
||||
@@ -652,7 +522,6 @@ 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: list[str] = msg["entity_ids"]
|
||||
entity_ids: 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, HomeeNode
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
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, setup_homee_platform
|
||||
from .helpers import get_name_for_enum
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -60,29 +60,18 @@ 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."""
|
||||
|
||||
await setup_homee_platform(
|
||||
add_alarm_control_panel_entities, async_add_entities, config_entry
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""The Homee binary sensor platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -14,7 +14,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -153,31 +152,20 @@ BINARY_SENSOR_DESCRIPTIONS: dict[AttributeType, BinarySensorEntityDescription] =
|
||||
}
|
||||
|
||||
|
||||
async def add_binary_sensor_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee binary sensor entities."""
|
||||
async_add_entities(
|
||||
HomeeBinarySensor(
|
||||
attribute, config_entry, BINARY_SENSOR_DESCRIPTIONS[attribute.type]
|
||||
)
|
||||
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,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the homee platform for the binary sensor component."""
|
||||
"""Add the Homee platform for the binary sensor component."""
|
||||
|
||||
await setup_homee_platform(
|
||||
add_binary_sensor_entities, async_add_entities, config_entry
|
||||
async_add_devices(
|
||||
HomeeBinarySensor(
|
||||
attribute, config_entry, BINARY_SENSOR_DESCRIPTIONS[attribute.type]
|
||||
)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in BINARY_SENSOR_DESCRIPTIONS and not attribute.editable
|
||||
)
|
||||
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""The homee button platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
@@ -14,7 +14,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -40,28 +39,19 @@ 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."""
|
||||
|
||||
await setup_homee_platform(add_button_entities, async_add_entities, config_entry)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class HomeeButton(HomeeEntity, ButtonEntity):
|
||||
|
@@ -21,7 +21,6 @@ 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
|
||||
|
||||
@@ -32,27 +31,18 @@ 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_entities: AddConfigEntryEntitiesCallback,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the climate component."""
|
||||
|
||||
await setup_homee_platform(add_climate_entities, async_add_entities, config_entry)
|
||||
async_add_devices(
|
||||
HomeeClimate(node, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
if node.profile in CLIMATE_PROFILES
|
||||
)
|
||||
|
||||
|
||||
class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
|
@@ -18,7 +18,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeNodeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -78,25 +77,18 @@ 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_entities: AddConfigEntryEntitiesCallback,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the homee platform for the cover integration."""
|
||||
|
||||
await setup_homee_platform(add_cover_entities, async_add_entities, config_entry)
|
||||
async_add_devices(
|
||||
HomeeCover(node, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
if is_cover_node(node)
|
||||
)
|
||||
|
||||
|
||||
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, HomeeNode
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.event import (
|
||||
EventDeviceClass,
|
||||
@@ -13,7 +13,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -50,22 +49,6 @@ 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,
|
||||
@@ -73,7 +56,14 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add event entities for homee."""
|
||||
|
||||
await setup_homee_platform(add_event_entities, async_add_entities, config_entry)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class HomeeEvent(HomeeEntity, EventEntity):
|
||||
|
@@ -19,32 +19,22 @@ 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_entities: AddConfigEntryEntitiesCallback,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Homee fan platform."""
|
||||
|
||||
await setup_homee_platform(add_fan_entities, async_add_entities, config_entry)
|
||||
async_add_devices(
|
||||
HomeeFan(node, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
if node.profile == NodeProfile.VENTILATION_CONTROL
|
||||
)
|
||||
|
||||
|
||||
class HomeeFan(HomeeNodeEntity, FanEntity):
|
||||
|
@@ -1,42 +1,11 @@
|
||||
"""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,7 +24,6 @@ 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,
|
||||
@@ -86,28 +85,19 @@ 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."""
|
||||
|
||||
await setup_homee_platform(add_light_entities, async_add_entities, config_entry)
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
class HomeeLight(HomeeNodeEntity, LightEntity):
|
||||
|
@@ -3,7 +3,6 @@
|
||||
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
|
||||
@@ -11,33 +10,24 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import get_name_for_enum, setup_homee_platform
|
||||
from .helpers import get_name_for_enum
|
||||
|
||||
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_entities: AddConfigEntryEntitiesCallback,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the homee platform for the lock component."""
|
||||
"""Add the Homee platform for the lock component."""
|
||||
|
||||
await setup_homee_platform(add_lock_entities, async_add_entities, config_entry)
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
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, HomeeNode
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
@@ -18,7 +18,6 @@ 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
|
||||
|
||||
@@ -137,28 +136,19 @@ 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."""
|
||||
|
||||
await setup_homee_platform(add_number_entities, async_add_entities, config_entry)
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
class HomeeNumber(HomeeEntity, NumberEntity):
|
||||
|
@@ -54,7 +54,7 @@ rules:
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: done
|
||||
dynamic-devices: todo
|
||||
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, HomeeNode
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -10,7 +10,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -28,28 +27,19 @@ 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."""
|
||||
|
||||
await setup_homee_platform(add_select_entities, async_add_entities, config_entry)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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, setup_homee_platform
|
||||
from .helpers import get_name_for_enum
|
||||
|
||||
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_entities: AddConfigEntryEntitiesCallback,
|
||||
async_add_devices: 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
|
||||
) -> list[HomeeSensor]:
|
||||
) -> None:
|
||||
"""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,9 +325,7 @@ async def async_setup_entry(
|
||||
f"deprecated_entity_{entity_uid}",
|
||||
)
|
||||
elif entity_entry:
|
||||
deprecated_entities.append(
|
||||
HomeeSensor(attribute, config_entry, description)
|
||||
)
|
||||
devices.append(HomeeSensor(attribute, config_entry, description))
|
||||
if entity_used_in(hass, entity_id):
|
||||
async_create_issue(
|
||||
hass,
|
||||
@@ -344,42 +342,27 @@ async def async_setup_entry(
|
||||
"entity": entity_id,
|
||||
},
|
||||
)
|
||||
return deprecated_entities
|
||||
|
||||
async def add_sensor_entities(
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
nodes: list[HomeeNode],
|
||||
) -> None:
|
||||
"""Add homee sensor entities."""
|
||||
entities: list[HomeeSensor | HomeeNodeSensor] = []
|
||||
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
|
||||
)
|
||||
|
||||
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]
|
||||
)
|
||||
# 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]
|
||||
)
|
||||
)
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
await setup_homee_platform(add_sensor_entities, async_add_entities, config_entry)
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class HomeeSensor(HomeeEntity, SensorEntity):
|
||||
|
@@ -3,7 +3,6 @@
|
||||
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
|
||||
@@ -11,33 +10,23 @@ 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_entities: AddConfigEntryEntitiesCallback,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add siren entities for homee."""
|
||||
|
||||
await setup_homee_platform(add_siren_entities, async_add_entities, config_entry)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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, HomeeNode
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
@@ -19,7 +19,6 @@ 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
|
||||
|
||||
@@ -66,35 +65,27 @@ 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_entities: AddConfigEntryEntitiesCallback,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform for the Homee component."""
|
||||
|
||||
await setup_homee_platform(add_switch_entities, async_add_entities, config_entry)
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class HomeeSwitch(HomeeEntity, SwitchEntity):
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""The Homee valve platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.valve import (
|
||||
ValveDeviceClass,
|
||||
@@ -14,7 +14,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import setup_homee_platform
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -26,28 +25,19 @@ 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."""
|
||||
|
||||
await setup_homee_platform(add_valve_entities, async_add_entities, config_entry)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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_led_brightness():
|
||||
if entry.runtime_data.data.device.supports_state():
|
||||
async_add_entities([HWEnergyNumberEntity(entry.runtime_data)])
|
||||
|
||||
|
||||
|
@@ -36,13 +36,12 @@ 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 after the WebSocket connection
|
||||
is ready.
|
||||
but only for mowers that support message events.
|
||||
"""
|
||||
coordinator = config_entry.runtime_data
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
restored_mowers: set[str] = {
|
||||
restored_mowers = {
|
||||
entry.unique_id.removesuffix("_message")
|
||||
for entry in er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
@@ -50,20 +49,14 @@ async def async_setup_entry(
|
||||
if entry.domain == EVENT_DOMAIN
|
||||
}
|
||||
|
||||
@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)
|
||||
async_add_entities(
|
||||
AutomowerMessageEventEntity(mower_id, coordinator)
|
||||
for mower_id in restored_mowers
|
||||
if mower_id in coordinator.data
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_message(msg: SingleMessageData) -> None:
|
||||
"""Add entity dynamically if a new mower sends messages."""
|
||||
if msg.id in restored_mowers:
|
||||
return
|
||||
|
||||
@@ -85,17 +78,11 @@ 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 = (
|
||||
websocket_alive
|
||||
if websocket_alive is not None
|
||||
else coordinator.websocket_alive
|
||||
)
|
||||
self.websocket_alive: bool = coordinator.websocket_alive
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
@@ -52,10 +52,8 @@ async def async_get_config_entry_diagnostics(
|
||||
try:
|
||||
CONFIG_SCHEMA(raw_config)
|
||||
except vol.Invalid as ex:
|
||||
diag["yaml_configuration_error"] = str(ex)
|
||||
diag["configuration_error"] = str(ex)
|
||||
else:
|
||||
diag["yaml_configuration_error"] = None
|
||||
|
||||
diag["config_store"] = knx_module.config_store.data
|
||||
diag["configuration_error"] = None
|
||||
|
||||
return diag
|
||||
|
@@ -285,19 +285,13 @@ 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_COLOR, CONF_GA_GREEN_BRIGHTNESS
|
||||
),
|
||||
group_address_brightness_green=conf.get_write(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_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_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_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.Optional(CONF_GA_BLUE_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
"section_white": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_WHITE_SWITCH): GASelector(
|
||||
vol.Optional(CONF_GA_BLUE_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
"section_white": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
vol.Optional(CONF_GA_WHITE_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
},
|
||||
),
|
||||
GroupSelectOption(
|
||||
|
@@ -36,9 +36,6 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"food_dispensed_today": {
|
||||
"default": "mdi:counter"
|
||||
},
|
||||
"hopper_status": {
|
||||
"default": "mdi:filter",
|
||||
"state": {
|
||||
|
@@ -163,17 +163,6 @@ 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",
|
||||
@@ -192,12 +181,6 @@ 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,10 +59,6 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"food_dispensed_today": {
|
||||
"name": "Food dispensed today",
|
||||
"unit_of_measurement": "cups"
|
||||
},
|
||||
"food_level": {
|
||||
"name": "Food level"
|
||||
},
|
||||
@@ -86,9 +82,6 @@
|
||||
"litter_level": {
|
||||
"name": "Litter level"
|
||||
},
|
||||
"next_feeding": {
|
||||
"name": "Next feeding"
|
||||
},
|
||||
"pet_weight": {
|
||||
"name": "Pet weight"
|
||||
},
|
||||
|
@@ -83,9 +83,7 @@ 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, range_stop=49
|
||||
)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
|
||||
return self
|
||||
|
||||
|
||||
|
@@ -270,7 +270,6 @@ 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,
|
||||
),
|
||||
),
|
||||
@@ -308,7 +307,6 @@ 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,
|
||||
),
|
||||
),
|
||||
@@ -620,8 +618,6 @@ 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:
|
||||
@@ -928,58 +924,3 @@ 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
|
||||
|
@@ -73,6 +73,7 @@ ABBREVIATIONS = {
|
||||
"fan_mode_stat_t": "fan_mode_state_topic",
|
||||
"frc_upd": "force_update",
|
||||
"g_tpl": "green_template",
|
||||
"grp": "group",
|
||||
"hs_cmd_t": "hs_command_topic",
|
||||
"hs_cmd_tpl": "hs_command_template",
|
||||
"hs_stat_t": "hs_state_topic",
|
||||
|
@@ -106,6 +106,7 @@ CONF_FLASH_TIME_SHORT = "flash_time_short"
|
||||
CONF_GET_POSITION_TEMPLATE = "position_template"
|
||||
CONF_GET_POSITION_TOPIC = "position_topic"
|
||||
CONF_GREEN_TEMPLATE = "green_template"
|
||||
CONF_GROUP = "group"
|
||||
CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
|
||||
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
|
||||
CONF_HS_STATE_TOPIC = "hs_state_topic"
|
||||
|
@@ -546,7 +546,7 @@ class MqttAttributesMixin(Entity):
|
||||
_LOGGER.warning("Erroneous JSON: %s", payload)
|
||||
else:
|
||||
if isinstance(json_dict, dict):
|
||||
filtered_dict = {
|
||||
filtered_dict: dict[str, Any] = {
|
||||
k: v
|
||||
for k, v in json_dict.items()
|
||||
if k not in MQTT_ATTRIBUTES_BLOCKED
|
||||
@@ -1373,6 +1373,7 @@ class MqttEntity(
|
||||
_attr_force_update = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_default_entity: str | None = None
|
||||
_default_name: str | None
|
||||
_entity_id_format: str
|
||||
_update_registry_entity_id: str | None = None
|
||||
@@ -1445,7 +1446,7 @@ class MqttEntity(
|
||||
},
|
||||
translation_key="deprecated_object_id",
|
||||
)
|
||||
elif CONF_DEFAULT_ENTITY_ID not in self._config:
|
||||
else:
|
||||
if CONF_ORIGIN in self._config:
|
||||
origin_name = self._config[CONF_ORIGIN][CONF_NAME]
|
||||
url = self._config[CONF_ORIGIN].get(CONF_URL)
|
||||
@@ -1609,7 +1610,7 @@ class MqttEntity(
|
||||
self._attr_entity_registry_enabled_default = bool(
|
||||
config.get(CONF_ENABLED_BY_DEFAULT)
|
||||
)
|
||||
self._attr_icon = config.get(CONF_ICON)
|
||||
self._attr_icon = config.get(CONF_ICON, self._default_entity)
|
||||
self._attr_entity_picture = config.get(CONF_ENTITY_PICTURE)
|
||||
# Set the entity name if needed
|
||||
self._set_entity_name(config)
|
||||
|
@@ -23,6 +23,7 @@ from homeassistant.components.light import (
|
||||
ATTR_XY_COLOR,
|
||||
DEFAULT_MAX_KELVIN,
|
||||
DEFAULT_MIN_KELVIN,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
FLASH_LONG,
|
||||
FLASH_SHORT,
|
||||
@@ -34,6 +35,7 @@ from homeassistant.components.light import (
|
||||
valid_supported_color_modes,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_COLOR_TEMP,
|
||||
CONF_EFFECT,
|
||||
@@ -45,7 +47,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, VolSchemaType
|
||||
@@ -62,6 +64,7 @@ from ..const import (
|
||||
CONF_FLASH,
|
||||
CONF_FLASH_TIME_LONG,
|
||||
CONF_FLASH_TIME_SHORT,
|
||||
CONF_GROUP,
|
||||
CONF_MAX_KELVIN,
|
||||
CONF_MAX_MIREDS,
|
||||
CONF_MIN_KELVIN,
|
||||
@@ -77,6 +80,7 @@ from ..const import (
|
||||
DEFAULT_FLASH_TIME_LONG,
|
||||
DEFAULT_FLASH_TIME_SHORT,
|
||||
DEFAULT_WHITE_SCALE,
|
||||
DOMAIN,
|
||||
)
|
||||
from ..entity import MqttEntity
|
||||
from ..models import ReceiveMessage
|
||||
@@ -91,8 +95,6 @@ from .schema_basic import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "mqtt_json"
|
||||
|
||||
DEFAULT_NAME = "MQTT JSON Light"
|
||||
|
||||
DEFAULT_FLASH = True
|
||||
@@ -115,6 +117,7 @@ _PLATFORM_SCHEMA_BASE = (
|
||||
vol.Optional(
|
||||
CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT
|
||||
): cv.positive_int,
|
||||
vol.Optional(CONF_GROUP): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_MAX_MIREDS): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.positive_int,
|
||||
vol.Optional(CONF_MAX_KELVIN): cv.positive_int,
|
||||
@@ -171,16 +174,20 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
|
||||
_fixed_color_mode: ColorMode | str | None = None
|
||||
_flash_times: dict[str, int | None]
|
||||
_group_member_entity_ids_resolved: bool
|
||||
_topic: dict[str, str | None]
|
||||
_optimistic: bool
|
||||
_extra_state_attributes: dict[str, Any] | None = None
|
||||
|
||||
@staticmethod
|
||||
def config_schema() -> VolSchemaType:
|
||||
"""Return the config schema."""
|
||||
return DISCOVERY_SCHEMA_JSON
|
||||
|
||||
@callback
|
||||
def _setup_from_config(self, config: ConfigType) -> None:
|
||||
"""(Re)Setup the entity."""
|
||||
self._group_member_entity_ids_resolved = False
|
||||
self._color_temp_kelvin = config[CONF_COLOR_TEMP_KELVIN]
|
||||
self._attr_min_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(max_mireds)
|
||||
@@ -226,6 +233,43 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
else:
|
||||
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
|
||||
self._update_extra_state_and_group_info()
|
||||
|
||||
@callback
|
||||
def _update_extra_state_and_group_info(self) -> None:
|
||||
"""Set the entity_id property if the light represents a group of lights.
|
||||
|
||||
Setting entity_id in the extra state attributes will show the discover the light
|
||||
as a group and allow to control the member light manually.
|
||||
"""
|
||||
if CONF_GROUP not in self._config:
|
||||
self._attr_extra_state_attributes = self._extra_state_attributes or {}
|
||||
self._default_entity = None
|
||||
return
|
||||
self._default_entity = "mdi:lightbulb-group"
|
||||
entity_registry = er.async_get(self.hass)
|
||||
_group_entity_ids: list[str] = []
|
||||
self._group_member_entity_ids_resolved = True
|
||||
for resource_id in self._config[CONF_GROUP]:
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
LIGHT_DOMAIN, DOMAIN, resource_id
|
||||
):
|
||||
_group_entity_ids.append(entity_id)
|
||||
else:
|
||||
# The ID is not (yet) resolved, so we retry at the next state update.
|
||||
# This can only happen the first time the member entities
|
||||
# are discovered, and added to the entity registry.
|
||||
self._group_member_entity_ids_resolved = False
|
||||
|
||||
entity_attribute: dict[str, Any] = {ATTR_ENTITY_ID: _group_entity_ids}
|
||||
if self._extra_state_attributes is None:
|
||||
self._attr_extra_state_attributes = entity_attribute
|
||||
return
|
||||
|
||||
self._attr_extra_state_attributes = (
|
||||
self._extra_state_attributes | entity_attribute
|
||||
)
|
||||
|
||||
def _update_color(self, values: dict[str, Any]) -> None:
|
||||
color_mode: str = values["color_mode"]
|
||||
if not self._supports_color_mode(color_mode):
|
||||
@@ -327,6 +371,21 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
with suppress(KeyError):
|
||||
self._attr_effect = cast(str, values["effect"])
|
||||
|
||||
# We update the group info on a received state up, as member
|
||||
if not self._group_member_entity_ids_resolved:
|
||||
self._update_extra_state_and_group_info()
|
||||
|
||||
@callback
|
||||
def _process_update_extra_state_attributes(
|
||||
self, extra_state_attributes: dict[str, Any]
|
||||
) -> None:
|
||||
"""Process an the extra state attributes update.
|
||||
|
||||
Add extracted group members if the light represents a group.
|
||||
"""
|
||||
self._extra_state_attributes = extra_state_attributes
|
||||
self._update_extra_state_and_group_info()
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
@@ -43,8 +43,6 @@ 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,6 +35,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
ATTR_STEP_VALIDATION,
|
||||
ATTR_VALUE,
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
@@ -183,7 +184,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_MODE}
|
||||
{ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_STEP_VALIDATION, ATTR_MODE}
|
||||
)
|
||||
|
||||
entity_description: NumberEntityDescription
|
||||
|
@@ -57,6 +57,7 @@ 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
|
||||
@@ -327,7 +328,6 @@ 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 native_visibility(self) -> float | None:
|
||||
def visibility(self) -> float | str | 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 (H.265 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 (h265 encoding) while RTMP and FLV do not. FLV is the least demanding on the camera."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -361,7 +361,6 @@ class SensorDeviceClass(StrEnum):
|
||||
- `Pa`, `hPa`, `kPa`
|
||||
- `inHg`
|
||||
- `psi`
|
||||
- `inH₂O`
|
||||
"""
|
||||
|
||||
REACTIVE_ENERGY = "reactive_energy"
|
||||
|
@@ -97,7 +97,6 @@ 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,
|
||||
@@ -128,7 +127,6 @@ 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,15 +11,12 @@ 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 (
|
||||
@@ -90,8 +87,6 @@ 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
|
||||
@@ -181,17 +176,9 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the SwitchBot API auth step."""
|
||||
errors: dict[str, str] = {}
|
||||
errors = {}
|
||||
assert self._discovered_adv is not None
|
||||
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,
|
||||
}
|
||||
|
||||
description_placeholders = {}
|
||||
if user_input is not None:
|
||||
model: SwitchbotModel = self._discovered_adv.data["modelName"]
|
||||
cls = ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS[model]
|
||||
@@ -213,9 +200,6 @@ 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)
|
||||
|
||||
@@ -255,7 +239,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the encryption key step."""
|
||||
errors: dict[str, str] = {}
|
||||
errors = {}
|
||||
assert self._discovered_adv is not None
|
||||
if user_input is not None:
|
||||
model: SwitchbotModel = self._discovered_adv.data["modelName"]
|
||||
@@ -324,73 +308,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""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."""
|
||||
"""Handle the user step to pick discovered device."""
|
||||
errors: dict[str, str] = {}
|
||||
device_adv: SwitchBotAdvertisement | None = None
|
||||
if user_input is not None:
|
||||
@@ -415,7 +333,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_confirm()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="select_device",
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
|
@@ -53,7 +53,6 @@ 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 = {
|
||||
@@ -86,7 +85,6 @@ 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 = {
|
||||
@@ -120,7 +118,6 @@ ENCRYPTED_MODELS = {
|
||||
SwitchbotModel.STRIP_LIGHT_3,
|
||||
SwitchbotModel.RGBICWW_STRIP_LIGHT,
|
||||
SwitchbotModel.RGBICWW_FLOOR_LAMP,
|
||||
SwitchbotModel.PLUG_MINI_EU,
|
||||
}
|
||||
|
||||
ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
|
||||
@@ -139,7 +136,6 @@ 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,24 +3,6 @@
|
||||
"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,7 +31,6 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.FAN,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
@@ -58,7 +57,6 @@ 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
|
||||
@@ -257,19 +255,6 @@ 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,12 +20,6 @@ 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."""
|
||||
@@ -39,21 +33,3 @@ 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]
|
||||
|
@@ -1,155 +0,0 @@
|
||||
"""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,22 +34,6 @@
|
||||
"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,7 +160,6 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
"Motion Sensor": (BATTERY_DESCRIPTION,),
|
||||
"Contact Sensor": (BATTERY_DESCRIPTION,),
|
||||
"Water Detector": (BATTERY_DESCRIPTION,),
|
||||
"Humidifier": (TEMPERATURE_DESCRIPTION,),
|
||||
}
|
||||
|
||||
|
||||
|
@@ -36,22 +36,6 @@
|
||||
"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,9 +752,6 @@
|
||||
},
|
||||
"vehicle_state_valet_mode": {
|
||||
"default": "mdi:speedometer-slow"
|
||||
},
|
||||
"guest_mode_enabled": {
|
||||
"default": "mdi:account-group"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -1084,9 +1084,6 @@
|
||||
},
|
||||
"vehicle_state_valet_mode": {
|
||||
"name": "Valet mode"
|
||||
},
|
||||
"guest_mode_enabled": {
|
||||
"name": "Guest mode"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
|
@@ -4,6 +4,7 @@ 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
|
||||
@@ -37,7 +38,6 @@ 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,7 +53,6 @@ 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")
|
||||
),
|
||||
@@ -63,7 +62,6 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="vehicle_state_valet_mode",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, value: vehicle.listen_ValetModeEnabled(
|
||||
value
|
||||
),
|
||||
@@ -74,7 +72,6 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
|
||||
),
|
||||
TeslemetrySwitchEntityDescription(
|
||||
key="climate_state_auto_seat_climate_left",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_AutoSeatClimateLeft(
|
||||
callback
|
||||
),
|
||||
@@ -88,7 +85,6 @@ 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(
|
||||
@@ -101,7 +97,6 @@ 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(
|
||||
@@ -114,7 +109,6 @@ 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")
|
||||
),
|
||||
@@ -126,7 +120,6 @@ 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(
|
||||
@@ -138,17 +131,6 @@ 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],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -159,40 +141,35 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Teslemetry Switch platform from a config entry."""
|
||||
|
||||
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
|
||||
)
|
||||
async_add_entities(
|
||||
chain(
|
||||
(
|
||||
TeslemetryVehiclePollingVehicleSwitchEntity(
|
||||
vehicle, description, entry.runtime_data.scopes
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
TeslemetryChargeFromGridSwitchEntity(
|
||||
energysite,
|
||||
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")
|
||||
),
|
||||
)
|
||||
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,7 +75,6 @@ class Platform(StrEnum):
|
||||
SWITCH = "switch"
|
||||
TEXT = "text"
|
||||
TIME = "time"
|
||||
TAG = "tag"
|
||||
TODO = "todo"
|
||||
TTS = "tts"
|
||||
UPDATE = "update"
|
||||
@@ -750,7 +749,6 @@ 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.1
|
||||
hass-nabucasa==1.1.0
|
||||
hassil==3.2.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250903.3
|
||||
|
@@ -82,7 +82,6 @@ _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³
|
||||
@@ -436,7 +435,6 @@ 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),
|
||||
@@ -449,7 +447,6 @@ class PressureConverter(BaseUnitConverter):
|
||||
UnitOfPressure.CBAR,
|
||||
UnitOfPressure.MBAR,
|
||||
UnitOfPressure.INHG,
|
||||
UnitOfPressure.INH2O,
|
||||
UnitOfPressure.PSI,
|
||||
UnitOfPressure.MMHG,
|
||||
}
|
||||
|
@@ -296,7 +296,6 @@ 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,
|
||||
@@ -380,7 +379,6 @@ 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.1",
|
||||
"hass-nabucasa==1.1.0",
|
||||
# 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.1
|
||||
hass-nabucasa==1.1.0
|
||||
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.1
|
||||
hass-nabucasa==1.1.0
|
||||
|
||||
# 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.6
|
||||
coverage==7.10.0
|
||||
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==7.0.0
|
||||
pytest-cov==6.2.1
|
||||
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.1
|
||||
hass-nabucasa==1.1.0
|
||||
|
||||
# homeassistant.components.assist_satellite
|
||||
# homeassistant.components.conversation
|
||||
|
@@ -342,15 +342,6 @@ 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("/api/ai_task/images/")
|
||||
assert result["url"].startswith("http://10.10.10.10:8123/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,7 +37,5 @@
|
||||
'region': 'XEU',
|
||||
'serial': 'serial_number',
|
||||
}),
|
||||
'remote_command_list': list([
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
|
@@ -69,7 +69,6 @@ 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,12 +7,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from brother import BrotherSensors
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.brother.const import (
|
||||
CONF_COMMUNITY,
|
||||
DOMAIN,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
|
||||
from homeassistant.components.brother.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -126,10 +122,5 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
domain=DOMAIN,
|
||||
title="HL-L2340DW 0123456789",
|
||||
unique_id="0123456789",
|
||||
data={
|
||||
CONF_HOST: "localhost",
|
||||
CONF_TYPE: "laser",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
minor_version=2,
|
||||
data={CONF_HOST: "localhost", CONF_TYPE: "laser"},
|
||||
)
|
||||
|
@@ -66,10 +66,6 @@
|
||||
}),
|
||||
'firmware': '1.2.3',
|
||||
'info': dict({
|
||||
'advanced_settings': dict({
|
||||
'community': 'public',
|
||||
'port': 161,
|
||||
}),
|
||||
'host': 'localhost',
|
||||
'type': 'laser',
|
||||
}),
|
||||
|
@@ -6,13 +6,9 @@ from unittest.mock import AsyncMock, patch
|
||||
from brother import SnmpError, UnsupportedModelError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.brother.const import (
|
||||
CONF_COMMUNITY,
|
||||
DOMAIN,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.components.brother.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
@@ -21,11 +17,7 @@ from . import init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONFIG = {
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_TYPE: "laser",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
}
|
||||
CONFIG = {CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"}
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry", "mock_unload_entry")
|
||||
|
||||
@@ -45,21 +37,16 @@ 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=config,
|
||||
data={CONF_HOST: host, CONF_TYPE: "laser"},
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -67,11 +54,7 @@ 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",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
data={CONF_HOST: "invalid/hostname", CONF_TYPE: "laser"},
|
||||
)
|
||||
|
||||
assert result["errors"] == {CONF_HOST: "wrong_host"}
|
||||
@@ -258,19 +241,13 @@ 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",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
result["flow_id"], user_input={CONF_TYPE: "laser"}
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -288,10 +265,7 @@ async def test_reconfigure_successful(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: "10.10.10.10",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
user_input={CONF_HOST: "10.10.10.10"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
@@ -299,7 +273,6 @@ 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"},
|
||||
}
|
||||
|
||||
|
||||
@@ -330,10 +303,7 @@ 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",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
user_input={CONF_HOST: "10.10.10.10"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -344,10 +314,7 @@ 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",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
user_input={CONF_HOST: "10.10.10.10"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
@@ -355,7 +322,6 @@ 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"},
|
||||
}
|
||||
|
||||
|
||||
@@ -374,10 +340,7 @@ async def test_reconfigure_invalid_hostname(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: "invalid/hostname",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
user_input={CONF_HOST: "invalid/hostname"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -402,10 +365,7 @@ 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",
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
},
|
||||
user_input={CONF_HOST: "10.10.10.10"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
@@ -5,13 +5,8 @@ from unittest.mock import AsyncMock, patch
|
||||
from brother import SnmpError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.brother.const import (
|
||||
CONF_COMMUNITY,
|
||||
DOMAIN,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.components.brother.const import DOMAIN
|
||||
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
|
||||
@@ -73,26 +68,3 @@ 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'>,
|
||||
}),
|
||||
|
@@ -1,66 +0,0 @@
|
||||
# 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,7 +5,6 @@ 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
|
||||
@@ -81,7 +80,6 @@ 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"
|
||||
@@ -95,15 +93,9 @@ 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",
|
||||
}
|
||||
@@ -126,7 +118,6 @@ async def test_function_call(
|
||||
"param2": 2.7,
|
||||
},
|
||||
},
|
||||
"thought_signature": b"_thought_signature_3",
|
||||
}
|
||||
],
|
||||
"role": "model",
|
||||
@@ -145,7 +136,6 @@ async def test_function_call(
|
||||
"parts": [
|
||||
{
|
||||
"text": "I've called the ",
|
||||
"thought_signature": b"_thought_signature_4",
|
||||
}
|
||||
],
|
||||
"role": "model",
|
||||
@@ -160,25 +150,6 @@ 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",
|
||||
@@ -234,22 +205,6 @@ 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,7 +208,6 @@ async def test_tts_service_speak(
|
||||
threshold=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
),
|
||||
],
|
||||
thinking_config=types.ThinkingConfig(include_thoughts=True),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -277,6 +276,5 @@ async def test_tts_service_speak_error(
|
||||
threshold=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
),
|
||||
],
|
||||
thinking_config=types.ThinkingConfig(include_thoughts=True),
|
||||
),
|
||||
)
|
||||
|
@@ -1,176 +0,0 @@
|
||||
{
|
||||
"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,26 +27,3 @@ 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,10 +9,7 @@
|
||||
'rate_limit': 0,
|
||||
'state_updater': True,
|
||||
}),
|
||||
'config_store': dict({
|
||||
'entities': dict({
|
||||
}),
|
||||
}),
|
||||
'configuration_error': "extra keys not allowed @ data['knx']['wrong_key']",
|
||||
'configuration_yaml': dict({
|
||||
'wrong_key': dict({
|
||||
}),
|
||||
@@ -22,7 +19,6 @@
|
||||
'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]
|
||||
@@ -39,17 +35,13 @@
|
||||
'state_updater': True,
|
||||
'user_password': '**REDACTED**',
|
||||
}),
|
||||
'config_store': dict({
|
||||
'entities': dict({
|
||||
}),
|
||||
}),
|
||||
'configuration_error': None,
|
||||
'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]
|
||||
@@ -62,17 +54,13 @@
|
||||
'rate_limit': 0,
|
||||
'state_updater': True,
|
||||
}),
|
||||
'config_store': dict({
|
||||
'entities': dict({
|
||||
}),
|
||||
}),
|
||||
'configuration_error': None,
|
||||
'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]
|
||||
@@ -85,50 +73,7 @@
|
||||
'rate_limit': 0,
|
||||
'state_updater': True,
|
||||
}),
|
||||
'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_error': None,
|
||||
'configuration_yaml': None,
|
||||
'project_info': dict({
|
||||
'created_by': 'ETS5',
|
||||
@@ -146,6 +91,5 @@
|
||||
'current_address': '0.0.0',
|
||||
'version': '0.0.0',
|
||||
}),
|
||||
'yaml_configuration_error': None,
|
||||
})
|
||||
# ---
|
||||
|
@@ -574,6 +574,26 @@
|
||||
'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,
|
||||
@@ -595,53 +615,12 @@
|
||||
'required': False,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
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({
|
||||
'collapsible': False,
|
||||
'name': 'section_white',
|
||||
'required': False,
|
||||
'type': 'knx_section_flat',
|
||||
}),
|
||||
dict({
|
||||
'name': 'ga_white_switch',
|
||||
'optional': True,
|
||||
'options': dict({
|
||||
'passive': True,
|
||||
'state': dict({
|
||||
'required': False,
|
||||
}),
|
||||
'validDPTs': list([
|
||||
dict({
|
||||
'main': 1,
|
||||
'sub': None,
|
||||
}),
|
||||
]),
|
||||
'write': dict({
|
||||
'required': False,
|
||||
}),
|
||||
}),
|
||||
'required': False,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
dict({
|
||||
'name': 'ga_white_brightness',
|
||||
'optional': True,
|
||||
@@ -663,6 +642,27 @@
|
||||
'required': False,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
dict({
|
||||
'name': 'ga_white_switch',
|
||||
'optional': True,
|
||||
'options': dict({
|
||||
'passive': True,
|
||||
'state': dict({
|
||||
'required': False,
|
||||
}),
|
||||
'validDPTs': list([
|
||||
dict({
|
||||
'main': 1,
|
||||
'sub': None,
|
||||
}),
|
||||
]),
|
||||
'write': dict({
|
||||
'required': False,
|
||||
}),
|
||||
}),
|
||||
'required': False,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
]),
|
||||
'translation_key': 'individual_addresses',
|
||||
'type': 'knx_group_select_option',
|
||||
|
@@ -120,13 +120,9 @@ async def test_diagnostics_project(
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics."""
|
||||
await knx.setup_integration(
|
||||
config_store_fixture="config_store_light_switch.json",
|
||||
state_updater=False,
|
||||
)
|
||||
await knx.setup_integration()
|
||||
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,25 +128,6 @@ 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,7 +104,6 @@ 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:
|
||||
@@ -118,16 +117,6 @@ 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': 1,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
@@ -3932,7 +3932,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states[platforms0][sensor.washing_machine_energy_forecast-entry]
|
||||
@@ -4501,7 +4501,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WATER: 'water'>,
|
||||
@@ -4529,7 +4529,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states[platforms0][sensor.washing_machine_water_forecast-entry]
|
||||
@@ -6050,7 +6050,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
@@ -6078,7 +6078,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# 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': 0,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WATER: 'water'>,
|
||||
@@ -6675,7 +6675,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_water_forecast-entry]
|
||||
|
@@ -315,13 +315,6 @@ 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
|
||||
@@ -344,41 +337,10 @@ 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)
|
||||
@@ -389,28 +351,6 @@ 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
|
||||
@@ -449,7 +389,6 @@ 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)
|
||||
@@ -467,9 +406,6 @@ 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", "deprecation_warning"),
|
||||
("topic", "config", "entity_id", "name", "domain"),
|
||||
[
|
||||
(
|
||||
"homeassistant/alarm_control_panel/object/bla/config",
|
||||
@@ -1339,7 +1339,6 @@ 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",
|
||||
@@ -1347,7 +1346,6 @@ async def test_discover_alarm_control_panel(
|
||||
"binary_sensor.hello_id",
|
||||
"Hello World 2",
|
||||
"binary_sensor",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/button/object/bla/config",
|
||||
@@ -1355,7 +1353,6 @@ async def test_discover_alarm_control_panel(
|
||||
"button.hello_id",
|
||||
"Hello World button",
|
||||
"button",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/camera/object/bla/config",
|
||||
@@ -1363,7 +1360,6 @@ async def test_discover_alarm_control_panel(
|
||||
"camera.hello_id",
|
||||
"Hello World 3",
|
||||
"camera",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/climate/object/bla/config",
|
||||
@@ -1371,7 +1367,6 @@ async def test_discover_alarm_control_panel(
|
||||
"climate.hello_id",
|
||||
"Hello World 4",
|
||||
"climate",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/cover/object/bla/config",
|
||||
@@ -1379,7 +1374,6 @@ async def test_discover_alarm_control_panel(
|
||||
"cover.hello_id",
|
||||
"Hello World 5",
|
||||
"cover",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/fan/object/bla/config",
|
||||
@@ -1387,7 +1381,6 @@ async def test_discover_alarm_control_panel(
|
||||
"fan.hello_id",
|
||||
"Hello World 6",
|
||||
"fan",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/humidifier/object/bla/config",
|
||||
@@ -1395,7 +1388,6 @@ async def test_discover_alarm_control_panel(
|
||||
"humidifier.hello_id",
|
||||
"Hello World 7",
|
||||
"humidifier",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/number/object/bla/config",
|
||||
@@ -1403,7 +1395,6 @@ async def test_discover_alarm_control_panel(
|
||||
"number.hello_id",
|
||||
"Hello World 8",
|
||||
"number",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/scene/object/bla/config",
|
||||
@@ -1411,7 +1402,6 @@ async def test_discover_alarm_control_panel(
|
||||
"scene.hello_id",
|
||||
"Hello World 9",
|
||||
"scene",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/select/object/bla/config",
|
||||
@@ -1419,7 +1409,6 @@ async def test_discover_alarm_control_panel(
|
||||
"select.hello_id",
|
||||
"Hello World 10",
|
||||
"select",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/sensor/object/bla/config",
|
||||
@@ -1427,7 +1416,6 @@ async def test_discover_alarm_control_panel(
|
||||
"sensor.hello_id",
|
||||
"Hello World 11",
|
||||
"sensor",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/switch/object/bla/config",
|
||||
@@ -1435,7 +1423,6 @@ async def test_discover_alarm_control_panel(
|
||||
"switch.hello_id",
|
||||
"Hello World 12",
|
||||
"switch",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/light/object/bla/config",
|
||||
@@ -1443,7 +1430,6 @@ async def test_discover_alarm_control_panel(
|
||||
"light.hello_id",
|
||||
"Hello World 13",
|
||||
"light",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/light/object/bla/config",
|
||||
@@ -1451,7 +1437,6 @@ async def test_discover_alarm_control_panel(
|
||||
"light.hello_id",
|
||||
"Hello World 14",
|
||||
"light",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/light/object/bla/config",
|
||||
@@ -1459,7 +1444,6 @@ async def test_discover_alarm_control_panel(
|
||||
"light.hello_id",
|
||||
"Hello World 15",
|
||||
"light",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/vacuum/object/bla/config",
|
||||
@@ -1467,7 +1451,6 @@ async def test_discover_alarm_control_panel(
|
||||
"vacuum.hello_id",
|
||||
"Hello World 16",
|
||||
"vacuum",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/valve/object/bla/config",
|
||||
@@ -1475,7 +1458,6 @@ async def test_discover_alarm_control_panel(
|
||||
"valve.hello_id",
|
||||
"Hello World 17",
|
||||
"valve",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/lock/object/bla/config",
|
||||
@@ -1483,7 +1465,6 @@ async def test_discover_alarm_control_panel(
|
||||
"lock.hello_id",
|
||||
"Hello World 18",
|
||||
"lock",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/device_tracker/object/bla/config",
|
||||
@@ -1491,7 +1472,6 @@ async def test_discover_alarm_control_panel(
|
||||
"device_tracker.hello_id",
|
||||
"Hello World 19",
|
||||
"device_tracker",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/binary_sensor/object/bla/config",
|
||||
@@ -1500,7 +1480,6 @@ async def test_discover_alarm_control_panel(
|
||||
"binary_sensor.hello_id",
|
||||
"Hello World 2",
|
||||
"binary_sensor",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/button/object/bla/config",
|
||||
@@ -1510,7 +1489,6 @@ async def test_discover_alarm_control_panel(
|
||||
"button.hello_id",
|
||||
"Hello World button",
|
||||
"button",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant/alarm_control_panel/object/bla/config",
|
||||
@@ -1519,7 +1497,6 @@ 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",
|
||||
@@ -1528,7 +1505,6 @@ async def test_discover_alarm_control_panel(
|
||||
"binary_sensor.hello_id",
|
||||
"Hello World 2",
|
||||
"binary_sensor",
|
||||
False,
|
||||
),
|
||||
(
|
||||
"homeassistant/button/object/bla/config",
|
||||
@@ -1538,31 +1514,17 @@ 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()
|
||||
@@ -1575,11 +1537,6 @@ 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,
|
||||
|
@@ -82,6 +82,7 @@ light:
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import call, patch
|
||||
|
||||
@@ -169,6 +170,39 @@ COLOR_MODES_CONFIG = {
|
||||
}
|
||||
}
|
||||
|
||||
GROUP_MEMBER_1_TOPIC = "homeassistant/light/member_1/config"
|
||||
GROUP_MEMBER_2_TOPIC = "homeassistant/light/member_2/config"
|
||||
GROUP_TOPIC = "homeassistant/light/group/config"
|
||||
GROUP_DISCOVERY_MEMBER_1_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-member1",
|
||||
"unique_id": "very_unique_member1",
|
||||
"name": "member1",
|
||||
"default_entity_id": "light.member1",
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_MEMBER_2_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-member2",
|
||||
"unique_id": "very_unique_member2",
|
||||
"name": "member2",
|
||||
"default_entity_id": "light.member2",
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_LIGHT_GROUP_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-group",
|
||||
"state_topic": "test-state-topic-group",
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "group",
|
||||
"default_entity_id": "light.group",
|
||||
"group": ["very_unique_member1", "very_unique_member2"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class JsonValidator:
|
||||
"""Helper to compare JSON."""
|
||||
@@ -1859,6 +1893,69 @@ async def test_white_scale(
|
||||
assert state.attributes.get("brightness") == 129
|
||||
|
||||
|
||||
async def test_light_group_discovery_members_before_group(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the discovery of a light group and linked entity IDs.
|
||||
|
||||
The members are discovered first, so they are known in the entity registry.
|
||||
"""
|
||||
await mqtt_mock_entry()
|
||||
# Discover light group members
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Discover group
|
||||
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("entity_id") == ["light.member1", "light.member2"]
|
||||
assert group_state.attributes.get("icon") == "mdi:lightbulb-group"
|
||||
|
||||
|
||||
async def test_light_group_discovery_group_before_members(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the discovery of a light group and linked entity IDs.
|
||||
|
||||
The group is discovered first, so the group members are
|
||||
not (all) known yet in the entity registry.
|
||||
"""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
# Discover group
|
||||
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Discover light group members
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
# Members are not added yet, we need a group state update first
|
||||
# to trigger a state update
|
||||
assert not group_state.attributes.get("entity_id")
|
||||
async_fire_mqtt_message(hass, "test-state-topic-group", '{"state": "ON"}')
|
||||
await hass.async_block_till_done()
|
||||
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("entity_id") == ["light.member1", "light.member2"]
|
||||
assert group_state.attributes.get("icon") == "mdi:lightbulb-group"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
@@ -2040,7 +2137,7 @@ async def test_custom_availability_payload(
|
||||
)
|
||||
|
||||
|
||||
async def test_setting_attribute_via_mqtt_json_message(
|
||||
async def test_setting_attribute_via_mqtt_json_message_single_light(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||
@@ -2049,6 +2146,52 @@ async def test_setting_attribute_via_mqtt_json_message(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
help_custom_config(
|
||||
light.DOMAIN,
|
||||
DEFAULT_CONFIG,
|
||||
(
|
||||
{
|
||||
"unique_id": "very_unique_member_1",
|
||||
"name": "Part 1",
|
||||
"default_entity_id": "light.member_1",
|
||||
},
|
||||
{
|
||||
"unique_id": "very_unique_member_2",
|
||||
"name": "Part 2",
|
||||
"default_entity_id": "light.member_2",
|
||||
},
|
||||
{
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "My group",
|
||||
"default_entity_id": "light.my_group",
|
||||
"json_attributes_topic": "attr-topic",
|
||||
"group": [
|
||||
"very_unique_member_1",
|
||||
"very_unique_member_2",
|
||||
"member_3_not_exists",
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_setting_attribute_via_mqtt_json_message_light_group(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }')
|
||||
state = hass.states.get("light.my_group")
|
||||
|
||||
assert state and state.attributes.get("val") == "100"
|
||||
assert state.attributes.get("entity_id") == ["light.member_1", "light.member_2"]
|
||||
assert state.attributes.get("icon") == "mdi:lightbulb-group"
|
||||
|
||||
|
||||
async def test_setting_blocked_attribute_via_mqtt_json_message(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
|
@@ -72,7 +72,6 @@
|
||||
'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,
|
||||
@@ -137,7 +136,6 @@
|
||||
'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,
|
||||
@@ -202,7 +200,6 @@
|
||||
'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,
|
||||
|
@@ -1,826 +0,0 @@
|
||||
# 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,46 +6,180 @@ 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_TEMPERATURE
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
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, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
HA_PLUGWISE_SMILE_ASYNC_UPDATE = (
|
||||
"homeassistant.components.plugwise.coordinator.Smile.async_update"
|
||||
)
|
||||
|
||||
|
||||
@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: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_platform: MockConfigEntry,
|
||||
async def test_adam_climate_entity_attributes(
|
||||
hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Adam climate snapshot."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||
"""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(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_adam_heat_cool: MagicMock,
|
||||
init_integration: 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,
|
||||
)
|
||||
|
||||
|
||||
async def test_adam_climate_entity_climate_changes(
|
||||
@@ -123,95 +257,6 @@ 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,
|
||||
@@ -268,17 +313,68 @@ 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)
|
||||
@pytest.mark.parametrize("platforms", [(CLIMATE_DOMAIN,)])
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_anna_climate_snapshot(
|
||||
async def test_anna_climate_entity_attributes(
|
||||
hass: HomeAssistant,
|
||||
mock_smile_anna: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_platform: MockConfigEntry,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test Anna climate snapshot."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||
"""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,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True)
|
||||
@@ -350,33 +446,3 @@ 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)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user