Compare commits

..

2 Commits

Author SHA1 Message Date
jbouwh
e2e907963a Improve doc string 2025-09-13 12:16:10 +00:00
jbouwh
104ff0f1e1 Add support for MQTT JSON light groups 2025-09-11 22:16:31 +00:00
110 changed files with 807 additions and 5427 deletions

View File

@@ -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),

View File

@@ -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

View File

@@ -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),
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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."
}
}
}
}
},

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -176,7 +176,7 @@
"name": "Max connection upload throughput"
},
"cpu_temperature": {
"name": "CPU temperature"
"name": "CPU Temperature"
}
}
},

View File

@@ -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"
}

View File

@@ -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),
)

View File

@@ -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(
(

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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):

View File

@@ -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):

View File

@@ -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:

View File

@@ -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):

View File

@@ -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):

View File

@@ -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:

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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)])

View File

@@ -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:

View File

@@ -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

View File

@@ -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
),

View File

@@ -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(

View File

@@ -36,9 +36,6 @@
}
},
"sensor": {
"food_dispensed_today": {
"default": "mdi:counter"
},
"hopper_status": {
"default": "mdi:filter",
"state": {

View File

@@ -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,
),
],
}

View File

@@ -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"
},

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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"

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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."
}
}
}

View File

@@ -361,7 +361,6 @@ class SensorDeviceClass(StrEnum):
- `Pa`, `hPa`, `kPa`
- `inHg`
- `psi`
- `inH₂O`
"""
REACTIVE_ENERGY = "reactive_energy"

View File

@@ -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,
}

View File

@@ -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(

View File

@@ -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 = {

View File

@@ -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"
},

View File

@@ -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."""

View File

@@ -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]

View File

@@ -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()

View File

@@ -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"
}
}
}
}
}
}
}

View File

@@ -160,7 +160,6 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
"Motion Sensor": (BATTERY_DESCRIPTION,),
"Contact Sensor": (BATTERY_DESCRIPTION,),
"Water Detector": (BATTERY_DESCRIPTION,),
"Humidifier": (TEMPERATURE_DESCRIPTION,),
}

View File

@@ -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"
}
}
}
}
}
}
}

View File

@@ -752,9 +752,6 @@
},
"vehicle_state_valet_mode": {
"default": "mdi:speedometer-slow"
},
"guest_mode_enabled": {
"default": "mdi:account-group"
}
}
},

View File

@@ -1084,9 +1084,6 @@
},
"vehicle_state_valet_mode": {
"name": "Valet mode"
},
"guest_mode_enabled": {
"name": "Guest mode"
}
},
"update": {

View File

@@ -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):

View File

@@ -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"

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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
View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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"

View File

@@ -37,7 +37,5 @@
'region': 'XEU',
'serial': 'serial_number',
}),
'remote_command_list': list([
]),
})
# ---

View File

@@ -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)

View File

@@ -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"},
)

View File

@@ -66,10 +66,6 @@
}),
'firmware': '1.2.3',
'info': dict({
'advanced_settings': dict({
'community': 'public',
'port': 161,
}),
'host': 'localhost',
'type': 'laser',
}),

View File

@@ -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

View File

@@ -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"

View File

@@ -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'>,
}),

View File

@@ -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'
),
])
# ---

View File

@@ -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")

View File

@@ -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),
),
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
})
# ---

View File

@@ -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',

View File

@@ -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

View File

@@ -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},

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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,

View File

@@ -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:

View File

@@ -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,

View File

@@ -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',
})
# ---

View File

@@ -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