Compare commits

...

10 Commits

Author SHA1 Message Date
Robert Resch
c311ff0464 Fix wheels building by using arch dependent requirements_all file (#164675) 2026-03-03 21:55:59 +01:00
Dave T
c45675a01f Add additional diagnostic sensors to aurora_abb_powerone PV inverter (#164622) 2026-03-03 21:34:44 +01:00
erikbadman
9d92141812 Add support for active power limit in Kostal Plenticore (#164674) 2026-03-03 21:33:54 +01:00
Robin Lintermann
501b973a98 Add send diagnostics button to smarla (#164335) 2026-03-03 21:31:31 +01:00
Kamil Breguła
fd4d8137da Change reconfiguration-flow status to 'todo' in WebDAV (#164637)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-03 21:23:24 +01:00
Miguel Angel Nubla
33881c1912 Fix infinite loop in esphome assist_satellite (#163097)
Co-authored-by: Artur Pragacz <artur@pragacz.com>
2026-03-03 20:44:36 +01:00
Robin Lintermann
9bdb03dbe8 Set device classes and measurement units for Smarla (#164682) 2026-03-03 18:36:02 +00:00
epenet
d2178ba458 Cleanup deprecated tuya entities (#164657) 2026-03-03 19:31:09 +01:00
Abílio Costa
06cdf3c5d2 Add PR review Claude skill (#164626) 2026-03-03 18:21:51 +00:00
r2xj
84c994ab80 Add support for samsungce.lamp as light entity and when not under main component (#164448)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-03 18:29:36 +01:00
35 changed files with 1296 additions and 171 deletions

View File

@@ -0,0 +1,46 @@
---
name: github-pr-reviewer
description: Review a GitHub pull request and provide feedback comments. Use when the user says "review the current PR" or asks to review a specific PR.
---
# Review GitHub Pull Request
## Preparation:
- Check if the local commit matches the last one in the PR. If not, checkout the PR locally using 'gh pr checkout'.
- CRITICAL: If 'gh pr checkout' fails for ANY reason, you MUST immediately STOP.
- Do NOT attempt any workarounds.
- Do NOT proceed with the review.
- ALERT about the failure and WAIT for instructions.
- This is a hard requirement - no exceptions.
## Follow these steps:
1. Use 'gh pr view' to get the PR details and description.
2. Use 'gh pr diff' to see all the changes in the PR.
3. Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
4. Ensure any existing review comments have been addressed.
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
## IMPORTANT:
- Just review. DO NOT make any changes
- Be constructive and specific in your comments
- Suggest improvements where appropriate
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] Memory leak in homeassistant/components/sensor/my_sensor.py:143
- [PROBLEM] Inefficient algorithm in homeassistant/helpers/data_processing.py:87
- [SUGGESTION] Improve variable naming in homeassistant/helpers/config_validation.py:45
```

View File

@@ -209,4 +209,4 @@ jobs:
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txt"
requirements: "requirements_all_wheels_${{ matrix.arch }}.txt"

View File

@@ -61,7 +61,13 @@ class AuroraAbbDataUpdateCoordinator(DataUpdateCoordinator[dict[str, float]]):
frequency = self.client.measure(4)
i_leak_dcdc = self.client.measure(6)
i_leak_inverter = self.client.measure(7)
power_in_1 = self.client.measure(8)
power_in_2 = self.client.measure(9)
temperature_c = self.client.measure(21)
voltage_in_1 = self.client.measure(23)
current_in_1 = self.client.measure(25)
voltage_in_2 = self.client.measure(26)
current_in_2 = self.client.measure(27)
r_iso = self.client.measure(30)
energy_wh = self.client.cumulated_energy(5)
[alarm, *_] = self.client.alarms()
@@ -87,7 +93,13 @@ class AuroraAbbDataUpdateCoordinator(DataUpdateCoordinator[dict[str, float]]):
data["grid_frequency"] = round(frequency, 1)
data["i_leak_dcdc"] = i_leak_dcdc
data["i_leak_inverter"] = i_leak_inverter
data["power_in_1"] = round(power_in_1, 1)
data["power_in_2"] = round(power_in_2, 1)
data["temp"] = round(temperature_c, 1)
data["voltage_in_1"] = round(voltage_in_1, 1)
data["current_in_1"] = round(current_in_1, 1)
data["voltage_in_2"] = round(voltage_in_2, 1)
data["current_in_2"] = round(current_in_2, 1)
data["r_iso"] = r_iso
data["totalenergy"] = round(energy_wh / 1000, 2)
data["alarm"] = alarm

View File

@@ -68,6 +68,7 @@ SENSOR_TYPES = [
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
translation_key="grid_frequency",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
@@ -88,6 +89,60 @@ SENSOR_TYPES = [
translation_key="i_leak_inverter",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="power_in_1",
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="power_in_1",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="power_in_2",
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="power_in_2",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="voltage_in_1",
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_in_1",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="current_in_1",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_in_1",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="voltage_in_2",
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_in_2",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="current_in_2",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_in_2",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="alarm",
device_class=SensorDeviceClass.ENUM,

View File

@@ -24,9 +24,18 @@
"alarm": {
"name": "Alarm status"
},
"current_in_1": {
"name": "String 1 current"
},
"current_in_2": {
"name": "String 2 current"
},
"grid_current": {
"name": "Grid current"
},
"grid_frequency": {
"name": "Grid frequency"
},
"grid_voltage": {
"name": "Grid voltage"
},
@@ -36,6 +45,12 @@
"i_leak_inverter": {
"name": "Inverter leak current"
},
"power_in_1": {
"name": "String 1 power"
},
"power_in_2": {
"name": "String 2 power"
},
"power_output": {
"name": "Power output"
},
@@ -44,6 +59,12 @@
},
"total_energy": {
"name": "Total energy"
},
"voltage_in_1": {
"name": "String 1 voltage"
},
"voltage_in_2": {
"name": "String 2 voltage"
}
}
}

View File

@@ -524,14 +524,10 @@ class EsphomeAssistSatellite(
self._active_pipeline_index = 0
maybe_pipeline_index = 0
while True:
if not (ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index)):
break
if not (ww_state := self.hass.states.get(ww_entity_id)):
continue
if ww_state.state == wake_word_phrase:
while ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index):
if (
ww_state := self.hass.states.get(ww_entity_id)
) and ww_state.state == wake_word_phrase:
# First match
self._active_pipeline_index = maybe_pipeline_index
break

View File

@@ -67,6 +67,22 @@ NUMBER_SETTINGS_DATA = [
fmt_from="format_round",
fmt_to="format_round_back",
),
PlenticoreNumberEntityDescription(
key="active_power_limitation",
device_class=NumberDeviceClass.POWER,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
icon="mdi:solar-power",
name="Active Power Limitation",
native_unit_of_measurement=UnitOfPower.WATT,
native_max_value=10000,
native_min_value=0,
native_step=1,
module_id="devices:local",
data_id="Inverter:ActivePowerLimitation",
fmt_from="format_round",
fmt_to="format_round_back",
),
]

View File

@@ -0,0 +1,53 @@
"""Support for the Swing2Sleep Smarla button entities."""
from dataclasses import dataclass
from pysmarlaapi.federwiege.services.classes import Property
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FederwiegeConfigEntry
from .entity import SmarlaBaseEntity, SmarlaEntityDescription
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class SmarlaButtonEntityDescription(SmarlaEntityDescription, ButtonEntityDescription):
"""Class describing Swing2Sleep Smarla button entity."""
BUTTONS: list[SmarlaButtonEntityDescription] = [
SmarlaButtonEntityDescription(
key="send_diagnostics",
translation_key="send_diagnostics",
service="system",
property="send_diagnostic_data",
entity_category=EntityCategory.CONFIG,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: FederwiegeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Smarla buttons from config entry."""
federwiege = config_entry.runtime_data
async_add_entities(SmarlaButton(federwiege, desc) for desc in BUTTONS)
class SmarlaButton(SmarlaBaseEntity, ButtonEntity):
"""Representation of a Smarla button."""
entity_description: SmarlaButtonEntityDescription
_property: Property[str]
def press(self) -> None:
"""Press the button."""
self._property.set("Sent from Home Assistant")

View File

@@ -6,7 +6,13 @@ DOMAIN = "smarla"
HOST = "https://devices.swing2sleep.de"
PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE]
PLATFORMS = [
Platform.BUTTON,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
DEVICE_MODEL_NAME = "Smarla"
MANUFACTURER_NAME = "Swing2Sleep"

View File

@@ -9,6 +9,7 @@ from homeassistant.components.number import (
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -32,6 +33,7 @@ NUMBERS: list[SmarlaNumberEntityDescription] = [
native_max_value=100,
native_min_value=0,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
),
]

View File

@@ -5,6 +5,7 @@ from dataclasses import dataclass
from pysmarlaapi.federwiege.services.classes import Property
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
@@ -35,6 +36,7 @@ SENSORS: list[SmarlaSensorEntityDescription] = [
property="oscillation",
multiple=True,
value_pos=0,
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.MILLIMETERS,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -45,6 +47,7 @@ SENSORS: list[SmarlaSensorEntityDescription] = [
property="oscillation",
multiple=True,
value_pos=1,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
state_class=SensorStateClass.MEASUREMENT,
),

View File

@@ -30,6 +30,11 @@
}
},
"entity": {
"button": {
"send_diagnostics": {
"name": "Send diagnostics"
}
},
"number": {
"intensity": {
"name": "Intensity"

View File

@@ -5,7 +5,11 @@ from typing import Any
from pysmarlaapi.federwiege.services.classes import Property
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -26,12 +30,14 @@ SWITCHES: list[SmarlaSwitchEntityDescription] = [
name=None,
service="babywiege",
property="swing_active",
device_class=SwitchDeviceClass.SWITCH,
),
SmarlaSwitchEntityDescription(
key="smart_mode",
translation_key="smart_mode",
service="babywiege",
property="smart_mode",
device_class=SwitchDeviceClass.SWITCH,
),
]

View File

@@ -592,7 +592,8 @@ def process_status(status: dict[str, ComponentStatus]) -> dict[str, ComponentSta
if "burner" in component:
burner_id = int(component.split("-")[-1])
component = f"burner-0{burner_id}"
if component in status:
# Don't delete 'lamp' component even when disabled
if component in status and component != "lamp":
del status[component]
for component_status in status.values():
process_component_status(component_status)

View File

@@ -3,9 +3,18 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from typing import Any, cast
from pysmartthings import Attribute, Capability, Command, DeviceEvent, SmartThings
from pysmartthings import (
Attribute,
Capability,
Category,
Command,
ComponentStatus,
DeviceEvent,
SmartThings,
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -21,6 +30,10 @@ from homeassistant.components.light import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
)
from . import FullDevice, SmartThingsConfigEntry
from .const import MAIN
@@ -32,6 +45,22 @@ CAPABILITIES = (
Capability.COLOR_TEMPERATURE,
)
LAMP_CAPABILITY_EXISTS: dict[str, Callable[[FullDevice, ComponentStatus], bool]] = {
"lamp": lambda _, __: True,
"hood": lambda device, component: (
Capability.SAMSUNG_CE_CONNECTION_STATE not in component
or component[Capability.SAMSUNG_CE_CONNECTION_STATE][
Attribute.CONNECTION_STATE
].value
!= "disconnected"
),
"cavity-02": lambda _, __: True,
"main": lambda device, component: (
device.device.components[MAIN].manufacturer_category
in {Category.MICROWAVE, Category.OVEN, Category.RANGE}
),
}
async def async_setup_entry(
hass: HomeAssistant,
@@ -40,12 +69,25 @@ async def async_setup_entry(
) -> None:
"""Add lights for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
SmartThingsLight(entry_data.client, device)
entities: list[LightEntity] = [
SmartThingsLight(entry_data.client, device, component)
for device in entry_data.devices.values()
if Capability.SWITCH in device.status[MAIN]
and any(capability in device.status[MAIN] for capability in CAPABILITIES)
for component in device.status
if (
Capability.SWITCH in device.status[MAIN]
and any(capability in device.status[MAIN] for capability in CAPABILITIES)
and Capability.SAMSUNG_CE_LAMP not in device.status[component]
)
]
entities.extend(
SmartThingsLamp(entry_data.client, device, component)
for device in entry_data.devices.values()
for component, exists_fn in LAMP_CAPABILITY_EXISTS.items()
if component in device.status
and Capability.SAMSUNG_CE_LAMP in device.status[component]
and exists_fn(device, device.status[component])
)
async_add_entities(entities)
def convert_scale(
@@ -71,7 +113,9 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
# highest kelvin found supported across 20+ handlers.
_attr_max_color_temp_kelvin = 9000 # 111 mireds
def __init__(self, client: SmartThings, device: FullDevice) -> None:
def __init__(
self, client: SmartThings, device: FullDevice, component: str = MAIN
) -> None:
"""Initialize a SmartThingsLight."""
super().__init__(
client,
@@ -82,6 +126,7 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
Capability.SWITCH_LEVEL,
Capability.SWITCH,
},
component=component,
)
color_modes = set()
if self.supports_capability(Capability.COLOR_TEMPERATURE):
@@ -236,3 +281,117 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
) is None:
return None
return state == "on"
class SmartThingsLamp(SmartThingsEntity, LightEntity):
"""Define a SmartThings lamp component as a light entity."""
_attr_translation_key = "light"
def __init__(
self, client: SmartThings, device: FullDevice, component: str = MAIN
) -> None:
"""Initialize a SmartThingsLamp."""
super().__init__(
client,
device,
{Capability.SWITCH, Capability.SAMSUNG_CE_LAMP},
component=component,
)
levels = (
self.get_attribute_value(
Capability.SAMSUNG_CE_LAMP, Attribute.SUPPORTED_BRIGHTNESS_LEVEL
)
or []
)
color_modes = set()
if "off" not in levels or len(levels) > 2:
color_modes.add(ColorMode.BRIGHTNESS)
if not color_modes:
color_modes.add(ColorMode.ONOFF)
self._attr_color_mode = list(color_modes)[0]
self._attr_supported_color_modes = color_modes
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the lamp on."""
# Switch/brightness/transition
if ATTR_BRIGHTNESS in kwargs:
await self.async_set_level(kwargs[ATTR_BRIGHTNESS])
return
if self.supports_capability(Capability.SWITCH):
await self.execute_device_command(Capability.SWITCH, Command.ON)
# if no switch, turn on via brightness level
else:
await self.async_set_level(255)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the lamp off."""
if self.supports_capability(Capability.SWITCH):
await self.execute_device_command(Capability.SWITCH, Command.OFF)
return
await self.execute_device_command(
Capability.SAMSUNG_CE_LAMP,
Command.SET_BRIGHTNESS_LEVEL,
argument="off",
)
async def async_set_level(self, brightness: int) -> None:
"""Set lamp brightness via supported levels."""
levels = (
self.get_attribute_value(
Capability.SAMSUNG_CE_LAMP, Attribute.SUPPORTED_BRIGHTNESS_LEVEL
)
or []
)
# remove 'off' for brightness mapping
if "off" in levels:
levels = [level for level in levels if level != "off"]
level = percentage_to_ordered_list_item(
levels, int(round(brightness * 100 / 255))
)
await self.execute_device_command(
Capability.SAMSUNG_CE_LAMP,
Command.SET_BRIGHTNESS_LEVEL,
argument=level,
)
# turn on switch separately if needed
if (
self.supports_capability(Capability.SWITCH)
and not self.is_on
and brightness > 0
):
await self.execute_device_command(Capability.SWITCH, Command.ON)
def _update_attr(self) -> None:
"""Update lamp-specific attributes."""
level = self.get_attribute_value(
Capability.SAMSUNG_CE_LAMP, Attribute.BRIGHTNESS_LEVEL
)
if level is None:
self._attr_brightness = None
return
levels = (
self.get_attribute_value(
Capability.SAMSUNG_CE_LAMP, Attribute.SUPPORTED_BRIGHTNESS_LEVEL
)
or []
)
if "off" in levels:
if level == "off":
self._attr_brightness = 0
return
levels = [level for level in levels if level != "off"]
percent = ordered_list_item_to_percentage(levels, level)
self._attr_brightness = int(convert_scale(percent, 100, 255))
@property
def is_on(self) -> bool | None:
"""Return true if lamp is on."""
if self.supports_capability(Capability.SWITCH):
state = self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
if state is None:
return None
return state == "on"
if (brightness := self.brightness) is not None:
return brightness > 0
return None

View File

@@ -165,6 +165,11 @@
}
}
},
"light": {
"light": {
"name": "[%key:component::light::title%]"
}
},
"number": {
"cool_select_plus_temperature": {
"name": "CoolSelect+ temperature"

View File

@@ -1097,11 +1097,5 @@
"action_dpcode_not_found": {
"message": "Unable to process action as the device does not provide a corresponding function code (expected one of {expected} in {available})."
}
},
"issues": {
"deprecated_entity_new_valve": {
"description": "The Tuya entity `{entity}` is deprecated, replaced by a new valve entity.\nPlease update your dashboards, automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue.",
"title": "{name} is deprecated"
}
}
}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
@@ -10,35 +9,19 @@ from tuya_device_handlers.device_wrapper.common import DPCodeBooleanWrapper
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from . import TuyaConfigEntry
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .entity import TuyaEntity
@dataclass(frozen=True, kw_only=True)
class TuyaDeprecatedSwitchEntityDescription(SwitchEntityDescription):
"""Describes Tuya deprecated switch entity."""
deprecated: str
breaks_in_ha_version: str
# All descriptions can be found here. Mostly the Boolean data types in the
# default instruction set of each category end up being a Switch.
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
@@ -664,14 +647,6 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SFKZQ: (
TuyaDeprecatedSwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
deprecated="deprecated_entity_new_valve",
breaks_in_ha_version="2026.4.0",
),
),
DeviceCategory.SGBJ: (
SwitchEntityDescription(
key=DPCode.MUFFLING,
@@ -937,7 +912,6 @@ async def async_setup_entry(
) -> None:
"""Set up tuya sensors dynamically through tuya discovery."""
manager = entry.runtime_data.manager
entity_registry = er.async_get(hass)
@callback
def async_discover_device(device_ids: list[str]) -> None:
@@ -954,12 +928,6 @@ async def async_setup_entry(
device, description.key, prefer_function=True
)
)
and _check_deprecation(
hass,
device,
description,
entity_registry,
)
)
async_add_entities(entities)
@@ -971,55 +939,6 @@ async def async_setup_entry(
)
def _check_deprecation(
hass: HomeAssistant,
device: CustomerDevice,
description: SwitchEntityDescription,
entity_registry: er.EntityRegistry,
) -> bool:
"""Check entity deprecation.
Returns:
`True` if the entity should be created, `False` otherwise.
"""
# Not deprecated, just create it
if not isinstance(description, TuyaDeprecatedSwitchEntityDescription):
return True
unique_id = f"tuya.{device.id}{description.key}"
entity_id = entity_registry.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, unique_id)
# Deprecated and not present in registry, skip creation
if not entity_id or not (entity_entry := entity_registry.async_get(entity_id)):
return False
# Deprecated and present in registry but disabled, remove it and skip creation
if entity_entry.disabled:
entity_registry.async_remove(entity_id)
async_delete_issue(
hass,
DOMAIN,
f"deprecated_entity_{unique_id}",
)
return False
# Deprecated and present in registry and enabled, raise issue and create it
async_create_issue(
hass,
DOMAIN,
f"deprecated_entity_{unique_id}",
breaks_in_ha_version=description.breaks_in_ha_version,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=description.deprecated,
translation_placeholders={
"name": f"{device.name} {entity_entry.name or entity_entry.original_name}",
"entity": entity_id,
},
)
return True
class TuyaSwitchEntity(TuyaEntity, SwitchEntity):
"""Tuya Switch Device."""

View File

@@ -129,10 +129,7 @@ rules:
status: exempt
comment: |
This integration does not have entities.
reconfiguration-flow:
status: exempt
comment: |
Nothing to reconfigure.
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: exempt

5
script/gen_copilot_instructions.py Normal file → Executable file
View File

@@ -17,6 +17,8 @@ SKILLS_DIR = Path(".claude/skills")
AGENTS_FILE = Path("AGENTS.md")
OUTPUT_FILE = Path(".github/copilot-instructions.md")
EXCLUDED_SKILLS = {"github-pr-reviewer"}
def gather_skills() -> list[tuple[str, Path]]:
"""Gather all skills from the skills directory.
@@ -32,6 +34,9 @@ def gather_skills() -> list[tuple[str, Path]]:
if not skill_dir.is_dir():
continue
if skill_dir.name in EXCLUDED_SKILLS:
continue
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
continue

View File

@@ -38,7 +38,13 @@ def _simulated_returns(index, global_measure=None):
4: 50.789, # frequency
6: 1.2345, # leak dcdc
7: 2.3456, # leak inverter
8: 12.345, # power in 1
9: 23.456, # power in 2
21: 9.876, # temperature
23: 123.456, # voltage in 1
25: 0.9876, # current in 1
26: 234.567, # voltage in 2
27: 1.234, # current in 2
30: 0.1234, # Isolation resistance
5: 12345, # energy
}
@@ -116,9 +122,15 @@ async def test_sensors(hass: HomeAssistant, entity_registry: EntityRegistry) ->
sensors = [
("sensor.mydevicename_grid_voltage", "235.9"),
("sensor.mydevicename_grid_current", "2.8"),
("sensor.mydevicename_frequency", "50.8"),
("sensor.mydevicename_grid_frequency", "50.8"),
("sensor.mydevicename_dc_dc_leak_current", "1.2345"),
("sensor.mydevicename_inverter_leak_current", "2.3456"),
("sensor.mydevicename_string_1_power", "12.3"),
("sensor.mydevicename_string_2_power", "23.5"),
("sensor.mydevicename_string_1_voltage", "123.5"),
("sensor.mydevicename_string_1_current", "1.0"),
("sensor.mydevicename_string_2_voltage", "234.6"),
("sensor.mydevicename_string_2_current", "1.2"),
("sensor.mydevicename_isolation_resistance", "0.1234"),
]
for entity_id, _ in sensors:

View File

@@ -2091,6 +2091,129 @@ async def test_secondary_pipeline(
assert (await get_pipeline(None)) == "Primary Pipeline"
@pytest.mark.timeout(5)
async def test_pipeline_start_missing_wake_word_entity_state(
hass: HomeAssistant,
mock_client: APIClient,
mock_esphome_device: MockESPHomeDeviceType,
) -> None:
"""Test pipeline selection when a wake word entity has no state.
Regression test for an infinite loop that occurred when a wake word entity
existed in the entity registry but had no state in the state machine.
"""
assert await async_setup_component(hass, "assist_pipeline", {})
pipeline_data = hass.data[KEY_ASSIST_PIPELINE]
pipeline_id_to_name: dict[str, str] = {}
for pipeline_name in ("Primary Pipeline", "Secondary Pipeline"):
pipeline = await pipeline_data.pipeline_store.async_create_item(
{
"name": pipeline_name,
"language": "en-US",
"conversation_engine": None,
"conversation_language": "en-US",
"tts_engine": None,
"tts_language": None,
"tts_voice": None,
"stt_engine": None,
"stt_language": None,
"wake_word_entity": None,
"wake_word_id": None,
}
)
pipeline_id_to_name[pipeline.id] = pipeline_name
device_config = AssistSatelliteConfiguration(
available_wake_words=[
AssistSatelliteWakeWord("okay_nabu", "Okay Nabu", ["en"]),
AssistSatelliteWakeWord("hey_jarvis", "Hey Jarvis", ["en"]),
],
active_wake_words=["hey_jarvis"],
max_active_wake_words=2,
)
mock_client.get_voice_assistant_configuration.return_value = device_config
configuration_set = asyncio.Event()
async def wrapper(*args, **kwargs):
device_config.active_wake_words = kwargs["active_wake_words"]
configuration_set.set()
mock_client.set_voice_assistant_configuration = AsyncMock(side_effect=wrapper)
mock_device = await mock_esphome_device(
mock_client=mock_client,
device_info={
"voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT
| VoiceAssistantFeature.ANNOUNCE
},
)
await hass.async_block_till_done()
satellite = get_satellite_entity(hass, mock_device.device_info.mac_address)
assert satellite is not None
# Set primary/secondary wake words and assistants
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: "select.test_wake_word", "option": "Okay Nabu"},
blocking=True,
)
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: "select.test_assistant", "option": "Primary Pipeline"},
blocking=True,
)
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: "select.test_wake_word_2", "option": "Hey Jarvis"},
blocking=True,
)
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.test_assistant_2",
"option": "Secondary Pipeline",
},
blocking=True,
)
await hass.async_block_till_done()
# Remove state for primary wake word entity to simulate the bug scenario:
# entity exists in the registry but has no state in the state machine.
hass.states.async_remove("select.test_wake_word")
async def get_pipeline(wake_word_phrase):
with patch(
"homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream",
) as mock_pipeline_from_audio_stream:
await satellite.handle_pipeline_start(
conversation_id="",
flags=0,
audio_settings=VoiceAssistantAudioSettings(),
wake_word_phrase=wake_word_phrase,
)
mock_pipeline_from_audio_stream.assert_called_once()
kwargs = mock_pipeline_from_audio_stream.call_args_list[0].kwargs
return pipeline_id_to_name[kwargs["pipeline_id"]]
# The primary wake word entity has no state, so the loop must skip it.
# The secondary wake word entity still has state, so "Hey Jarvis" matches.
assert (await get_pipeline("Hey Jarvis")) == "Secondary Pipeline"
# "Okay Nabu" can't match because its entity has no state — falls back to
# default pipeline (index 0).
assert (await get_pipeline("Okay Nabu")) == "Primary Pipeline"
# No wake word phrase also falls back to default.
assert (await get_pipeline(None)) == "Primary Pipeline"
async def test_custom_wake_words(
hass: HomeAssistant,
mock_client: APIClient,

View File

@@ -25,6 +25,7 @@ DEFAULT_SETTING_VALUES = {
"Properties:VersionMC": "01.46",
"Battery:MinSoc": "5",
"Battery:MinHomeComsumption": "50",
"Inverter:ActivePowerLimitation": "8000",
},
"scb:network": {"Hostname": "scb"},
}
@@ -49,6 +50,15 @@ DEFAULT_SETTINGS = {
id="Battery:MinHomeComsumption",
type="byte",
),
SettingsData(
min="0",
max="10000",
default=None,
access="readwrite",
unit="W",
id="Inverter:ActivePowerLimitation",
type="byte",
),
],
"scb:network": [
SettingsData(

View File

@@ -52,6 +52,7 @@ async def test_entry_diagnostics(
"devices:local": [
"min='5' max='100' default=None access='readwrite' unit='%' id='Battery:MinSoc' type='byte'",
"min='50' max='38000' default=None access='readwrite' unit='W' id='Battery:MinHomeComsumption' type='byte'",
"min='0' max='10000' default=None access='readwrite' unit='W' id='Inverter:ActivePowerLimitation' type='byte'",
],
"scb:network": [
"min='1' max='63' default=None access='readwrite' unit=None id='Hostname' type='string'"

View File

@@ -41,6 +41,7 @@ async def test_setup_all_entries(
assert (
entity_registry.async_get("number.scb_battery_min_home_consumption") is not None
)
assert entity_registry.async_get("number.scb_active_power_limitation") is not None
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@@ -77,6 +78,7 @@ async def test_setup_no_entries(
assert entity_registry.async_get("number.scb_battery_min_soc") is None
assert entity_registry.async_get("number.scb_battery_min_home_consumption") is None
assert entity_registry.async_get("number.scb_active_power_limitation") is None
@pytest.mark.usefixtures("entity_registry_enabled_by_default")

View File

@@ -105,6 +105,7 @@ def _mock_system_service() -> MagicMock:
mock_system_service.props = {
"firmware_update": MagicMock(spec=Property),
"firmware_update_status": MagicMock(spec=Property),
"send_diagnostic_data": MagicMock(spec=Property),
}
mock_system_service.props["firmware_update"].get.return_value = 0

View File

@@ -0,0 +1,50 @@
# serializer version: 1
# name: test_entities[button.smarla_send_diagnostics-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': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.smarla_send_diagnostics',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Send diagnostics',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Send diagnostics',
'platform': 'smarla',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'send_diagnostics',
'unique_id': 'ABCD-send_diagnostics',
'unit_of_measurement': None,
})
# ---
# name: test_entities[button.smarla_send_diagnostics-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Smarla Send diagnostics',
}),
'context': <ANY>,
'entity_id': 'button.smarla_send_diagnostics',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -37,7 +37,7 @@
'supported_features': 0,
'translation_key': 'intensity',
'unique_id': 'ABCD-intensity',
'unit_of_measurement': None,
'unit_of_measurement': '%',
})
# ---
# name: test_entities[number.smarla_intensity-state]
@@ -48,6 +48,7 @@
'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.smarla_intensity',

View File

@@ -76,8 +76,11 @@
'name': None,
'object_id_base': 'Amplitude',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None,
'original_name': 'Amplitude',
'platform': 'smarla',
@@ -92,6 +95,7 @@
# name: test_entities[sensor.smarla_amplitude-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'distance',
'friendly_name': 'Smarla Amplitude',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
@@ -129,8 +133,11 @@
'name': None,
'object_id_base': 'Period',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Period',
'platform': 'smarla',
@@ -145,6 +152,7 @@
# name: test_entities[sensor.smarla_period-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Smarla Period',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,

View File

@@ -23,7 +23,7 @@
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': None,
'platform': 'smarla',
@@ -38,6 +38,7 @@
# name: test_entities[switch.smarla-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Smarla',
}),
'context': <ANY>,
@@ -72,7 +73,7 @@
'object_id_base': 'Smart Mode',
'options': dict({
}),
'original_device_class': None,
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Smart Mode',
'platform': 'smarla',
@@ -87,6 +88,7 @@
# name: test_entities[switch.smarla_smart_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Smarla Smart Mode',
}),
'context': <ANY>,

View File

@@ -0,0 +1,67 @@
"""Test button platform for Swing2Sleep Smarla integration."""
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
BUTTON_ENTITIES = [
{
"entity_id": "button.smarla_send_diagnostics",
"service": "system",
"property": "send_diagnostic_data",
},
]
@pytest.mark.usefixtures("mock_federwiege")
async def test_entities(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Smarla entities."""
with (
patch("homeassistant.components.smarla.PLATFORMS", [Platform.BUTTON]),
):
assert await setup_integration(hass, mock_config_entry)
await snapshot_platform(
hass, entity_registry, snapshot, mock_config_entry.entry_id
)
@pytest.mark.parametrize("entity_info", BUTTON_ENTITIES)
async def test_button_action(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_federwiege: MagicMock,
entity_info: dict[str, str],
) -> None:
"""Test Smarla Button press behavior."""
assert await setup_integration(hass, mock_config_entry)
mock_button_property = mock_federwiege.get_property(
entity_info["service"], entity_info["property"]
)
entity_id = entity_info["entity_id"]
# Turn on
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mock_button_property.set.assert_called_once()

View File

@@ -471,13 +471,13 @@
"lamp": {
"switch": {
"switch": {
"value": "off",
"value": "on",
"timestamp": "2025-11-12T00:04:46.554Z"
}
},
"samsungce.lamp": {
"brightnessLevel": {
"value": "high",
"value": "low",
"timestamp": "2025-11-12T00:04:44.863Z"
},
"supportedBrightnessLevel": {

View File

@@ -125,6 +125,414 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_ks_hood_01001][light.range_hood_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.range_hood_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': 'fa5fca25-fa7a-1807-030a-2f72ee0f7bff_lamp',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_hood_01001][light.range_hood_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': 127,
'color_mode': <ColorMode.BRIGHTNESS: 'brightness'>,
'friendly_name': 'Range hood Light',
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.range_hood_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[da_ks_microwave_0101x][light.microwave_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.microwave_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_hood',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_microwave_0101x][light.microwave_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': None,
'color_mode': None,
'friendly_name': 'Microwave Light',
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.microwave_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_ks_oven_01061][light.oven_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.oven_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_oven_01061][light.oven_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'color_mode': <ColorMode.ONOFF: 'onoff'>,
'friendly_name': 'Oven Light',
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.oven_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[da_ks_oven_0107x][light.kitchen_oven_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.kitchen_oven_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': '199d7863-ad04-793d-176d-658f10062575_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_oven_0107x][light.kitchen_oven_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'color_mode': None,
'friendly_name': 'Kitchen oven Light',
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.kitchen_oven_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_ks_range_0101x][light.vulcan_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.vulcan_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_range_0101x][light.vulcan_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'color_mode': <ColorMode.ONOFF: 'onoff'>,
'friendly_name': 'Vulcan Light',
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.vulcan_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[da_ks_walloven_0107x][light.four_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.four_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': '1c77a562-df00-7d6a-ca73-b67f6d4c4607_cavity-02',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_walloven_0107x][light.four_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'color_mode': None,
'friendly_name': 'Four Light',
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.four_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_ks_walloven_0107x][light.four_light_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.four_light_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': '1c77a562-df00-7d6a-ca73-b67f6d4c4607_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_walloven_0107x][light.four_light_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'color_mode': <ColorMode.ONOFF: 'onoff'>,
'friendly_name': 'Four Light',
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.four_light_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[ge_in_wall_smart_dimmer][light.basement_exit_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -30,6 +30,7 @@ from homeassistant.const import (
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant, State
@@ -451,3 +452,193 @@ async def test_availability_at_start(
"""Test unavailable at boot."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("light.standing_light").state == STATE_UNAVAILABLE
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
@pytest.mark.parametrize(
("service", "command"),
[
(SERVICE_TURN_ON, Command.ON),
(SERVICE_TURN_OFF, Command.OFF),
],
)
async def test_lamp_with_switch(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
service: str,
command: Command,
) -> None:
"""Test samsungce.lamp on/off with switch capability."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
LIGHT_DOMAIN,
service,
{ATTR_ENTITY_ID: "light.range_hood_light"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
Capability.SWITCH,
command,
"lamp",
)
@pytest.mark.parametrize(
("brightness", "brightness_level"),
[(128, "low"), (129, "high"), (240, "high")],
)
@pytest.mark.parametrize(
("device_fixture", "entity_id", "device_id", "component"),
[
(
"da_ks_hood_01001",
"light.range_hood_light",
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
"lamp",
),
(
"da_ks_microwave_0101x",
"light.microwave_light",
"2bad3237-4886-e699-1b90-4a51a3d55c8a",
"hood",
),
],
)
async def test_lamp_component_with_brightness(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
device_id: str,
component: str,
brightness: int,
brightness_level: str,
) -> None:
"""Test samsungce.lamp on/off with switch capability."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: brightness},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
device_id,
Capability.SAMSUNG_CE_LAMP,
Command.SET_BRIGHTNESS_LEVEL,
component,
argument=brightness_level,
)
@pytest.mark.parametrize("device_fixture", ["da_ks_range_0101x"])
@pytest.mark.parametrize(
("service", "argument"),
[(SERVICE_TURN_ON, "extraHigh"), (SERVICE_TURN_OFF, "off")],
)
async def test_lamp_without_switch(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
service: str,
argument: str,
) -> None:
"""Test samsungce.lamp on/off without switch capability."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
LIGHT_DOMAIN,
service,
{ATTR_ENTITY_ID: "light.vulcan_light"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"2c3cbaa0-1899-5ddc-7b58-9d657bd48f18",
Capability.SAMSUNG_CE_LAMP,
Command.SET_BRIGHTNESS_LEVEL,
MAIN,
argument=argument,
)
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
async def test_lamp_from_off(
hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test samsungce.lamp on with brightness level from off state."""
set_attribute_value(
devices, Capability.SWITCH, Attribute.SWITCH, "off", component="lamp"
)
await setup_integration(hass, mock_config_entry)
assert hass.states.get("light.range_hood_light").state == STATE_OFF
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.range_hood_light", ATTR_BRIGHTNESS: 255},
blocking=True,
)
assert devices.execute_device_command.mock_calls == [
call(
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
Capability.SAMSUNG_CE_LAMP,
Command.SET_BRIGHTNESS_LEVEL,
"lamp",
argument="high",
),
call(
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
Capability.SWITCH,
Command.ON,
"lamp",
),
]
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
async def test_lamp_unknown_switch(
hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test lamp state becomes unknown when switch state is unknown."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("light.range_hood_light").state == STATE_ON
await trigger_update(
hass,
devices,
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
Capability.SWITCH,
Attribute.SWITCH,
None,
component="lamp",
)
assert hass.states.get("light.range_hood_light").state == STATE_UNKNOWN
@pytest.mark.parametrize("device_fixture", ["da_ks_range_0101x"])
async def test_lamp_unknown_brightness(
hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test lamp state becomes unknown when brightness level is unknown."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("light.vulcan_light").state == STATE_ON
await trigger_update(
hass,
devices,
"2c3cbaa0-1899-5ddc-7b58-9d657bd48f18",
Capability.SAMSUNG_CE_LAMP,
Attribute.BRIGHTNESS_LEVEL,
None,
)
assert hass.states.get("light.vulcan_light").state == STATE_UNKNOWN

View File

@@ -15,10 +15,9 @@ from homeassistant.components.switch import (
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.components.tuya import DOMAIN
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
from homeassistant.helpers import entity_registry as er
from . import MockDeviceListener, check_selective_state_update, initialize_entry
@@ -89,57 +88,6 @@ async def test_selective_state_update(
)
@pytest.mark.parametrize(
("preexisting_entity", "disabled_by", "expected_entity", "expected_issue"),
[
(True, None, True, True),
(True, er.RegistryEntryDisabler.USER, False, False),
(False, None, False, False),
],
)
@pytest.mark.parametrize(
"mock_device_code",
["sfkzq_rzklytdei8i8vo37"],
)
async def test_sfkzq_deprecated_switch(
hass: HomeAssistant,
mock_manager: Manager,
mock_config_entry: MockConfigEntry,
mock_device: CustomerDevice,
issue_registry: ir.IssueRegistry,
entity_registry: er.EntityRegistry,
preexisting_entity: bool,
disabled_by: er.RegistryEntryDisabler,
expected_entity: bool,
expected_issue: bool,
) -> None:
"""Test switch deprecation issue."""
original_entity_id = "switch.balkonbewasserung_switch"
entity_unique_id = "tuya.73ov8i8iedtylkzrqzkfsswitch"
if preexisting_entity:
suggested_id = original_entity_id.replace(f"{SWITCH_DOMAIN}.", "")
entity_registry.async_get_or_create(
SWITCH_DOMAIN,
DOMAIN,
entity_unique_id,
suggested_object_id=suggested_id,
disabled_by=disabled_by,
)
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
assert (
entity_registry.async_get(original_entity_id) is not None
) is expected_entity
assert (
issue_registry.async_get_issue(
domain=DOMAIN,
issue_id=f"deprecated_entity_{entity_unique_id}",
)
is not None
) is expected_issue
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH])
@pytest.mark.parametrize(
"mock_device_code",