Compare commits

..

2 Commits

Author SHA1 Message Date
Michael Hansen
cd4db314cd Implement suggestions 2026-03-03 11:58:18 -06:00
Michael Hansen
5b2e0e2a4f Add missing parameters from handle API 2026-03-03 11:27:30 -06:00
37 changed files with 309 additions and 1303 deletions

View File

@@ -1,46 +0,0 @@
---
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_wheels_${{ matrix.arch }}.txt"
requirements: "requirements_all.txt"

View File

@@ -61,13 +61,7 @@ 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()
@@ -93,13 +87,7 @@ 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,7 +68,6 @@ 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(
@@ -89,60 +88,6 @@ 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,18 +24,9 @@
"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"
},
@@ -45,12 +36,6 @@
"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"
},
@@ -59,12 +44,6 @@
},
"total_energy": {
"name": "Total energy"
},
"voltage_in_1": {
"name": "String 1 voltage"
},
"voltage_in_2": {
"name": "String 2 voltage"
}
}
}

View File

@@ -524,10 +524,14 @@ class EsphomeAssistSatellite(
self._active_pipeline_index = 0
maybe_pipeline_index = 0
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:
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:
# First match
self._active_pipeline_index = maybe_pipeline_index
break

View File

@@ -627,13 +627,17 @@ class IntentHandleView(http.HomeAssistantView):
{
vol.Required("name"): cv.string,
vol.Optional("data"): vol.Schema({cv.string: object}),
vol.Optional("language"): cv.string,
vol.Optional("assistant"): vol.Any(cv.string, None),
vol.Optional("device_id"): vol.Any(cv.string, None),
vol.Optional("satellite_id"): vol.Any(cv.string, None),
}
)
)
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Handle intent with name/data."""
hass = request.app[http.KEY_HASS]
language = hass.config.language
language = data.get("language", hass.config.language)
try:
intent_name = data["name"]
@@ -641,14 +645,21 @@ class IntentHandleView(http.HomeAssistantView):
key: {"value": value} for key, value in data.get("data", {}).items()
}
intent_result = await intent.async_handle(
hass, DOMAIN, intent_name, slots, "", self.context(request)
hass,
DOMAIN,
intent_name,
slots,
"",
self.context(request),
language=language,
assistant=data.get("assistant"),
device_id=data.get("device_id"),
satellite_id=data.get("satellite_id"),
)
except (intent.IntentHandleError, intent.MatchFailedError) as err:
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_speech(str(err))
if intent_result is None:
intent_result = intent.IntentResponse(language=language) # type: ignore[unreachable]
intent_result.async_set_speech("Sorry, I couldn't handle that")
intent_result.async_set_error(
intent.IntentResponseErrorCode.FAILED_TO_HANDLE, str(err)
)
return self.json(intent_result)

View File

@@ -67,22 +67,6 @@ 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

@@ -1,53 +0,0 @@
"""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,13 +6,7 @@ DOMAIN = "smarla"
HOST = "https://devices.swing2sleep.de"
PLATFORMS = [
Platform.BUTTON,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE]
DEVICE_MODEL_NAME = "Smarla"
MANUFACTURER_NAME = "Swing2Sleep"

View File

