mirror of
https://github.com/home-assistant/core.git
synced 2026-03-09 21:49:51 +00:00
Compare commits
14 Commits
revert_bin
...
mqtt-vacuu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e7fb43345 | ||
|
|
9f1efb656a | ||
|
|
c671173d5c | ||
|
|
a6309cea50 | ||
|
|
35fccc76a2 | ||
|
|
8098329139 | ||
|
|
bd4bbf2d26 | ||
|
|
e4d6e57f44 | ||
|
|
461f391a01 | ||
|
|
89e9be796d | ||
|
|
58e4090cc8 | ||
|
|
8b9b296600 | ||
|
|
79de280343 | ||
|
|
27004c4077 |
@@ -18,6 +18,8 @@ ABBREVIATIONS = {
|
||||
"bri_stat_t": "brightness_state_topic",
|
||||
"bri_tpl": "brightness_template",
|
||||
"bri_val_tpl": "brightness_value_template",
|
||||
"cln_segmnts_cmd_t": "clean_segments_command_topic",
|
||||
"cln_segmnts_cmd_tpl": "clean_segments_command_template",
|
||||
"clr_temp_cmd_tpl": "color_temp_command_template",
|
||||
"clrm_stat_t": "color_mode_state_topic",
|
||||
"clrm_val_tpl": "color_mode_value_template",
|
||||
@@ -184,6 +186,7 @@ ABBREVIATIONS = {
|
||||
"rgbww_cmd_t": "rgbww_command_topic",
|
||||
"rgbww_stat_t": "rgbww_state_topic",
|
||||
"rgbww_val_tpl": "rgbww_value_template",
|
||||
"segmnts": "segments",
|
||||
"send_cmd_t": "send_command_topic",
|
||||
"send_if_off": "send_if_off",
|
||||
"set_fan_spd_t": "set_fan_speed_topic",
|
||||
|
||||
@@ -1468,6 +1468,7 @@ class MqttEntity(
|
||||
self._config = config
|
||||
self._setup_from_config(self._config)
|
||||
self._setup_common_attributes_from_config(self._config)
|
||||
self._process_entity_update()
|
||||
|
||||
# Prepare MQTT subscriptions
|
||||
self.attributes_prepare_discovery_update(config)
|
||||
@@ -1567,9 +1568,14 @@ class MqttEntity(
|
||||
# Set the entity name if needed
|
||||
self._set_entity_name(config)
|
||||
|
||||
@callback
|
||||
def _setup_from_config(self, config: ConfigType) -> None:
|
||||
"""(Re)Setup the entity."""
|
||||
|
||||
@callback
|
||||
def _process_entity_update(self) -> None:
|
||||
"""Process an entity discovery update."""
|
||||
|
||||
@abstractmethod
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
|
||||
@@ -10,12 +10,13 @@ import voluptuous as vol
|
||||
from homeassistant.components import vacuum
|
||||
from homeassistant.components.vacuum import (
|
||||
ENTITY_ID_FORMAT,
|
||||
Segment,
|
||||
StateVacuumEntity,
|
||||
VacuumActivity,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -27,7 +28,7 @@ from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
|
||||
from .entity import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import ReceiveMessage
|
||||
from .models import MqttCommandTemplate, ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
from .util import valid_publish_topic
|
||||
|
||||
@@ -52,6 +53,9 @@ POSSIBLE_STATES: dict[str, VacuumActivity] = {
|
||||
STATE_CLEANING: VacuumActivity.CLEANING,
|
||||
}
|
||||
|
||||
CONF_SEGMENTS = "segments"
|
||||
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC = "clean_segments_command_topic"
|
||||
CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE = "clean_segments_command_template"
|
||||
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
|
||||
CONF_PAYLOAD_TURN_ON = "payload_turn_on"
|
||||
CONF_PAYLOAD_TURN_OFF = "payload_turn_off"
|
||||
@@ -137,8 +141,39 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
|
||||
MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/"
|
||||
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
def validate_clean_area_config(config: ConfigType) -> ConfigType:
|
||||
"""Check for a valid configuration and check segments."""
|
||||
if (config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC not in config) or (
|
||||
not config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config
|
||||
):
|
||||
raise vol.Invalid(
|
||||
f"Options `{CONF_SEGMENTS}` and "
|
||||
f"`{CONF_CLEAN_SEGMENTS_COMMAND_TOPIC}` must be defined together"
|
||||
)
|
||||
segments: list[str]
|
||||
if segments := config[CONF_SEGMENTS]:
|
||||
if not config.get(CONF_UNIQUE_ID):
|
||||
raise vol.Invalid(
|
||||
f"Option `{CONF_SEGMENTS}` requires `{CONF_UNIQUE_ID}` to be configured"
|
||||
)
|
||||
unique_segments: set[str] = set()
|
||||
for segment in segments:
|
||||
segment_id, _, _ = segment.partition(".")
|
||||
if not segment_id or segment_id in unique_segments:
|
||||
raise vol.Invalid(
|
||||
f"The `{CONF_SEGMENTS}` option contains an invalid or non-"
|
||||
f"unique segment ID '{segment_id}'. Got {segments}"
|
||||
)
|
||||
unique_segments.add(segment_id)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_SEGMENTS, default=[]): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
),
|
||||
@@ -164,7 +199,10 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA)
|
||||
PLATFORM_SCHEMA_MODERN = vol.All(_BASE_SCHEMA, validate_clean_area_config)
|
||||
DISCOVERY_SCHEMA = vol.All(
|
||||
_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_clean_area_config
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -191,9 +229,11 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
|
||||
|
||||
_segments: list[Segment]
|
||||
_command_topic: str | None
|
||||
_set_fan_speed_topic: str | None
|
||||
_send_command_topic: str | None
|
||||
_clean_segments_command_topic: str
|
||||
_payloads: dict[str, str | None]
|
||||
|
||||
def __init__(
|
||||
@@ -229,6 +269,23 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
self._attr_supported_features = _strings_to_services(
|
||||
supported_feature_strings, STRING_TO_SERVICE
|
||||
)
|
||||
if config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config:
|
||||
self._attr_supported_features |= VacuumEntityFeature.CLEAN_AREA
|
||||
segments: list[str] = config[CONF_SEGMENTS]
|
||||
self._segments = [
|
||||
Segment(id=segment_id, name=name or segment_id)
|
||||
for segment_id, _, name in [
|
||||
segment.partition(".") for segment in segments
|
||||
]
|
||||
]
|
||||
self._clean_segments_command_topic = config[
|
||||
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC
|
||||
]
|
||||
self._clean_segments_command_template = MqttCommandTemplate(
|
||||
config.get(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE),
|
||||
entity=self,
|
||||
).async_render
|
||||
|
||||
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
|
||||
self._command_topic = config.get(CONF_COMMAND_TOPIC)
|
||||
self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC)
|
||||
@@ -246,6 +303,20 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
)
|
||||
}
|
||||
|
||||
@callback
|
||||
def _process_entity_update(self) -> None:
|
||||
"""Check vacuum segments with registry entry."""
|
||||
if (
|
||||
self._attr_supported_features & VacuumEntityFeature.CLEAN_AREA
|
||||
and (last_seen := self.last_seen_segments) is not None
|
||||
and {s.id: s for s in last_seen} != {s.id: s for s in self._segments}
|
||||
):
|
||||
self.async_create_segments_issue()
|
||||
|
||||
async def mqtt_async_added_to_hass(self) -> None:
|
||||
"""Check vacuum segments with registry entry."""
|
||||
self._process_entity_update()
|
||||
|
||||
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
|
||||
"""Update the entity state attributes."""
|
||||
self._state_attrs.update(payload)
|
||||
@@ -277,6 +348,19 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
"""(Re)Subscribe to topics."""
|
||||
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
|
||||
|
||||
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
|
||||
"""Perform an area clean."""
|
||||
await self.async_publish_with_config(
|
||||
self._clean_segments_command_topic,
|
||||
self._clean_segments_command_template(
|
||||
json_dumps(segment_ids), {"value": segment_ids}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_get_segments(self) -> list[Segment]:
|
||||
"""Return the available segments."""
|
||||
return self._segments
|
||||
|
||||
async def _async_publish_command(self, feature: VacuumEntityFeature) -> None:
|
||||
"""Publish a command."""
|
||||
if self._command_topic is None:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from copy import deepcopy
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -30,6 +30,7 @@ from homeassistant.components.vacuum import (
|
||||
from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
|
||||
from .common import (
|
||||
help_custom_config,
|
||||
@@ -63,7 +64,11 @@ from .common import (
|
||||
|
||||
from tests.common import async_fire_mqtt_message
|
||||
from tests.components.vacuum import common
|
||||
from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient
|
||||
from tests.typing import (
|
||||
MqttMockHAClientGenerator,
|
||||
MqttMockPahoClient,
|
||||
WebSocketGenerator,
|
||||
)
|
||||
|
||||
COMMAND_TOPIC = "vacuum/command"
|
||||
SEND_COMMAND_TOPIC = "vacuum/send_command"
|
||||
@@ -82,6 +87,27 @@ DEFAULT_CONFIG = {
|
||||
}
|
||||
}
|
||||
|
||||
CONFIG_CLEAN_SEGMENTS_1 = {
|
||||
mqtt.DOMAIN: {
|
||||
vacuum.DOMAIN: {
|
||||
"name": "test",
|
||||
"unique_id": "veryunique",
|
||||
"segments": ["Livingroom", "Kitchen"],
|
||||
"clean_segments_command_topic": "vacuum/clean_segment",
|
||||
}
|
||||
}
|
||||
}
|
||||
CONFIG_CLEAN_SEGMENTS_2 = {
|
||||
mqtt.DOMAIN: {
|
||||
vacuum.DOMAIN: {
|
||||
"name": "test",
|
||||
"unique_id": "veryunique",
|
||||
"segments": ["1.Livingroom", "2.Kitchen"],
|
||||
"clean_segments_command_topic": "vacuum/clean_segment",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}}
|
||||
|
||||
CONFIG_ALL_SERVICES = help_custom_config(
|
||||
@@ -294,6 +320,341 @@ async def test_command_without_command_topic(
|
||||
mqtt_mock.async_publish.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_1])
|
||||
async def test_clean_segments_initial_setup_without_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
) -> None:
|
||||
"""Test cleanable segments initial setup does not fire repair flow."""
|
||||
await mqtt_mock_entry()
|
||||
issue_registry = ir.async_get(hass)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_1])
|
||||
async def test_clean_segments_command_without_id(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
) -> None:
|
||||
"""Test cleanable segments without ID."""
|
||||
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
||||
entity_registry.async_get_or_create(
|
||||
vacuum.DOMAIN,
|
||||
mqtt.DOMAIN,
|
||||
"veryunique",
|
||||
config_entry=config_entry,
|
||||
suggested_object_id="test",
|
||||
)
|
||||
entity_registry.async_update_entity_options(
|
||||
"vacuum.test",
|
||||
vacuum.DOMAIN,
|
||||
{
|
||||
"area_mapping": {"Nabu Casa": ["Kitchen", "Livingroom"]},
|
||||
"last_seen_segments": [
|
||||
{"id": "Livingroom", "name": "Livingroom"},
|
||||
{"id": "Kitchen", "name": "Kitchen"},
|
||||
],
|
||||
},
|
||||
)
|
||||
mqtt_mock = await mqtt_mock_entry()
|
||||
await hass.async_block_till_done()
|
||||
issue_registry = ir.async_get(hass)
|
||||
# The area mapping was already set
|
||||
# so we do not expect a repair flow
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
state = hass.states.get("vacuum.test")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
await common.async_clean_area(hass, ["Nabu Casa"], entity_id="vacuum.test")
|
||||
assert (
|
||||
call("vacuum/clean_segment", '["Kitchen","Livingroom"]', 0, False)
|
||||
in mqtt_mock.async_publish.mock_calls
|
||||
)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"]["segments"] == [
|
||||
{"id": "Livingroom", "name": "Livingroom", "group": None},
|
||||
{"id": "Kitchen", "name": "Kitchen", "group": None},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_2])
|
||||
async def test_clean_segments_command_with_id(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
) -> None:
|
||||
"""Test cleanable segments with ID."""
|
||||
mqtt_mock = await mqtt_mock_entry()
|
||||
# Set the area mapping
|
||||
entity_registry.async_update_entity_options(
|
||||
"vacuum.test",
|
||||
vacuum.DOMAIN,
|
||||
{
|
||||
"area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("vacuum.test")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
await common.async_clean_area(hass, ["Kitchen"], entity_id="vacuum.test")
|
||||
assert (
|
||||
call("vacuum/clean_segment", '["2"]', 0, False)
|
||||
in mqtt_mock.async_publish.mock_calls
|
||||
)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"]["segments"] == [
|
||||
{"id": "1", "name": "Livingroom", "group": None},
|
||||
{"id": "2", "name": "Kitchen", "group": None},
|
||||
]
|
||||
|
||||
|
||||
async def test_clean_segments_command_update(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test cleanable segments update via discovery."""
|
||||
# Prepare original entity config entry
|
||||
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
||||
entity_registry.async_get_or_create(
|
||||
vacuum.DOMAIN,
|
||||
mqtt.DOMAIN,
|
||||
"veryunique",
|
||||
config_entry=config_entry,
|
||||
suggested_object_id="test",
|
||||
)
|
||||
entity_registry.async_update_entity_options(
|
||||
"vacuum.test",
|
||||
vacuum.DOMAIN,
|
||||
{
|
||||
"area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]},
|
||||
"last_seen_segments": [
|
||||
{"id": "1", "name": "Livingroom"},
|
||||
{"id": "2", "name": "Kitchen"},
|
||||
],
|
||||
},
|
||||
)
|
||||
await mqtt_mock_entry()
|
||||
# Do initial discovery
|
||||
config1 = CONFIG_CLEAN_SEGMENTS_2[mqtt.DOMAIN][vacuum.DOMAIN]
|
||||
payload1 = json.dumps(config1)
|
||||
config_topic = "homeassistant/vacuum/bla/config"
|
||||
async_fire_mqtt_message(hass, config_topic, payload1)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("vacuum.test")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
# The area mapping was already set
|
||||
# so we do not expect a repair flow
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
# Update the segments
|
||||
config2 = config1.copy()
|
||||
config2["segments"] = ["1.Livingroom", "2.Kitchen", "3.Diningroom"]
|
||||
payload2 = json.dumps(config2)
|
||||
async_fire_mqtt_message(hass, config_topic, payload2)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# A repair flow should start
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"]["segments"] == [
|
||||
{"id": "1", "name": "Livingroom", "group": None},
|
||||
{"id": "2", "name": "Kitchen", "group": None},
|
||||
{"id": "3", "name": "Diningroom", "group": None},
|
||||
]
|
||||
|
||||
# Test update with a non-unique segment list fails
|
||||
config3 = config1.copy()
|
||||
config3["segments"] = ["1.Livingroom", "2.Kitchen", "2.Diningroom"]
|
||||
payload3 = json.dumps(config3)
|
||||
async_fire_mqtt_message(hass, config_topic, payload3)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
"Error 'The `segments` option contains an invalid or non-unique segment ID '2'"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
vacuum.DOMAIN: {
|
||||
"name": "test",
|
||||
"unique_id": "veryunique",
|
||||
"segments": ["Livingroom", "Kitchen", "Kitchen"],
|
||||
"clean_segments_command_topic": "vacuum/clean_segment",
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
vacuum.DOMAIN: {
|
||||
"name": "test",
|
||||
"unique_id": "veryunique",
|
||||
"segments": ["Livingroom", "Kitchen", ""],
|
||||
"clean_segments_command_topic": "vacuum/clean_segment",
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
vacuum.DOMAIN: {
|
||||
"name": "test",
|
||||
"unique_id": "veryunique",
|
||||
"segments": ["1.Livingroom", "1.Kitchen"],
|
||||
"clean_segments_command_topic": "vacuum/clean_segment",
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
vacuum.DOMAIN: {
|
||||
"name": "test",
|
||||
"unique_id": "veryunique",
|
||||
"segments": ["1.Livingroom", "1.Kitchen", ".Diningroom"],
|
||||
"clean_segments_command_topic": "vacuum/clean_segment",
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_non_unique_segments(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test with non-unique list of cleanable segments with valid segment IDs."""
|
||||
await mqtt_mock_entry()
|
||||
assert (
|
||||
"The `segments` option contains an invalid or non-unique segment ID"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hass")
|
||||
@pytest.mark.parametrize(
|
||||
("hass_config", "error_message"),
|
||||
[
|
||||
(
|
||||
help_custom_config(
|
||||
vacuum.DOMAIN,
|
||||
DEFAULT_CONFIG,
|
||||
({"clean_segments_command_topic": "test-topic"},),
|
||||
),
|
||||
"Options `segments` and "
|
||||
"`clean_segments_command_topic` must be defined together",
|
||||
),
|
||||
(
|
||||
help_custom_config(
|
||||
vacuum.DOMAIN,
|
||||
DEFAULT_CONFIG,
|
||||
({"segments": ["Livingroom"]},),
|
||||
),
|
||||
"Options `segments` and "
|
||||
"`clean_segments_command_topic` must be defined together",
|
||||
),
|
||||
(
|
||||
help_custom_config(
|
||||
vacuum.DOMAIN,
|
||||
DEFAULT_CONFIG,
|
||||
(
|
||||
{
|
||||
"segments": ["Livingroom"],
|
||||
"clean_segments_command_topic": "test-topic",
|
||||
},
|
||||
),
|
||||
),
|
||||
"Option `segments` requires `unique_id` to be configured",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_clean_segments_config_validation(
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
error_message: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test status clean segment config validation."""
|
||||
await mqtt_mock_entry()
|
||||
assert error_message in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
help_custom_config(
|
||||
vacuum.DOMAIN,
|
||||
CONFIG_CLEAN_SEGMENTS_2,
|
||||
({"clean_segments_command_template": "{{ ';'.join(value) }}"},),
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_clean_segments_command_with_id_and_command_template(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
) -> None:
|
||||
"""Test clean segments with command template."""
|
||||
mqtt_mock = await mqtt_mock_entry()
|
||||
entity_registry.async_update_entity_options(
|
||||
"vacuum.test",
|
||||
vacuum.DOMAIN,
|
||||
{
|
||||
"area_mapping": {"Kitchen": ["1"], "Livingroom": ["2"]},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("vacuum.test")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
await common.async_clean_area(
|
||||
hass, ["Kitchen", "Livingroom"], entity_id="vacuum.test"
|
||||
)
|
||||
assert (
|
||||
call("vacuum/clean_segment", "1;2", 0, False)
|
||||
in mqtt_mock.async_publish.mock_calls
|
||||
)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"]["segments"] == [
|
||||
{"id": "1", "name": "Livingroom", "group": None},
|
||||
{"id": "2", "name": "Kitchen", "group": None},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hass_config", [CONFIG_ALL_SERVICES])
|
||||
async def test_status(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.vacuum import (
|
||||
ATTR_FAN_SPEED,
|
||||
ATTR_PARAMS,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAN_AREA,
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
@@ -82,6 +83,25 @@ async def async_locate(hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL) -
|
||||
await hass.services.async_call(DOMAIN, SERVICE_LOCATE, data, blocking=True)
|
||||
|
||||
|
||||
def clean_area(
|
||||
hass: HomeAssistant, segments: list[str], entity_id: str = ENTITY_MATCH_ALL
|
||||
) -> None:
|
||||
"""Tell all or specified vacuum to perform an area clean."""
|
||||
hass.add_job(async_clean_area, hass, segments, entity_id)
|
||||
|
||||
|
||||
async def async_clean_area(
|
||||
hass: HomeAssistant,
|
||||
segments: list[str],
|
||||
entity_id: str = ENTITY_MATCH_ALL,
|
||||
) -> None:
|
||||
"""Tell all or specified vacuum to perform an area clean."""
|
||||
data = (
|
||||
{ATTR_ENTITY_ID: entity_id, "cleaning_area_id": segments} if entity_id else None
|
||||
)
|
||||
await hass.services.async_call(DOMAIN, SERVICE_CLEAN_AREA, data, blocking=True)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def clean_spot(hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL) -> None:
|
||||
"""Tell all or specified vacuum to perform a spot clean-up."""
|
||||
|
||||
Reference in New Issue
Block a user