mirror of
https://github.com/home-assistant/core.git
synced 2025-12-03 14:38:06 +00:00
Compare commits
4 Commits
knx-data-s
...
epenet-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1d088c3d5 | ||
|
|
10c2559e48 | ||
|
|
9ced2677f3 | ||
|
|
cd077b3616 |
@@ -15,8 +15,9 @@ from tuya_sharing import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_APP_TYPE,
|
||||
@@ -31,6 +32,9 @@ from .const import (
|
||||
TUYA_DISCOVERY_NEW,
|
||||
TUYA_HA_SIGNAL_UPDATE_ENTITY,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
# Suppress logs from the library, it logs unneeded on error
|
||||
logging.getLogger("tuya_sharing").setLevel(logging.CRITICAL)
|
||||
@@ -45,6 +49,12 @@ class HomeAssistantTuyaData(NamedTuple):
|
||||
listener: SharingDeviceListener
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Tuya component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool:
|
||||
"""Async setup hass config entry."""
|
||||
if CONF_APP_TYPE in entry.data:
|
||||
|
||||
@@ -381,5 +381,10 @@
|
||||
"default": "mdi:alarm-light"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_text_command": {
|
||||
"service": "mdi:pencil"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
111
homeassistant/components/tuya/services.py
Normal file
111
homeassistant/components/tuya/services.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Support for Tuya services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TuyaConfigEntry
|
||||
|
||||
SEND_TEXT_COMMAND = "send_text_command"
|
||||
|
||||
ATTR_CODE = "code"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
|
||||
SEND_TEXT_COMMAND_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_device(
|
||||
call: ServiceCall,
|
||||
) -> tuple[str, TuyaConfigEntry]:
|
||||
"""Get the tuya device ID and config entry related to a service call."""
|
||||
device_registry = dr.async_get(call.hass)
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
if (device_entry := device_registry.async_get(device_id)) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device_id",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
entry: TuyaConfigEntry | None
|
||||
for entry_id in device_entry.config_entries:
|
||||
if (entry := call.hass.config_entries.async_get_entry(entry_id)) is None:
|
||||
continue
|
||||
if entry.domain == DOMAIN:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_loaded",
|
||||
translation_placeholders={"entry": entry.title},
|
||||
)
|
||||
tuya_device_id = next(
|
||||
(
|
||||
key
|
||||
for key in entry.runtime_data.manager.device_map
|
||||
if (DOMAIN, key) in device_entry.identifiers
|
||||
),
|
||||
None,
|
||||
)
|
||||
if tuya_device_id is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="tuya_device_not_found",
|
||||
translation_placeholders={"device_id": device_entry.id},
|
||||
)
|
||||
return tuya_device_id, entry
|
||||
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_found",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
|
||||
async def _async_send_device_command(call: ServiceCall) -> None:
|
||||
"""Send Tuya device command."""
|
||||
tuya_device_id, config_entry = _async_get_device(call)
|
||||
|
||||
commands = [
|
||||
{
|
||||
"code": call.data[ATTR_CODE],
|
||||
"value": call.data[ATTR_VALUE],
|
||||
}
|
||||
]
|
||||
|
||||
LOGGER.debug("Sending commands for device %s: %s", tuya_device_id, commands)
|
||||
await call.hass.async_add_executor_job(
|
||||
config_entry.runtime_data.manager.send_commands,
|
||||
tuya_device_id,
|
||||
commands,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SEND_TEXT_COMMAND,
|
||||
_async_send_device_command,
|
||||
schema=SEND_TEXT_COMMAND_SCHEMA,
|
||||
)
|
||||
17
homeassistant/components/tuya/services.yaml
Normal file
17
homeassistant/components/tuya/services.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
send_text_command:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: tuya
|
||||
code:
|
||||
required: true
|
||||
example: "up_down"
|
||||
selector:
|
||||
text:
|
||||
value:
|
||||
required: true
|
||||
example: "up"
|
||||
selector:
|
||||
text:
|
||||
@@ -1002,5 +1002,25 @@
|
||||
"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})."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_text_command": {
|
||||
"name": "Send text command",
|
||||
"description": "Send a text command to a device.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Device",
|
||||
"description": "The ID of the device to send the command to."
|
||||
},
|
||||
"code": {
|
||||
"name": "Code",
|
||||
"description": "The name of the data point."
|
||||
},
|
||||
"value": {
|
||||
"name": "Value",
|
||||
"description": "The new value of the data point."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ async def test_device_registry(
|
||||
hass: HomeAssistant,
|
||||
mock_manager: Manager,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_devices: CustomerDevice,
|
||||
mock_devices: list[CustomerDevice],
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
|
||||
63
tests/components/tuya/test_services.py
Normal file
63
tests/components/tuya/test_services.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Test Tuya initialization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
from homeassistant.components.tuya.const import DOMAIN
|
||||
from homeassistant.components.tuya.services import SEND_TEXT_COMMAND
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import initialize_entry
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_services(
|
||||
hass: HomeAssistant,
|
||||
mock_manager: Manager,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setup of Tuya services."""
|
||||
|
||||
await initialize_entry(hass, mock_manager, mock_config_entry, [])
|
||||
|
||||
assert (services := hass.services.async_services_for_domain(DOMAIN))
|
||||
assert SEND_TEXT_COMMAND in services
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_device_code",
|
||||
["sjz_ftbc8rp8ipksdfpv"],
|
||||
)
|
||||
async def test_send_device_command(
|
||||
hass: HomeAssistant,
|
||||
mock_manager: Manager,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device: CustomerDevice,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Validate send_device_command."""
|
||||
|
||||
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "vpfdskpi8pr8cbtfzjs")}
|
||||
)
|
||||
assert device_entry is not None
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SEND_TEXT_COMMAND,
|
||||
{
|
||||
"device_id": device_entry.id,
|
||||
"code": "up_down",
|
||||
"value": "up",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_manager.send_commands.assert_called_once_with(
|
||||
"vpfdskpi8pr8cbtfzjs", [{"code": "up_down", "value": "up"}]
|
||||
)
|
||||
Reference in New Issue
Block a user