@@ -9,7 +9,6 @@ from homeassistant.components.number import (
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -33,7 +32,6 @@ 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,7 +5,6 @@ from dataclasses import dataclass
from pysmarlaapi.federwiege.services.classes import Property
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
@@ -36,7 +35,6 @@ SENSORS: list[SmarlaSensorEntityDescription] = [
property="oscillation",
multiple=True,
value_pos=0,
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.MILLIMETERS,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -47,7 +45,6 @@ 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,11 +30,6 @@
}
},
"entity": {
"button": {
"send_diagnostics": {
"name": "Send diagnostics"
}
},
"number": {
"intensity": {
"name": "Intensity"

View File

@@ -5,11 +5,7 @@ from typing import Any
from pysmarlaapi.federwiege.services.classes import Property
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -30,14 +26,12 @@ 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,8 +592,7 @@ 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}"
# Don't delete 'lamp' component even when disabled
if component in status and component != "lamp":
if component in status:
del status[component]
for component_status in status.values():
process_component_status(component_status)

View File

@@ -3,18 +3,9 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from typing import Any, cast
from pysmartthings import (
Attribute,
Capability,
Category,
Command,
ComponentStatus,
DeviceEvent,
SmartThings,
)
from pysmartthings import Attribute, Capability, Command, DeviceEvent, SmartThings
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -30,10 +21,6 @@ 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
@@ -45,22 +32,6 @@ 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,
@@ -69,25 +40,12 @@ async def async_setup_entry(
) -> None:
"""Add lights for a config entry."""
entry_data = entry.runtime_data
entities: list[LightEntity] = [
SmartThingsLight(entry_data.client, device, component)
async_add_entities(
SmartThingsLight(entry_data.client, device)
for device in entry_data.devices.values()
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])
if Capability.SWITCH in device.status[MAIN]
and any(capability in device.status[MAIN] for capability in CAPABILITIES)
)
async_add_entities(entities)
def convert_scale(
@@ -113,9 +71,7 @@ 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, component: str = MAIN
) -> None:
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Initialize a SmartThingsLight."""
super().__init__(
client,
@@ -126,7 +82,6 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
Capability.SWITCH_LEVEL,
Capability.SWITCH,
},
component=component,
)
color_modes = set()
if self.supports_capability(Capability.COLOR_TEMPERATURE):
@@ -281,117 +236,3 @@ 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,11 +165,6 @@
}
}
},
"light": {
"light": {
"name": "[%key:component::light::title%]"
}
},
"number": {
"cool_select_plus_temperature": {
"name": "CoolSelect+ temperature"

View File

@@ -1097,5 +1097,11 @@
"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,6 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from tuya_device_handlers.device_wrapper.base import DeviceWrapper
@@ -9,19 +10,35 @@ 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 TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .const import DOMAIN, 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
@@ -647,6 +664,14 @@ 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,
@@ -912,6 +937,7 @@ 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:
@@ -928,6 +954,12 @@ async def async_setup_entry(
device, description.key, prefer_function=True
)
)
and _check_deprecation(
hass,
device,
description,
entity_registry,
)
)
async_add_entities(entities)
@@ -939,6 +971,55 @@ 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,7 +129,10 @@ rules:
status: exempt
comment: |
This integration does not have entities.
reconfiguration-flow: todo
reconfiguration-flow:
status: exempt
comment: |
Nothing to reconfigure.
repair-issues: todo
stale-devices:
status: exempt

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

@@ -17,8 +17,6 @@ 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.
@@ -34,9 +32,6 @@ 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,13 +38,7 @@ 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
}
@@ -122,15 +116,9 @@ async def test_sensors(hass: HomeAssistant, entity_registry: EntityRegistry) ->
sensors = [
("sensor.mydevicename_grid_voltage", "235.9"),
("sensor.mydevicename_grid_current", "2.8"),
("sensor.mydevicename_grid_frequency", "50.8"),
("sensor.mydevicename_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,129 +2091,6 @@ 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

@@ -4,6 +4,7 @@ from typing import Any
import pytest
from homeassistant.components import conversation
from homeassistant.components.button import SERVICE_PRESS
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
@@ -12,6 +13,7 @@ from homeassistant.components.cover import (
SERVICE_STOP_COVER,
CoverState,
)
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
from homeassistant.components.valve import (
DOMAIN as VALVE_DOMAIN,
@@ -82,6 +84,70 @@ async def test_http_handle_intent(
}
},
"language": hass.config.language,
"response_type": intent.IntentResponseType.ACTION_DONE.value,
"data": {"targets": [], "success": [], "failed": []},
}
async def test_http_language_device_satellite_id(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser
) -> None:
"""Test handle intent with language, device id, and satellite id."""
device_id = "test-device-id"
satellite_id = "test-satellite-id"
language = "en-GB"
class TestIntentHandler(intent.IntentHandler):
"""Test Intent Handler."""
intent_type = "TestIntent"
async def async_handle(self, intent_obj: intent.Intent):
"""Handle the intent."""
assert intent_obj.context.user_id == hass_admin_user.id
assert intent_obj.device_id == device_id
assert intent_obj.satellite_id == satellite_id
assert intent_obj.language == language
response = intent_obj.create_response()
response.async_set_speech("Test response")
response.async_set_speech_slots({"slot1": "value 1", "slot2": 2})
return response
intent.async_register(hass, TestIntentHandler())
result = await async_setup_component(hass, "intent", {})
assert result
client = await hass_client()
resp = await client.post(
"/api/intent/handle",
json={
"name": "TestIntent",
"language": language,
"device_id": device_id,
"satellite_id": satellite_id,
},
)
assert resp.status == 200
data = await resp.json()
# Verify language, device id, and satellite id were passed through.
# Also check speech slots.
assert data == {
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Test response",
}
},
"speech_slots": {
"slot1": "value 1",
"slot2": 2,
},
"language": language,
"response_type": "action_done",
"data": {"targets": [], "success": [], "failed": []},
}
@@ -113,6 +179,60 @@ async def test_http_handle_intent_match_failure(
assert "DUPLICATE_NAME" in data["speech"]["plain"]["speech"]
async def test_http_assistant(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser
) -> None:
"""Test handle intent only targets exposed entities with 'assistant' set."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "intent", {})
hass.states.async_set(
"cover.garage_door_1", "closed", {ATTR_FRIENDLY_NAME: "Garage Door 1"}
)
async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
client = await hass_client()
# Exposed
async_expose_entity(hass, conversation.DOMAIN, "cover.garage_door_1", True)
resp = await client.post(
"/api/intent/handle",
json={
"name": "HassTurnOn",
"data": {"name": "Garage Door 1"},
"assistant": conversation.DOMAIN,
},
)
assert resp.status == 200
data = await resp.json()
assert data["response_type"] == intent.IntentResponseType.ACTION_DONE.value
# Not exposed
async_expose_entity(hass, conversation.DOMAIN, "cover.garage_door_1", False)
resp = await client.post(
"/api/intent/handle",
json={
"name": "HassTurnOn",
"data": {"name": "Garage Door 1"},
"assistant": conversation.DOMAIN,
},
)
assert resp.status == 200
data = await resp.json()
assert data["response_type"] == intent.IntentResponseType.ERROR.value
assert data["data"]["code"] == intent.IntentResponseErrorCode.FAILED_TO_HANDLE.value
# No assistant (exposure is irrelevant)
resp = await client.post(
"/api/intent/handle",
json={"name": "HassTurnOn", "data": {"name": "Garage Door 1"}},
)
assert resp.status == 200
data = await resp.json()
assert data["response_type"] == intent.IntentResponseType.ACTION_DONE.value
async def test_cover_intents_loading(hass: HomeAssistant) -> None:
"""Test Cover Intents Loading."""
assert await async_setup_component(hass, "intent", {})

View File

@@ -25,7 +25,6 @@ DEFAULT_SETTING_VALUES = {
"Properties:VersionMC": "01.46",
"Battery:MinSoc": "5",
"Battery:MinHomeComsumption": "50",
"Inverter:ActivePowerLimitation": "8000",
},
"scb:network": {"Hostname": "scb"},
}
@@ -50,15 +49,6 @@ 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,7 +52,6 @@ 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,7 +41,6 @@ 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")
@@ -78,7 +77,6 @@ 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,7 +105,6 @@ 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

