mirror of
https://github.com/home-assistant/core.git
synced 2026-03-03 22:37:03 +01:00
Compare commits
2 Commits
dev
...
synesthesi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd4db314cd | ||
|
|
5b2e0e2a4f |
@@ -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
|
||||
```
|
||||
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_wheels_${{ matrix.arch }}.txt"
|
||||
requirements: "requirements_all.txt"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -30,11 +30,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"send_diagnostics": {
|
||||
"name": "Send diagnostics"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"intensity": {
|
||||
"name": "Intensity"
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -165,11 +165,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"light": {
|
||||
"name": "[%key:component::light::title%]"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"cool_select_plus_temperature": {
|
||||
"name": "CoolSelect+ temperature"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
5
script/gen_copilot_instructions.py
Executable file → Normal 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
# ---
|
||||
@@ -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',
|
||||
|
||||
@@ -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'>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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()
|
||||
@@ -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": {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user