mirror of
https://github.com/home-assistant/core.git
synced 2026-03-03 22:37:03 +01:00
Compare commits
10 Commits
synesthesi
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c311ff0464 | ||
|
|
c45675a01f | ||
|
|
9d92141812 | ||
|
|
501b973a98 | ||
|
|
fd4d8137da | ||
|
|
33881c1912 | ||
|
|
9bdb03dbe8 | ||
|
|
d2178ba458 | ||
|
|
06cdf3c5d2 | ||
|
|
84c994ab80 |
46
.claude/skills/github-pr-reviewer/SKILL.md
Normal file
46
.claude/skills/github-pr-reviewer/SKILL.md
Normal 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
|
||||
```
|
||||
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
53
homeassistant/components/smarla/button.py
Normal file
53
homeassistant/components/smarla/button.py
Normal 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")
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"send_diagnostics": {
|
||||
"name": "Send diagnostics"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"intensity": {
|
||||
"name": "Intensity"
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -165,6 +165,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"light": {
|
||||
"name": "[%key:component::light::title%]"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"cool_select_plus_temperature": {
|
||||
"name": "CoolSelect+ temperature"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
5
script/gen_copilot_instructions.py
Normal file → Executable 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
50
tests/components/smarla/snapshots/test_button.ambr
Normal file
50
tests/components/smarla/snapshots/test_button.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
||||
@@ -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',
|
||||
|
||||
@@ -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'>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
67
tests/components/smarla/test_button.py
Normal file
67
tests/components/smarla/test_button.py
Normal 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()
|
||||
@@ -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": {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user