Compare commits

..

13 Commits

Author SHA1 Message Date
Ludovic BOUÉ
29fbfff138 Add Eve Shutter and window covering config status entries to binary sensor tests 2025-11-07 17:58:21 +00:00
Ludovic BOUÉ
fd60b738c9 Update Eve Shutter test to reflect correct ConfigStatus state changes 2025-11-07 17:57:16 +00:00
Ludovic BOUÉ
fe94f978b2 Update window covering ConfigStatus handling in Matter binary sensor 2025-11-07 17:55:55 +00:00
Ludovic BOUÉ
3aea41c722 Add test for Eve Shutter ConfigStatus in binary sensor tests 2025-11-07 17:35:38 +00:00
Ludovic BOUÉ
750123cc1f Add Eve Shutter fixture and update integration fixture 2025-11-07 17:28:49 +00:00
Ludovic BOUÉ
99b6362b59 Add window covering configuration status to Matter strings 2025-11-07 17:27:31 +00:00
Ludovic BOUÉ
e1fa5a11cb Add Window Covering Config Status binary sensor to Matter discovery schemas 2025-11-07 17:26:10 +00:00
David Rapan
a265ecfade Add Shelly sensor translation (#154106)
Signed-off-by: David Rapan <david@rapan.cz>
2025-11-07 18:09:41 +02:00
epenet
d52749c71a Add wrapper class for boolean values in Tuya models (#155905) 2025-11-07 16:16:14 +01:00
Guido Schmitz
5eb5b93c0e Allow devolo Home Control remote gateways to be offline (#152486)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-11-07 15:00:48 +00:00
epenet
7c6a39ec91 Add wrapper class for enum values in Tuya models (#155847) 2025-11-07 15:23:36 +01:00
Abílio Costa
57c3a5c349 Move imports to top level in websocket_api commands (#156004) 2025-11-07 14:10:19 +00:00
Erik Montnemery
07c4c58ce4 Deprecate http.server_host option and raise issue if used (#155849)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-11-07 14:51:07 +01:00
30 changed files with 1784 additions and 465 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Mapping
from functools import partial
import logging
from typing import Any
from devolo_home_control_api.exceptions.gateway import GatewayOfflineError
@@ -22,6 +23,8 @@ from .const import DOMAIN, PLATFORMS
type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, entry: DevoloHomeControlConfigEntry
@@ -44,26 +47,29 @@ async def async_setup_entry(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
)
try:
zeroconf_instance = await zeroconf.async_get_instance(hass)
entry.runtime_data = []
for gateway_id in gateway_ids:
zeroconf_instance = await zeroconf.async_get_instance(hass)
entry.runtime_data = []
offline_gateways = 0
for gateway_id in gateway_ids:
try:
entry.runtime_data.append(
await hass.async_add_executor_job(
partial(
HomeControl,
gateway_id=str(gateway_id),
gateway_id=gateway_id,
mydevolo_instance=mydevolo,
zeroconf_instance=zeroconf_instance,
)
)
)
except GatewayOfflineError as err:
except GatewayOfflineError:
offline_gateways += 1
_LOGGER.info("Central unit %s cannot be reached locally", gateway_id)
if len(gateway_ids) == offline_gateways:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="connection_failed",
translation_placeholders={"gateway_id": gateway_id},
) from err
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["devolo_home_control_api"],
"loggers": ["HomeControl", "Mydevolo", "MprmRest", "MprmWebsocket", "Mprm"],
"requirements": ["devolo-home-control-api==0.19.0"],
"zeroconf": ["_dvl-deviceapi._tcp.local."]
}

View File

@@ -58,7 +58,7 @@
},
"exceptions": {
"connection_failed": {
"message": "Failed to connect to devolo Home Control central unit {gateway_id}."
"message": "Failed to connect to any devolo Home Control central unit."
},
"invalid_auth": {
"message": "Authentication failed. Please re-authenticate with your mydevolo account."

View File

@@ -108,6 +108,7 @@ _DEFAULT_BIND = ["0.0.0.0", "::"] if _HAS_IPV6 else ["0.0.0.0"]
HTTP_SCHEMA: Final = vol.All(
cv.deprecated(CONF_BASE_URL),
cv.deprecated(CONF_SERVER_HOST), # Deprecated in HA Core 2025.12
vol.Schema(
{
vol.Optional(CONF_SERVER_HOST): vol.All(
@@ -208,14 +209,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if conf is None:
conf = cast(ConfData, HTTP_SCHEMA({}))
if CONF_SERVER_HOST in conf and is_hassio(hass):
if CONF_SERVER_HOST in conf:
if is_hassio(hass):
issue_id = "server_host_deprecated_hassio"
severity = ir.IssueSeverity.ERROR
else:
issue_id = "server_host_deprecated"
severity = ir.IssueSeverity.WARNING
ir.async_create_issue(
hass,
DOMAIN,
"server_host_may_break_hassio",
issue_id,
breaks_in_ha_version="2026.6.0",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="server_host_may_break_hassio",
severity=severity,
translation_key=issue_id,
)
server_host = conf.get(CONF_SERVER_HOST, _DEFAULT_BIND)

View File

@@ -1,7 +1,11 @@
{
"issues": {
"server_host_may_break_hassio": {
"description": "The `server_host` configuration option in the HTTP integration is prone to break the communication between Home Assistant Core and Supervisor, and will be removed in a future release.\n\nIf you are using this option to bind Home Assistant to specific network interfaces, please remove it from your configuration. Home Assistant will automatically bind to all available interfaces by default.\n\nIf you have specific networking requirements, consider using firewall rules or other network configuration to control access to Home Assistant.",
"server_host_deprecated": {
"description": "The `server_host` configuration option in the HTTP integration is deprecated and will be removed.\n\nIf you are using this option to bind Home Assistant to specific network interfaces, please remove it from your configuration. Home Assistant will automatically bind to all available interfaces by default.\n\nIf you have specific networking requirements, consider using firewall rules or other network configuration to control access to Home Assistant.",
"title": "The `server_host` HTTP configuration option is deprecated"
},
"server_host_deprecated_hassio": {
"description": "The deprecated `server_host` configuration option in the HTTP integration is prone to break the communication between Home Assistant Core and Supervisor, and will be removed.\n\nIf you are using this option to bind Home Assistant to specific network interfaces, please remove it from your configuration. Home Assistant will automatically bind to all available interfaces by default.\n\nIf you have specific networking requirements, consider using firewall rules or other network configuration to control access to Home Assistant.",
"title": "The `server_host` HTTP configuration may break Home Assistant Core - Supervisor communication"
},
"ssl_configured_without_configured_urls": {

View File

@@ -486,4 +486,19 @@ DISCOVERY_SCHEMAS = [
required_attributes=(clusters.RefrigeratorAlarm.Attributes.State,),
allow_multi=True,
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="WindowCoveringConfigStatus",
translation_key="window_covering_config_status",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
# ConfigStatus is a bitmap return True when the 'Operational' bit(s) is set
device_to_ha=lambda x: bool(
x & clusters.WindowCovering.Bitmaps.ConfigStatus.kOperational
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.WindowCovering.Attributes.ConfigStatus,),
),
]

View File

@@ -103,6 +103,9 @@
},
"water_leak": {
"name": "Water leak"
},
"window_covering_config_status": {
"name": "Config status"
}
},
"button": {

View File

@@ -295,10 +295,6 @@ def async_setup_entry_rest(
class BlockEntityDescription(EntityDescription):
"""Class to describe a BLOCK entity."""
# BlockEntity does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
unit_fn: Callable[[dict], str] | None = None
value: Callable[[Any], Any] = lambda val: val
available: Callable[[Block], bool] | None = None
@@ -311,10 +307,6 @@ class BlockEntityDescription(EntityDescription):
class RpcEntityDescription(EntityDescription):
"""Class to describe a RPC entity."""
# BlockEntity does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
sub_key: str | None = None
value: Callable[[Any, Any], Any] | None = None
@@ -332,10 +324,6 @@ class RpcEntityDescription(EntityDescription):
class RestEntityDescription(EntityDescription):
"""Class to describe a REST entity."""
# BlockEntity does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
value: Callable[[dict, Any], Any] | None = None

View File

@@ -26,6 +26,9 @@
"detected_objects": {
"default": "mdi:account-group"
},
"detected_objects_with_channel_name": {
"default": "mdi:account-group"
},
"gas_concentration": {
"default": "mdi:gauge"
},
@@ -38,9 +41,21 @@
"lamp_life": {
"default": "mdi:progress-wrench"
},
"left_slot_level": {
"default": "mdi:bottle-tonic-outline"
},
"left_slot_vial": {
"default": "mdi:scent"
},
"operation": {
"default": "mdi:cog-transfer"
},
"right_slot_level": {
"default": "mdi:bottle-tonic-outline"
},
"right_slot_vial": {
"default": "mdi:scent"
},
"self_test": {
"default": "mdi:progress-wrench"
},
@@ -52,12 +67,6 @@
},
"valve_status": {
"default": "mdi:valve"
},
"vial_level": {
"default": "mdi:bottle-tonic-outline"
},
"vial_name": {
"default": "mdi:scent"
}
},
"switch": {

File diff suppressed because it is too large Load Diff

View File

@@ -163,7 +163,29 @@
}
},
"sensor": {
"adc": {
"name": "ADC"
},
"analog": {
"name": "Analog"
},
"analog_value": {
"name": "Analog value"
},
"analog_value_with_channel_name": {
"name": "{channel_name} analog value"
},
"analog_with_channel_name": {
"name": "{channel_name} analog"
},
"apparent_power_with_channel_name": {
"name": "{channel_name} apparent power"
},
"average_temperature": {
"name": "Average temperature"
},
"charger_state": {
"name": "Charger state",
"state": {
"charger_charging": "[%key:common::state::charging%]",
"charger_end": "Charge completed",
@@ -175,10 +197,43 @@
"charger_wait": "Charging paused by vehicle"
}
},
"current_with_channel_name": {
"name": "{channel_name} current"
},
"detected_objects": {
"name": "Detected objects",
"unit_of_measurement": "objects"
},
"detected_objects_with_channel_name": {
"name": "{channel_name} detected objects",
"unit_of_measurement": "objects"
},
"device_temperature": {
"name": "Device temperature"
},
"energy_consumed": {
"name": "Energy consumed"
},
"energy_consumed_with_channel_name": {
"name": "{channel_name} energy consumed"
},
"energy_returned": {
"name": "Energy returned"
},
"energy_returned_with_channel_name": {
"name": "{channel_name} energy returned"
},
"energy_with_channel_name": {
"name": "{channel_name} energy"
},
"frequency_with_channel_name": {
"name": "{channel_name} frequency"
},
"gas_concentration": {
"name": "Gas concentration"
},
"gas_detected": {
"name": "Gas detected",
"state": {
"heavy": "Heavy",
"mild": "Mild",
@@ -196,21 +251,81 @@
}
}
},
"humidity_with_channel_name": {
"name": "{channel_name} humidity"
},
"illuminance_level": {
"name": "Illuminance level",
"state": {
"bright": "Bright",
"dark": "Dark",
"twilight": "Twilight"
}
},
"lamp_life": {
"name": "Lamp life"
},
"last_restart": {
"name": "Last restart"
},
"left_slot_level": {
"name": "Left slot level"
},
"left_slot_vial": {
"name": "Left slot vial"
},
"neutral_current": {
"name": "Neutral current"
},
"operation": {
"name": "Operation",
"state": {
"fault": "[%key:common::state::fault%]",
"normal": "[%key:common::state::normal%]",
"warmup": "Warm-up"
}
},
"power_factor_with_channel_name": {
"name": "{channel_name} power factor"
},
"power_with_channel_name": {
"name": "{channel_name} power"
},
"pulse_counter": {
"name": "Pulse counter"
},
"pulse_counter_frequency": {
"name": "Pulse counter frequency"
},
"pulse_counter_frequency_value": {
"name": "Pulse counter frequency value"
},
"pulse_counter_frequency_value_with_channel_name": {
"name": "{channel_name} pulse counter frequency value"
},
"pulse_counter_frequency_with_channel_name": {
"name": "{channel_name} pulse counter frequency"
},
"pulse_counter_value": {
"name": "Pulse counter value"
},
"pulse_counter_value_with_channel_name": {
"name": "{channel_name} Pulse counter value"
},
"pulse_counter_with_channel_name": {
"name": "{channel_name} pulse counter"
},
"rainfall_last_24h": {
"name": "Rainfall last 24h"
},
"right_slot_level": {
"name": "Right slot level"
},
"right_slot_vial": {
"name": "Right slot vial"
},
"self_test": {
"name": "Self test",
"state": {
"completed": "Completed",
"not_completed": "Not completed",
@@ -228,7 +343,23 @@
}
}
},
"session_duration": {
"name": "Session duration"
},
"session_energy": {
"name": "Session energy"
},
"temperature_with_channel_name": {
"name": "{channel_name} temperature"
},
"tilt": {
"name": "Tilt"
},
"valve_position": {
"name": "Valve position"
},
"valve_status": {
"name": "Valve status",
"state": {
"checking": "Checking",
"closed": "[%key:common::state::closed%]",
@@ -237,6 +368,30 @@
"opened": "Opened",
"opening": "[%key:common::state::opening%]"
}
},
"voltage_with_channel_name": {
"name": "{channel_name} voltage"
},
"voltage_with_phase_name": {
"name": "Phase {phase_name} voltage"
},
"voltmeter": {
"name": "Voltmeter"
},
"voltmeter_value": {
"name": "Voltmeter value"
},
"water_consumption": {
"name": "Water consumption"
},
"water_flow_rate": {
"name": "Water flow rate"
},
"water_pressure": {
"name": "Water pressure"
},
"water_temperature": {
"name": "Water temperature"
}
}
},

View File

@@ -49,6 +49,7 @@ from homeassistant.helpers.device_registry import (
DeviceInfo,
)
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util.dt import utcnow
from .const import (
@@ -119,12 +120,12 @@ def get_number_of_channels(device: BlockDevice, block: Block) -> int:
def get_block_entity_name(
device: BlockDevice,
block: Block | None,
description: str | None = None,
description: str | UndefinedType | None = None,
) -> str | None:
"""Naming for block based switch and sensors."""
channel_name = get_block_channel_name(device, block)
if description:
if description is not UNDEFINED and description:
return f"{channel_name} {description.lower()}" if channel_name else description
return channel_name
@@ -442,12 +443,15 @@ def get_rpc_sub_device_name(
def get_rpc_entity_name(
device: RpcDevice, key: str, name: str | None = None, role: str | None = None
device: RpcDevice,
key: str,
name: str | UndefinedType | None = None,
role: str | None = None,
) -> str | None:
"""Naming for RPC based switch and sensors."""
channel_name = get_rpc_channel_name(device, key)
if name:
if name is not UNDEFINED and name:
if role and role != ROLE_GENERIC:
return name
return f"{channel_name} {name.lower()}" if channel_name else name

View File

@@ -6,7 +6,7 @@ import base64
from dataclasses import dataclass
import json
import struct
from typing import Literal, Self, overload
from typing import Any, Literal, Self, overload
from tuya_sharing import CustomerDevice
@@ -14,6 +14,76 @@ from .const import DPCode, DPType
from .util import remap_value
@dataclass
class DPCodeWrapper:
"""Base DPCode wrapper.
Used as a common interface for referring to a DPCode, and
access read conversion routines.
"""
dpcode: str
def _read_device_status_raw(self, device: CustomerDevice) -> Any | None:
"""Read the raw device status for the DPCode.
Private helper method for `read_device_status`.
"""
return device.status.get(self.dpcode)
def read_device_status(self, device: CustomerDevice) -> Any | None:
"""Read the device value for the dpcode."""
raise NotImplementedError("read_device_value must be implemented")
@dataclass
class DPCodeBooleanWrapper(DPCodeWrapper):
"""Simple wrapper for boolean values.
Supports True/False only.
"""
def read_device_status(self, device: CustomerDevice) -> bool | None:
"""Read the device value for the dpcode."""
if (raw_value := self._read_device_status_raw(device)) in (True, False):
return raw_value
return None
@dataclass(kw_only=True)
class DPCodeEnumWrapper(DPCodeWrapper):
"""Simple wrapper for EnumTypeData values."""
enum_type_information: EnumTypeData
def read_device_status(self, device: CustomerDevice) -> str | None:
"""Read the device value for the dpcode.
Values outside of the list defined by the Enum type information will
return None.
"""
if (
raw_value := self._read_device_status_raw(device)
) in self.enum_type_information.range:
return raw_value
return None
@classmethod
def find_dpcode(
cls,
device: CustomerDevice,
dpcodes: str | DPCode | tuple[DPCode, ...],
*,
prefer_function: bool = False,
) -> Self | None:
"""Find and return a DPCodeEnumWrapper for the given DP codes."""
if enum_type := find_dpcode(
device, dpcodes, dptype=DPType.ENUM, prefer_function=prefer_function
):
return cls(dpcode=enum_type.dpcode, enum_type_information=enum_type)
return None
@overload
def find_dpcode(
device: CustomerDevice,

View File

@@ -11,9 +11,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import find_dpcode
from .models import DPCodeEnumWrapper
# All descriptions can be found here. Mostly the Enum data types in the
# default instructions set of each category end up being a select.
@@ -360,9 +360,15 @@ async def async_setup_entry(
device = manager.device_map[device_id]
if descriptions := SELECTS.get(device.category):
entities.extend(
TuyaSelectEntity(device, manager, description)
TuyaSelectEntity(
device, manager, description, dpcode_wrapper=dpcode_wrapper
)
for description in descriptions
if description.key in device.status
if (
dpcode_wrapper := DPCodeEnumWrapper.find_dpcode(
device, description.key, prefer_function=True
)
)
)
async_add_entities(entities)
@@ -382,35 +388,20 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity):
device: CustomerDevice,
device_manager: Manager,
description: SelectEntityDescription,
dpcode_wrapper: DPCodeEnumWrapper,
) -> None:
"""Init Tuya sensor."""
super().__init__(device, device_manager)
self.entity_description = description
self._attr_unique_id = f"{super().unique_id}{description.key}"
self._attr_options: list[str] = []
if enum_type := find_dpcode(
self.device, description.key, dptype=DPType.ENUM, prefer_function=True
):
self._attr_options = enum_type.range
self._dpcode_wrapper = dpcode_wrapper
self._attr_options = dpcode_wrapper.enum_type_information.range
@property
def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state."""
# Raw value
value = self.device.status.get(self.entity_description.key)
if value is None or value not in self._attr_options:
return None
return value
return self._dpcode_wrapper.read_device_status(self.device)
def select_option(self, option: str) -> None:
"""Change the selected option."""
self._send_command(
[
{
"code": self.entity_description.key,
"value": option,
}
]
)
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": option}])

View File

@@ -27,6 +27,7 @@ from homeassistant.helpers.issue_registry import (
from . import TuyaConfigEntry
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DPCodeBooleanWrapper
@dataclass(frozen=True, kw_only=True)
@@ -938,7 +939,12 @@ async def async_setup_entry(
device = manager.device_map[device_id]
if descriptions := SWITCHES.get(device.category):
entities.extend(
TuyaSwitchEntity(device, manager, description)
TuyaSwitchEntity(
device,
manager,
description,
DPCodeBooleanWrapper(description.key),
)
for description in descriptions
if description.key in device.status
and _check_deprecation(
@@ -1015,21 +1021,23 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity):
device: CustomerDevice,
device_manager: Manager,
description: SwitchEntityDescription,
dpcode_wrapper: DPCodeBooleanWrapper,
) -> None:
"""Init TuyaHaSwitch."""
super().__init__(device, device_manager)
self.entity_description = description
self._attr_unique_id = f"{super().unique_id}{description.key}"
self._dpcode_wrapper = dpcode_wrapper
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return true if switch is on."""
return self.device.status.get(self.entity_description.key, False)
return self._dpcode_wrapper.read_device_status(self.device)
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._send_command([{"code": self.entity_description.key, "value": True}])
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": True}])
def turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
self._send_command([{"code": self.entity_description.key, "value": False}])
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": False}])

View File

@@ -17,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
from .models import DPCodeBooleanWrapper
VALVES: dict[DeviceCategory, tuple[ValveEntityDescription, ...]] = {
DeviceCategory.SFKZQ: (
@@ -93,7 +94,12 @@ async def async_setup_entry(
device = manager.device_map[device_id]
if descriptions := VALVES.get(device.category):
entities.extend(
TuyaValveEntity(device, manager, description)
TuyaValveEntity(
device,
manager,
description,
DPCodeBooleanWrapper(description.key),
)
for description in descriptions
if description.key in device.status
)
@@ -117,25 +123,29 @@ class TuyaValveEntity(TuyaEntity, ValveEntity):
device: CustomerDevice,
device_manager: Manager,
description: ValveEntityDescription,
dpcode_wrapper: DPCodeBooleanWrapper,
) -> None:
"""Init TuyaValveEntity."""
super().__init__(device, device_manager)
self.entity_description = description
self._attr_unique_id = f"{super().unique_id}{description.key}"
self._dpcode_wrapper = dpcode_wrapper
@property
def is_closed(self) -> bool:
def is_closed(self) -> bool | None:
"""Return if the valve is closed."""
return not self.device.status.get(self.entity_description.key, False)
if (is_open := self._dpcode_wrapper.read_device_status(self.device)) is None:
return None
return not is_open
async def async_open_valve(self) -> None:
"""Open the valve."""
await self.hass.async_add_executor_job(
self._send_command, [{"code": self.entity_description.key, "value": True}]
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": True}]
)
async def async_close_valve(self) -> None:
"""Close the valve."""
await self.hass.async_add_executor_job(
self._send_command, [{"code": self.entity_description.key, "value": False}]
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": False}]
)

View File

@@ -41,8 +41,11 @@ from homeassistant.helpers import (
template,
)
from homeassistant.helpers.condition import (
async_from_config as async_condition_from_config,
async_get_all_descriptions as async_get_all_condition_descriptions,
async_subscribe_platform_events as async_subscribe_condition_platform_events,
async_validate_condition_config,
async_validate_conditions_config,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entityfilter import (
@@ -66,7 +69,9 @@ from homeassistant.helpers.service import (
)
from homeassistant.helpers.trigger import (
async_get_all_descriptions as async_get_all_trigger_descriptions,
async_initialize_triggers,
async_subscribe_platform_events as async_subscribe_trigger_platform_events,
async_validate_trigger_config,
)
from homeassistant.loader import (
IntegrationNotFound,
@@ -885,10 +890,7 @@ async def handle_subscribe_trigger(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle subscribe trigger command."""
# Circular dep
from homeassistant.helpers import trigger # noqa: PLC0415
trigger_config = await trigger.async_validate_trigger_config(hass, msg["trigger"])
trigger_config = await async_validate_trigger_config(hass, msg["trigger"])
@callback
def forward_triggers(
@@ -905,7 +907,7 @@ async def handle_subscribe_trigger(
)
connection.subscriptions[msg["id"]] = (
await trigger.async_initialize_triggers(
await async_initialize_triggers(
hass,
trigger_config,
forward_triggers,
@@ -935,13 +937,10 @@ async def handle_test_condition(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle test condition command."""
# Circular dep
from homeassistant.helpers import condition # noqa: PLC0415
# Do static + dynamic validation of the condition
config = await condition.async_validate_condition_config(hass, msg["condition"])
config = await async_validate_condition_config(hass, msg["condition"])
# Test the condition
check_condition = await condition.async_from_config(hass, config)
check_condition = await async_condition_from_config(hass, config)
connection.send_result(
msg["id"], {"result": check_condition(hass, msg.get("variables"))}
)
@@ -1028,16 +1027,16 @@ async def handle_validate_config(
) -> None:
"""Handle validate config command."""
# Circular dep
from homeassistant.helpers import condition, script, trigger # noqa: PLC0415
from homeassistant.helpers import script # noqa: PLC0415
result = {}
for key, schema, validator in (
("triggers", cv.TRIGGER_SCHEMA, trigger.async_validate_trigger_config),
("triggers", cv.TRIGGER_SCHEMA, async_validate_trigger_config),
(
"conditions",
cv.CONDITIONS_SCHEMA,
condition.async_validate_conditions_config,
async_validate_conditions_config,
),
("actions", cv.SCRIPT_SCHEMA, script.async_validate_actions_config),
):

View File

@@ -25,7 +25,6 @@ from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
@@ -527,9 +526,6 @@ class Entity(
__capabilities_updated_at_reported: bool = False
__remove_future: asyncio.Future[None] | None = None
# Remember we keep track of included entities
__init_track_included_entities: bool = False
# Entity Properties
_attr_assumed_state: bool = False
_attr_attribution: str | None = None
@@ -545,8 +541,6 @@ class Entity(
_attr_extra_state_attributes: dict[str, Any]
_attr_force_update: bool
_attr_icon: str | None
_attr_included_entities: list[str]
_attr_included_unique_ids: list[str]
_attr_name: str | None
_attr_should_poll: bool = True
_attr_state: StateType = STATE_UNKNOWN
@@ -1093,8 +1087,6 @@ class Entity(
available = self.available # only call self.available once per update cycle
state = self._stringify_state(available)
if available:
if self._included_entities:
attr[ATTR_ENTITY_ID] = self._included_entities.copy()
if state_attributes := self.state_attributes:
attr |= state_attributes
if extra_state_attributes := self.extra_state_attributes:
@@ -1386,7 +1378,6 @@ class Entity(
"""Finish adding an entity to a platform."""
await self.async_internal_added_to_hass()
await self.async_added_to_hass()
self.async_set_included_entities()
self._platform_state = EntityPlatformState.ADDED
self.async_write_ha_state()
@@ -1644,67 +1635,6 @@ class Entity(
self.hass, integration_domain=platform_name, module=type(self).__module__
)
@callback
def async_set_included_entities(self) -> None:
"""Set the list of included entities identified by their unique IDs.
Integrations need to when the list of included entities changes.
"""
entity_registry = er.async_get(self.hass)
assert self.entity_id is not None
def _update_group_entity_ids() -> None:
self._attr_included_entities = []
for included_id in self.included_unique_ids:
if entity_id := entity_registry.async_get_entity_id(
self.platform.domain, self.platform.platform_name, included_id
):
self._attr_included_entities.append(entity_id)
async def _handle_entity_registry_updated(event: Event[Any]) -> None:
"""Handle registry create or update event."""
if (
event.data["action"] in {"create", "update"}
and (entry := entity_registry.async_get(event.data["entity_id"]))
and entry.unique_id in self.included_unique_ids
) or (
event.data["action"] == "remove"
and self._included_entities is not None
and event.data["entity_id"] in self._included_entities
):
_update_group_entity_ids()
self.async_write_ha_state()
if not self.__init_track_included_entities:
self.async_on_remove(
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
_handle_entity_registry_updated,
)
)
self.__init_track_included_entities = True
_update_group_entity_ids()
@property
def included_unique_ids(self) -> list[str]:
"""Return the list of unique IDs if the entity represents a group.
The corresponding entities will be shown as members in the UI.
"""
if hasattr(self, "_attr_included_unique_ids"):
return self._attr_included_unique_ids
return []
@property
def _included_entities(self) -> list[str] | None:
"""Return a list of entity IDs if the entity represents a group.
Included entities will be shown as members in the UI.
"""
if hasattr(self, "_attr_included_entities"):
return self._attr_included_entities
return None
class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes toggle entities."""

View File

@@ -47,7 +47,19 @@ async def test_setup_entry_maintenance(
async def test_setup_gateway_offline(hass: HomeAssistant) -> None:
"""Test setup entry fails on gateway offline."""
"""Test setup entry with one gateway online and one gateway offline."""
entry = configure_integration(hass)
test_gateway = HomeControlMock()
with patch(
"homeassistant.components.devolo_home_control.HomeControl",
side_effect=[test_gateway, GatewayOfflineError],
):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
async def test_setup_all_gateways_offline(hass: HomeAssistant) -> None:
"""Test setup entry fails on all gateways offline."""
entry = configure_integration(hass)
with patch(
"homeassistant.components.devolo_home_control.HomeControl",

View File

@@ -683,19 +683,27 @@ async def test_ssl_issue_urls_configured(
"hassio",
"http_config",
"expected_serverhost",
"expected_warning_count",
"expected_issues",
),
[
(False, {}, ["0.0.0.0", "::"], set()),
(False, {"server_host": "0.0.0.0"}, ["0.0.0.0"], set()),
(True, {}, ["0.0.0.0", "::"], set()),
(False, {}, ["0.0.0.0", "::"], 0, set()),
(
False,
{"server_host": "0.0.0.0"},
["0.0.0.0"],
1,
{("http", "server_host_deprecated")},
),
(True, {}, ["0.0.0.0", "::"], 0, set()),
(
True,
{"server_host": "0.0.0.0"},
[
"0.0.0.0",
],
{("http", "server_host_may_break_hassio")},
1,
{("http", "server_host_deprecated_hassio")},
),
],
)
@@ -705,7 +713,9 @@ async def test_server_host(
issue_registry: ir.IssueRegistry,
http_config: dict,
expected_serverhost: list,
expected_warning_count: int,
expected_issues: set[tuple[str, str]],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test server_host behavior."""
mock_server = Mock()
@@ -733,4 +743,11 @@ async def test_server_host(
reuse_port=None,
)
assert (
caplog.text.count(
"The 'server_host' option is deprecated, please remove it from your configuration"
)
== expected_warning_count
)
assert set(issue_registry.issues) == expected_issues

View File

@@ -94,6 +94,7 @@ async def integration_fixture(
"eve_energy_20ecn4101",
"eve_energy_plug",
"eve_energy_plug_patched",
"eve_shutter",
"eve_thermo",
"eve_weather_sensor",
"extended_color_light",

View File

@@ -0,0 +1,617 @@
{
"node_id": 148,
"date_commissioned": "2025-11-07T16:57:31.360667",
"last_interview": "2025-11-07T16:57:31.360690",
"interview_version": 6,
"available": true,
"is_bridge": false,
"attributes": {
"0/29/0": [
{
"0": 18,
"1": 1
},
{
"0": 22,
"1": 3
}
],
"0/29/1": [29, 31, 40, 42, 48, 49, 50, 51, 52, 53, 56, 60, 62, 63],
"0/29/2": [41],
"0/29/3": [1],
"0/29/65532": 0,
"0/29/65533": 2,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/31/0": [
{
"1": 5,
"2": 2,
"3": [112233],
"4": null,
"254": 3
}
],
"0/31/1": [],
"0/31/2": 10,
"0/31/3": 3,
"0/31/4": 5,
"0/31/65532": 1,
"0/31/65533": 2,
"0/31/65528": [],
"0/31/65529": [],
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/40/0": 18,
"0/40/1": "Eve Systems",
"0/40/2": 4874,
"0/40/3": "Eve Shutter Switch 20ECI1701",
"0/40/4": 96,
"0/40/5": "",
"0/40/6": "**REDACTED**",
"0/40/7": 1,
"0/40/8": "1.1",
"0/40/9": 10203,
"0/40/10": "3.6.1",
"0/40/15": "**********",
"0/40/18": "**********",
"0/40/19": {
"0": 3,
"1": 3
},
"0/40/21": 17039616,
"0/40/22": 1,
"0/40/65532": 0,
"0/40/65533": 4,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 19, 21, 22, 65528, 65529, 65531,
65532, 65533
],
"0/42/0": [],
"0/42/1": true,
"0/42/2": 1,
"0/42/3": null,
"0/42/65532": 0,
"0/42/65533": 1,
"0/42/65528": [],
"0/42/65529": [0],
"0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/48/0": 0,
"0/48/1": {
"0": 60,
"1": 900
},
"0/48/2": 0,
"0/48/3": 0,
"0/48/4": true,
"0/48/65532": 0,
"0/48/65533": 2,
"0/48/65528": [1, 3, 5],
"0/48/65529": [0, 2, 4],
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/49/0": 1,
"0/49/1": [
{
"0": "p0jbsOzJRNw=",
"1": true
}
],
"0/49/2": 10,
"0/49/3": 20,
"0/49/4": true,
"0/49/5": 0,
"0/49/6": "p0jbsOzJRNw=",
"0/49/7": null,
"0/49/9": 10,
"0/49/10": 5,
"0/49/65532": 2,
"0/49/65533": 2,
"0/49/65528": [1, 5, 7],
"0/49/65529": [0, 3, 4, 6, 8],
"0/49/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533
],
"0/50/65532": 0,
"0/50/65533": 1,
"0/50/65528": [1],
"0/50/65529": [0],
"0/50/65531": [65528, 65529, 65531, 65532, 65533],
"0/51/0": [
{
"0": "ieee802154",
"1": true,
"2": null,
"3": null,
"4": "Wi5/8pP0edY=",
"5": [],
"6": [],
"7": 4
}
],
"0/51/1": 1,
"0/51/2": 213,
"0/51/3": 0,
"0/51/5": [],
"0/51/6": [],
"0/51/7": [],
"0/51/8": false,
"0/51/65532": 0,
"0/51/65533": 2,
"0/51/65528": [2],
"0/51/65529": [0, 1],
"0/51/65531": [0, 1, 2, 3, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533],
"0/52/1": 10104,
"0/52/2": 2008,
"0/52/65532": 0,
"0/52/65533": 1,
"0/52/65528": [],
"0/52/65529": [],
"0/52/65531": [1, 2, 65528, 65529, 65531, 65532, 65533],
"0/53/0": 25,
"0/53/1": 5,
"0/53/2": "MyHome",
"0/53/3": 4660,
"0/53/4": 12054125955590472924,
"0/53/5": "QP0ADbgAoAAA",
"0/53/6": 0,
"0/53/7": [
{
"0": 12864791528929066571,
"1": 28,
"2": 11264,
"3": 411672,
"4": 11555,
"5": 3,
"6": -53,
"7": -53,
"8": 44,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 13438285129078731668,
"1": 16,
"2": 18432,
"3": 50641,
"4": 10901,
"5": 1,
"6": -89,
"7": -89,
"8": 0,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 8265194500311707858,
"1": 51,
"2": 24576,
"3": 75011,
"4": 10782,
"5": 2,
"6": -84,
"7": -84,
"8": 0,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 14318601490803184919,
"1": 16,
"2": 27648,
"3": 310236,
"4": 10937,
"5": 3,
"6": -50,
"7": -50,
"8": 20,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 2202349555917590819,
"1": 22,
"2": 45056,
"3": 86183,
"4": 25554,
"5": 3,
"6": -78,
"7": -85,
"8": 3,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 4206032556233211940,
"1": 63,
"2": 53248,
"3": 80879,
"4": 10668,
"5": 3,
"6": -78,
"7": -77,
"8": 3,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 7085268071783685380,
"1": 15,
"2": 54272,
"3": 4269,
"4": 3159,
"5": 3,
"6": -76,
"7": -74,
"8": 0,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 10848996971365580420,
"1": 17,
"2": 60416,
"3": 318410,
"4": 10506,
"5": 3,
"6": -61,
"7": -62,
"8": 43,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
}
],
"0/53/8": [
{
"0": 12864791528929066571,
"1": 11264,
"2": 11,
"3": 27,
"4": 1,
"5": 3,
"6": 3,
"7": 28,
"8": true,
"9": true
},
{
"0": 13438285129078731668,
"1": 18432,
"2": 18,
"3": 53,
"4": 1,
"5": 1,
"6": 2,
"7": 16,
"8": true,
"9": true
},
{
"0": 8265194500311707858,
"1": 24576,
"2": 24,
"3": 52,
"4": 1,
"5": 2,
"6": 3,
"7": 51,
"8": true,
"9": true
},
{
"0": 14318601490803184919,
"1": 27648,
"2": 27,
"3": 11,
"4": 1,
"5": 3,
"6": 3,
"7": 16,
"8": true,
"9": true
},
{
"0": 6498271992183290326,
"1": 40960,
"2": 40,
"3": 63,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"8": true,
"9": false
},
{
"0": 2202349555917590819,
"1": 45056,
"2": 44,
"3": 27,
"4": 1,
"5": 3,
"6": 2,
"7": 22,
"8": true,
"9": true
},
{
"0": 4206032556233211940,
"1": 53248,
"2": 52,
"3": 59,
"4": 1,
"5": 3,
"6": 3,
"7": 63,
"8": true,
"9": true
},
{
"0": 7085268071783685380,
"1": 54272,
"2": 53,
"3": 27,
"4": 2,
"5": 3,
"6": 3,
"7": 15,
"8": true,
"9": true
},
{
"0": 10848996971365580420,
"1": 60416,
"2": 59,
"3": 11,
"4": 1,
"5": 3,
"6": 3,
"7": 17,
"8": true,
"9": true
}
],
"0/53/9": 1938283056,
"0/53/10": 68,
"0/53/11": 65,
"0/53/12": 8,
"0/53/13": 27,
"0/53/14": 1,
"0/53/15": 1,
"0/53/16": 1,
"0/53/17": 0,
"0/53/18": 1,
"0/53/19": 1,
"0/53/20": 0,
"0/53/21": 0,
"0/53/22": 759,
"0/53/23": 737,
"0/53/24": 22,
"0/53/25": 737,
"0/53/26": 737,
"0/53/27": 22,
"0/53/28": 759,
"0/53/29": 0,
"0/53/30": 0,
"0/53/31": 0,
"0/53/32": 0,
"0/53/33": 529,
"0/53/34": 0,
"0/53/35": 0,
"0/53/36": 0,
"0/53/37": 0,
"0/53/38": 0,
"0/53/39": 3405,
"0/53/40": 275,
"0/53/41": 126,
"0/53/42": 392,
"0/53/43": 0,
"0/53/44": 0,
"0/53/45": 0,
"0/53/46": 0,
"0/53/47": 0,
"0/53/48": 2796,
"0/53/49": 9,
"0/53/50": 18,
"0/53/51": 10,
"0/53/52": 0,
"0/53/53": 0,
"0/53/54": 70,
"0/53/55": 110,
"0/53/59": {
"0": 672,
"1": 143
},
"0/53/60": "AB//4A==",
"0/53/61": {
"0": true,
"1": false,
"2": true,
"3": true,
"4": true,
"5": true,
"6": false,
"7": true,
"8": true,
"9": true,
"10": true,
"11": true
},
"0/53/62": [],
"0/53/65532": 15,
"0/53/65533": 3,
"0/53/65528": [],
"0/53/65529": [0],
"0/53/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59,
60, 61, 62, 65528, 65529, 65531, 65532, 65533
],
"0/56/0": 815849852639528,
"0/56/1": 2,
"0/56/2": 2,
"0/56/3": null,
"0/56/5": [
{
"0": 3600,
"1": 0,
"2": "Europe/Paris"
}
],
"0/56/6": [
{
"0": 0,
"1": 0,
"2": 828061200000000
},
{
"0": 3600,
"1": 828061200000000,
"2": 846205200000000
}
],
"0/56/7": 815853452640810,
"0/56/8": 2,
"0/56/10": 2,
"0/56/11": 2,
"0/56/65532": 9,
"0/56/65533": 2,
"0/56/65528": [3],
"0/56/65529": [0, 1, 2, 4],
"0/56/65531": [
0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 65528, 65529, 65531, 65532, 65533
],
"0/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65532": 1,
"0/60/65533": 1,
"0/60/65528": [],
"0/60/65529": [0, 1, 2],
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"0/62/0": [
{
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRlBgkBwEkCAEwCUEEQ2q1XJVV19WnxHHSfOUdx9bDmdDqjNtb9YgZA2j76IaZCChToVK6aKvw+YxIPL3mgzVfD08t2wHpcNjyBIAYFjcKNQEoARgkAgE2AwQCBAEYMAQUXObIaHWU7+qbdq7roNf1TweBIfMwBRS5+zzv8ZPGnI9mC3wH9vq10JnwlhgwC0BByE+Cvdi+klStM4F55ptZC4sE7IRIzqFHEUa2CZY2k7uTFPj9Yo1YzWgpnNJlAc0vnGXdN9E7B6yttZk4tSkZGA==",
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE/DujEcdTsX19xbxX+KuKKWiMaA5D9u99P/pVxIOmscd2BA2PadEMNnjvtPOpf+WE2Zxar4rby1IfAClGUUuQrTcKNQEpARgkAmAwBBS5+zzv8ZPGnI9mC3wH9vq10JnwljAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQGkPpvsbkAFEbfPN6H3Kf23R0zzmW/gpAA3kgaL6wKB2Ofm+Tmylw22qM536Kj8mOMwaV0EL1dCCGcuxF98aL6gY",
"254": 3
}
],
"0/62/1": [
{
"1": "BBmX+KwLR5HGlVNbvlC+dO8Jv9fPthHiTfGpUzi2JJADX5az6GxBAFn02QKHwLcZHyh+lh9faf6rf38/nPYF7/M=",
"2": 4939,
"3": 2,
"4": 148,
"5": "ha-freebox",
"254": 3
}
],
"0/62/2": 5,
"0/62/3": 3,
"0/62/4": [
"FTABAQAkAgE3AyYUyakYCSYVj6gLsxgmBBHhoDAkBQA3BiYUyakYCSYVj6gLsxgkBwEkCAEwCUEEgYwxrTB+tyiEGfrRwjlXTG34MiQtJXbg5Qqd0ohdRW7MfwYY7vZiX/0h9hI8MqUralFaVPcnghAP0MSJm1YrqTcKNQEpARgkAmAwBBS3BS9aJzt+p6i28Nj+trB2Uu+vdzAFFLcFL1onO36nqLbw2P62sHZS7693GDALQKhZq5zQ3AYFGQVcWu+OD8c4yQyTpkGu09UkZu0SXSjWU0Onq7U6RnfhEnsCTZeNC3TB25octZQPnoe4yQyMhOMY",
"FTABAQAkAgE3AycUQhmZbaIbYjokFQIYJgRWZLcqJAUANwYnFEIZmW2iG2I6JBUCGCQHASQIATAJQQT2AlKGW/kOMjqayzeO0md523/fuhrhGEUU91uQpTiKo0I7wcPpKnmrwfQNPX6g0kEQl+VGaXa3e22lzfu5Tzp0Nwo1ASkBGCQCYDAEFOOMk13ScMKuT2hlaydi1yEJnhTqMAUU44yTXdJwwq5PaGVrJ2LXIQmeFOoYMAtAv2jJd1qd5miXbYesH1XrJ+vgyY0hzGuZ78N6Jw4Cb1oN1sLSpA+PNM0u7+hsEqcSvvn2eSV8EaRR+hg5YQjHDxg=",
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEGZf4rAtHkcaVU1u+UL507wm/18+2EeJN8alTOLYkkANflrPobEEAWfTZAofAtxkfKH6WH19p/qt/fz+c9gXv8zcKNQEpARgkAmAwBBT0+qfdyShnG+4Pq01pwOnrxdhHRjAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQPVrsFnfFplsQGV5m5EUua+rmo9hAr+OP1bvaifdLqiEIn3uXLTLoKmVUkPImRL2Fb+xcMEAqR2p7RM6ZlFCR20Y"
],
"0/62/5": 3,
"0/62/65532": 0,
"0/62/65533": 1,
"0/62/65528": [1, 3, 5, 8],
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11],
"0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
"0/63/0": [],
"0/63/1": [],
"0/63/2": 4,
"0/63/3": 3,
"0/63/65532": 0,
"0/63/65533": 2,
"0/63/65528": [2, 5],
"0/63/65529": [0, 1, 3, 4],
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/3/0": 0,
"1/3/1": 2,
"1/3/65532": 0,
"1/3/65533": 5,
"1/3/65528": [],
"1/3/65529": [0],
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/4/0": 128,
"1/4/65532": 1,
"1/4/65533": 4,
"1/4/65528": [0, 1, 2, 3],
"1/4/65529": [0, 1, 2, 3, 4, 5],
"1/4/65531": [0, 65528, 65529, 65531, 65532, 65533],
"1/29/0": [
{
"0": 514,
"1": 3
}
],
"1/29/1": [3, 4, 29, 258, 319486977],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 2,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/258/0": 0,
"1/258/7": 9,
"1/258/10": 0,
"1/258/11": 0,
"1/258/13": 0,
"1/258/14": 0,
"1/258/23": 0,
"1/258/26": 0,
"1/258/65532": 5,
"1/258/65533": 5,
"1/258/65528": [],
"1/258/65529": [0, 1, 2, 5],
"1/258/65531": [
0, 7, 10, 11, 13, 14, 23, 26, 65528, 65529, 65531, 65532, 65533
],
"1/319486977/319422464": "AAJgAAsCAAADAuEnBAxCSzM2TjJBMDEyMDWcAQD/BAECAKD5AQEdAQj/BCUCvg7wAcPxAf/vHwEAAAD//7mFDWkAAAAAzzAOaQAAAAAAZAAAAAAAAAD6AQDzFQHDAP8KFAAAAAEAAAAAAAAAAAAAAF0EAAAAAP4JEagIAABuCwAARQUFAAAAAEZUBW0jLA8AAEIGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASQYFDAgQgAFEEQUMAAUDPAAAAOEpPkKHWiu/RxEFAiTUJG4lRyZ4AAAAPAAAAEgGBQAAAAAASgYFAAAAAAD/CyIJEAAAAAAAAAAA",
"1/319486977/319422466": "2AAAAM0AAABxXL4uBiUBKQEaARcBGAEZAQMAABAAAAAAAgAAAAEAAA==",
"1/319486977/319422467": "",
"1/319486977/319422479": false,
"1/319486977/319422480": false,
"1/319486977/319422481": false,
"1/319486977/319422482": 40960,
"1/319486977/65532": 0,
"1/319486977/65533": 1,
"1/319486977/65528": [],
"1/319486977/65529": [],
"1/319486977/65531": [
65528, 65529, 65531, 319422464, 319422465, 319422466, 319422467,
319422468, 319422469, 319422479, 319422480, 319422481, 319422482, 65532,
65533
]
},
"attribute_subscriptions": []
}

View File

@@ -342,6 +342,55 @@
'state': 'on',
})
# ---
# name: test_binary_sensors[eve_shutter][binary_sensor.eve_shutter_switch_20eci1701_config_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.eve_shutter_switch_20eci1701_config_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Config status',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'window_covering_config_status',
'unique_id': '00000000000004D2-0000000000000094-MatterNodeDevice-1-WindowCoveringConfigStatus-258-7',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[eve_shutter][binary_sensor.eve_shutter_switch_20eci1701_config_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Eve Shutter Switch 20ECI1701 Config status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.eve_shutter_switch_20eci1701_config_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[heiman_motion_sensor_m1][binary_sensor.smart_motion_sensor_occupancy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1516,3 +1565,297 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[window_covering_full][binary_sensor.mock_full_window_covering_config_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.mock_full_window_covering_config_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Config status',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'window_covering_config_status',
'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-WindowCoveringConfigStatus-258-7',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[window_covering_full][binary_sensor.mock_full_window_covering_config_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Mock Full Window Covering Config status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_full_window_covering_config_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[window_covering_lift][binary_sensor.mock_lift_window_covering_config_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.mock_lift_window_covering_config_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Config status',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'window_covering_config_status',
'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-WindowCoveringConfigStatus-258-7',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[window_covering_lift][binary_sensor.mock_lift_window_covering_config_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Mock Lift Window Covering Config status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_lift_window_covering_config_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[window_covering_pa_lift][binary_sensor.longan_link_wncv_da01_config_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.longan_link_wncv_da01_config_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Config status',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'window_covering_config_status',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-WindowCoveringConfigStatus-258-7',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[window_covering_pa_lift][binary_sensor.longan_link_wncv_da01_config_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Longan link WNCV DA01 Config status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.longan_link_wncv_da01_config_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[window_covering_pa_tilt][binary_sensor.mock_pa_tilt_window_covering_config_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.mock_pa_tilt_window_covering_config_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Config status',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'window_covering_config_status',
'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-WindowCoveringConfigStatus-258-7',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[window_covering_pa_tilt][binary_sensor.mock_pa_tilt_window_covering_config_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Mock PA Tilt Window Covering Config status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_pa_tilt_window_covering_config_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[window_covering_tilt][binary_sensor.mock_tilt_window_covering_config_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.mock_tilt_window_covering_config_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Config status',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'window_covering_config_status',
'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-WindowCoveringConfigStatus-258-7',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[window_covering_tilt][binary_sensor.mock_tilt_window_covering_config_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Mock Tilt Window Covering Config status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_tilt_window_covering_config_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[zemismart_mt25b][binary_sensor.zemismart_mt25b_roller_motor_config_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.zemismart_mt25b_roller_motor_config_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Config status',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'window_covering_config_status',
'unique_id': '00000000000004D2-000000000000007A-MatterNodeDevice-1-WindowCoveringConfigStatus-258-7',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[zemismart_mt25b][binary_sensor.zemismart_mt25b_roller_motor_config_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Zemismart MT25B Roller Motor Config status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.zemismart_mt25b_roller_motor_config_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@@ -373,3 +373,24 @@ async def test_thermostat_occupancy(
state = hass.states.get("binary_sensor.longan_link_hvac_occupancy")
assert state
assert state.state == "off"
@pytest.mark.parametrize("node_fixture", ["eve_shutter"])
async def test_shutter(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test shutter ConfigStatus."""
# Eve Shutter default state (ConfigStatus = 9)
state = hass.states.get("binary_sensor.eve_shutter_switch_20eci1701_config_status")
assert state
assert state.state == "on"
# Eve Shutter ConfigStatus Operational bit not set
set_node_attribute(matter_node, 1, 258, 7, 8)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("binary_sensor.eve_shutter_switch_20eci1701_config_status")
assert state
assert state.state == "off"

View File

@@ -292,7 +292,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'last_restart',
'unique_id': '123456789ABC-sys-uptime',
'unit_of_measurement': None,
})
@@ -343,7 +343,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'vial_level',
'translation_key': 'left_slot_level',
'unique_id': '123456789ABC-cury:0-cury_left_level',
'unit_of_measurement': '%',
})
@@ -393,7 +393,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'vial_name',
'translation_key': 'left_slot_vial',
'unique_id': '123456789ABC-cury:0-cury_left_vial',
'unit_of_measurement': None,
})
@@ -443,7 +443,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'vial_level',
'translation_key': 'right_slot_level',
'unique_id': '123456789ABC-cury:0-cury_right_level',
'unit_of_measurement': '%',
})
@@ -493,7 +493,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'vial_name',
'translation_key': 'right_slot_vial',
'unique_id': '123456789ABC-cury:0-cury_right_vial',
'unit_of_measurement': None,
})
@@ -1126,7 +1126,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'last_restart',
'unique_id': '123456789ABC-sys-uptime',
'unit_of_measurement': None,
})
@@ -1183,7 +1183,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_with_channel_name',
'unique_id': '123456789ABC-cct:0-energy_cct',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -1239,7 +1239,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'power_with_channel_name',
'unique_id': '123456789ABC-cct:0-power_cct',
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
@@ -2576,7 +2576,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_consumed',
'unique_id': '123456789ABC-switch:1-consumed_energy_switch',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -2635,7 +2635,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-switch:1-ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -2977,7 +2977,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_consumed',
'unique_id': '123456789ABC-switch:3-consumed_energy_switch',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -3036,7 +3036,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-switch:3-ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -3255,7 +3255,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'last_restart',
'unique_id': '123456789ABC-sys-uptime',
'unit_of_measurement': None,
})
@@ -3480,7 +3480,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_consumed',
'unique_id': '123456789ABC-switch:0-consumed_energy_switch',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -3539,7 +3539,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-switch:0-ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -3881,7 +3881,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_consumed',
'unique_id': '123456789ABC-switch:2-consumed_energy_switch',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -3940,7 +3940,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-switch:2-ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -4779,7 +4779,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'last_restart',
'unique_id': '123456789ABC-sys-uptime',
'unit_of_measurement': None,
})
@@ -4830,7 +4830,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'detected_objects',
'translation_key': 'detected_objects_with_channel_name',
'unique_id': '123456789ABC-presencezone:201-presencezone_num_objects',
'unit_of_measurement': 'objects',
})
@@ -4882,7 +4882,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'detected_objects',
'translation_key': 'detected_objects_with_channel_name',
'unique_id': '123456789ABC-presencezone:202-presencezone_num_objects',
'unit_of_measurement': 'objects',
})
@@ -4934,7 +4934,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'detected_objects',
'translation_key': 'detected_objects_with_channel_name',
'unique_id': '123456789ABC-presencezone:200-presencezone_num_objects',
'unit_of_measurement': 'objects',
})
@@ -5750,7 +5750,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'last_restart',
'unique_id': '123456789ABC-sys-uptime',
'unit_of_measurement': None,
})
@@ -6740,7 +6740,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'last_restart',
'unique_id': '123456789ABC-sys-uptime',
'unit_of_measurement': None,
})
@@ -7769,7 +7769,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'last_restart',
'unique_id': '123456789ABC-sys-uptime',
'unit_of_measurement': None,
})
@@ -7994,7 +7994,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_consumed',
'unique_id': '123456789ABC-switch:0-consumed_energy_switch',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -8053,7 +8053,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-switch:0-ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -8451,7 +8451,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_consumed',
'unique_id': '123456789ABC-switch:1-consumed_energy_switch',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -8510,7 +8510,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-switch:1-ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -9329,7 +9329,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-emdata:0-total_act_ret',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -9380,7 +9380,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'last_restart',
'unique_id': '123456789ABC-sys-uptime',
'unit_of_measurement': None,
})
@@ -9434,7 +9434,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'neutral_current',
'unique_id': '123456789ABC-em:0-n_current',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
@@ -9664,7 +9664,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-emdata:0-a_total_act_ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -10114,7 +10114,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-emdata:0-b_total_act_ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -10564,7 +10564,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned',
'unique_id': '123456789ABC-emdata:0-c_total_act_ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})

View File

@@ -189,7 +189,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'vial_level',
'translation_key': 'left_slot_level',
'unique_id': '123456789ABC-cury:0-cury_left_level',
'unit_of_measurement': '%',
})
@@ -239,7 +239,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'vial_name',
'translation_key': 'left_slot_vial',
'unique_id': '123456789ABC-cury:0-cury_left_vial',
'unit_of_measurement': None,
})
@@ -289,7 +289,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'vial_level',
'translation_key': 'right_slot_level',
'unique_id': '123456789ABC-cury:0-cury_right_level',
'unit_of_measurement': '%',
})
@@ -339,7 +339,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'vial_name',
'translation_key': 'right_slot_vial',
'unique_id': '123456789ABC-cury:0-cury_right_vial',
'unit_of_measurement': None,
})
@@ -460,7 +460,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'session_duration',
'unique_id': '123456789ABC-number:202-number_time_charge',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
@@ -515,7 +515,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'session_energy',
'unique_id': '123456789ABC-number:201-number_energy_charge',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -574,7 +574,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_with_channel_name',
'unique_id': '123456789ABC-switch:0-energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -633,7 +633,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_consumed_with_channel_name',
'unique_id': '123456789ABC-switch:0-consumed_energy_switch',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -692,7 +692,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'energy_returned_with_channel_name',
'unique_id': '123456789ABC-switch:0-ret_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
@@ -741,12 +741,12 @@
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Average Temperature',
'original_name': 'Average temperature',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'average_temperature',
'unique_id': '123456789ABC-number:200-number_average_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
@@ -755,7 +755,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Test name Average Temperature',
'friendly_name': 'Test name Average temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
@@ -799,7 +799,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'rainfall_last_24h',
'unique_id': '123456789ABC-number:201-number_last_precipitation',
'unit_of_measurement': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
})

View File

@@ -2928,7 +2928,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'countdown',
'unique_id': 'tuya.owozxdzgbibizu4sjkcountdown_set',
'unique_id': 'tuya.owozxdzgbibizu4sjkcountdown',
'unit_of_measurement': None,
})
# ---
@@ -2953,6 +2953,71 @@
'state': 'unknown',
})
# ---
# name: test_platform_setup_and_discovery[select.ion1000pro_countdown_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'1',
'2',
'3',
'4',
'5',
'6',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.ion1000pro_countdown_2',
'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': 'Countdown',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'countdown',
'unique_id': 'tuya.owozxdzgbibizu4sjkcountdown_set',
'unit_of_measurement': None,
})
# ---
# name: test_platform_setup_and_discovery[select.ion1000pro_countdown_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'ION1000PRO Countdown',
'options': list([
'1',
'2',
'3',
'4',
'5',
'6',
]),
}),
'context': <ANY>,
'entity_id': 'select.ion1000pro_countdown_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_platform_setup_and_discovery[select.jardin_fraises_power_on_behavior-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -4088,6 +4153,65 @@
'state': 'power_on',
})
# ---
# name: test_platform_setup_and_discovery[select.seating_side_6_ch_smart_switch_power_on_behavior-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'0',
'1',
'2',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.seating_side_6_ch_smart_switch_power_on_behavior',
'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': 'Power on behavior',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'relay_status',
'unique_id': 'tuya.kxxrbv93k2vvkconqdtrelay_status',
'unit_of_measurement': None,
})
# ---
# name: test_platform_setup_and_discovery[select.seating_side_6_ch_smart_switch_power_on_behavior-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Seating side 6-ch Smart Switch Power on behavior',
'options': list([
'0',
'1',
'2',
]),
}),
'context': <ANY>,
'entity_id': 'select.seating_side_6_ch_smart_switch_power_on_behavior',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_platform_setup_and_discovery[select.security_light_indicator_light_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -2,15 +2,20 @@
from __future__ import annotations
from typing import Any
from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.components.tuya import DOMAIN
from homeassistant.const import Platform
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er, issue_registry as ir
@@ -83,3 +88,95 @@ async def test_sfkzq_deprecated_switch(
)
is not None
) is expected_issue
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH])
@pytest.mark.parametrize(
"mock_device_code",
["cz_PGEkBctAbtzKOZng"],
)
async def test_turn_on(
hass: HomeAssistant,
mock_manager: Manager,
mock_config_entry: MockConfigEntry,
mock_device: CustomerDevice,
) -> None:
"""Test turning on a switch."""
entity_id = "switch.din_socket"
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
state = hass.states.get(entity_id)
assert state is not None, f"{entity_id} does not exist"
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: entity_id,
},
blocking=True,
)
mock_manager.send_commands.assert_called_once_with(
mock_device.id, [{"code": "switch", "value": True}]
)
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH])
@pytest.mark.parametrize(
"mock_device_code",
["cz_PGEkBctAbtzKOZng"],
)
async def test_turn_off(
hass: HomeAssistant,
mock_manager: Manager,
mock_config_entry: MockConfigEntry,
mock_device: CustomerDevice,
) -> None:
"""Test turning off a switch."""
entity_id = "switch.din_socket"
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
state = hass.states.get(entity_id)
assert state is not None, f"{entity_id} does not exist"
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: entity_id,
},
blocking=True,
)
mock_manager.send_commands.assert_called_once_with(
mock_device.id, [{"code": "switch", "value": False}]
)
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH])
@pytest.mark.parametrize(
"mock_device_code",
["cz_PGEkBctAbtzKOZng"],
)
@pytest.mark.parametrize(
("initial_status", "expected_state"),
[
(True, "on"),
(False, "off"),
(None, STATE_UNKNOWN),
("some string", STATE_UNKNOWN),
],
)
async def test_state(
hass: HomeAssistant,
mock_manager: Manager,
mock_config_entry: MockConfigEntry,
mock_device: CustomerDevice,
initial_status: Any,
expected_state: str,
) -> None:
"""Test switch state."""
entity_id = "switch.din_socket"
mock_device.status["switch"] = initial_status
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
state = hass.states.get(entity_id)
assert state is not None, f"{entity_id} does not exist"
assert state.state == expected_state

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from typing import Any
from unittest.mock import patch
import pytest
@@ -13,7 +14,7 @@ from homeassistant.components.valve import (
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -95,3 +96,35 @@ async def test_close_valve(
mock_manager.send_commands.assert_called_once_with(
mock_device.id, [{"code": "switch_1", "value": False}]
)
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.VALVE])
@pytest.mark.parametrize(
"mock_device_code",
["sfkzq_ed7frwissyqrejic"],
)
@pytest.mark.parametrize(
("initial_status", "expected_state"),
[
(True, "open"),
(False, "closed"),
(None, STATE_UNKNOWN),
("some string", STATE_UNKNOWN),
],
)
async def test_state(
hass: HomeAssistant,
mock_manager: Manager,
mock_config_entry: MockConfigEntry,
mock_device: CustomerDevice,
initial_status: Any,
expected_state: str,
) -> None:
"""Test valve state."""
entity_id = "valve.jie_hashui_fa_valve_1"
mock_device.status["switch_1"] = initial_status
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
state = hass.states.get(entity_id)
assert state is not None, f"{entity_id} does not exist"
assert state.state == expected_state

View File

@@ -6,7 +6,7 @@ import dataclasses
from datetime import timedelta
import logging
import threading
from typing import Any, final
from typing import Any
from unittest.mock import MagicMock, PropertyMock, patch
from freezegun.api import FrozenDateTimeFactory
@@ -20,7 +20,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
@@ -1879,7 +1878,6 @@ async def test_change_entity_id(
self.remove_calls = []
async def async_added_to_hass(self):
await super().async_added_to_hass()
self.added_calls.append(None)
self.async_on_remove(lambda: result.append(1))
@@ -2898,107 +2896,3 @@ async def test_platform_state_write_from_init_unique_id(
# The early attempt to write is interpreted as a unique ID collision
assert "Platform test_platform does not generate unique IDs." in caplog.text
assert "Entity id already exists - ignoring: test.test" not in caplog.text
async def test_included_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test included entities are exposed via the entity_id attribute."""
entity_registry.async_get_or_create(
domain="hello",
platform="test",
unique_id="very_unique_oceans",
suggested_object_id="oceans",
)
entity_registry.async_get_or_create(
domain="hello",
platform="test",
unique_id="very_unique_continents",
suggested_object_id="continents",
)
entity_registry.async_get_or_create(
domain="hello",
platform="test",
unique_id="very_unique_moon",
suggested_object_id="moon",
)
class MockHelloBaseClass(entity.Entity):
"""Domain base entity platform domain Hello."""
@final
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {"extra": "beer"}
class MockHelloIncludedEntitiesClass(MockHelloBaseClass, entity.Entity):
"""Mock hello grouped entity class for a test integration."""
platform = MockEntityPlatform(hass, domain="hello", platform_name="test")
mock_entity = MockHelloIncludedEntitiesClass()
mock_entity.hass = hass
mock_entity.entity_id = "hello.universe"
mock_entity.unique_id = "very_unique_universe"
mock_entity._attr_included_unique_ids = [
"very_unique_continents",
"very_unique_oceans",
]
await platform.async_add_entities([mock_entity])
# Initiate mock grouped entity for hello domain
mock_entity.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.continents", "hello.oceans"]
# Add an entity to the group of included entities
mock_entity._attr_included_unique_ids = [
"very_unique_continents",
"very_unique_moon",
"very_unique_oceans",
]
mock_entity.async_set_included_entities()
mock_entity.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get("extra") == "beer"
assert state.attributes.get(ATTR_ENTITY_ID) == [
"hello.continents",
"hello.moon",
"hello.oceans",
]
# Remove an entity from the group of included entities
mock_entity._attr_included_unique_ids = ["very_unique_moon", "very_unique_oceans"]
mock_entity.async_set_included_entities()
mock_entity.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon", "hello.oceans"]
# Rename an included entity via the registry entity
entity_registry.async_update_entity(
entity_id="hello.moon", new_entity_id="hello.moon_light"
)
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon_light", "hello.oceans"]
# Remove an included entity from the registry entity
entity_registry.async_remove(entity_id="hello.oceans")
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon_light"]