@@ -1,50 +0,0 @@
# 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': '%',
'unit_of_measurement': None,
})
# ---
# name: test_entities[number.smarla_intensity-state]
@@ -48,7 +48,6 @@
'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.smarla_intensity',

View File

@@ -76,11 +76,8 @@
'name': None,
'object_id_base': 'Amplitude',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_device_class': None,
'original_icon': None,
'original_name': 'Amplitude',
'platform': 'smarla',
@@ -95,7 +92,6 @@
# 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'>,
@@ -133,11 +129,8 @@
'name': None,
'object_id_base': 'Period',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_device_class': None,
'original_icon': None,
'original_name': 'Period',
'platform': 'smarla',
@@ -152,7 +145,6 @@
# 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': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'smarla',
@@ -38,7 +38,6 @@
# name: test_entities[switch.smarla-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Smarla',
}),
'context': <ANY>,
@@ -73,7 +72,7 @@
'object_id_base': 'Smart Mode',
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_device_class': None,
'original_icon': None,
'original_name': 'Smart Mode',
'platform': 'smarla',
@@ -88,7 +87,6 @@
# name: test_entities[switch.smarla_smart_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Smarla Smart Mode',
}),
'context': <ANY>,

View File

@@ -1,67 +0,0 @@
"""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": "on",
"value": "off",
"timestamp": "2025-11-12T00:04:46.554Z"
}
},
"samsungce.lamp": {
"brightnessLevel": {
"value": "low",
"value": "high",
"timestamp": "2025-11-12T00:04:44.863Z"
},
"supportedBrightnessLevel": {

View File

@@ -125,414 +125,6 @@
'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,7 +30,6 @@ from homeassistant.const import (
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant, State
@@ -452,193 +451,3 @@ 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,9 +15,10 @@ 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
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from . import MockDeviceListener, check_selective_state_update, initialize_entry
@@ -88,6 +89,57 @@ 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",