Skip to content

Commit 5747703

Browse files
committed
Add switch as entity platform on MQTT subentries
1 parent 0b32f67 commit 5747703

File tree

4 files changed

+107
-13
lines changed

4 files changed

+107
-13
lines changed

homeassistant/components/mqtt/config_flow.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
SensorDeviceClass,
3434
SensorStateClass,
3535
)
36+
from homeassistant.components.switch import SwitchDeviceClass
3637
from homeassistant.config_entries import (
3738
SOURCE_RECONFIGURE,
3839
ConfigEntry,
@@ -55,6 +56,7 @@
5556
CONF_DISCOVERY,
5657
CONF_HOST,
5758
CONF_NAME,
59+
CONF_OPTIMISTIC,
5860
CONF_PASSWORD,
5961
CONF_PAYLOAD,
6062
CONF_PLATFORM,
@@ -233,7 +235,7 @@
233235
)
234236

235237
# Subentry selectors
236-
SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
238+
SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH]
237239
SUBENTRY_PLATFORM_SELECTOR = SelectSelector(
238240
SelectSelectorConfig(
239241
options=[platform.value for platform in SUBENTRY_PLATFORMS],
@@ -286,6 +288,15 @@
286288
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0)
287289
)
288290

291+
# Switch specific selectors
292+
SWITCH_DEVICE_CLASS_SELECTOR = SelectSelector(
293+
SelectSelectorConfig(
294+
options=[device_class.value for device_class in SwitchDeviceClass],
295+
mode=SelectSelectorMode.DROPDOWN,
296+
translation_key="device_class_switch",
297+
)
298+
)
299+
289300

290301
@dataclass(frozen=True)
291302
class PlatformField:
@@ -352,6 +363,9 @@ def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
352363
conditions=({"device_class": "enum"},),
353364
),
354365
},
366+
Platform.SWITCH.value: {
367+
CONF_DEVICE_CLASS: PlatformField(SWITCH_DEVICE_CLASS_SELECTOR, False, str),
368+
},
355369
}
356370
PLATFORM_MQTT_FIELDS = {
357371
Platform.NOTIFY.value: {
@@ -381,13 +395,30 @@ def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
381395
EXIRE_AFTER_SELECTOR, False, cv.positive_int, section="advanced_settings"
382396
),
383397
},
398+
Platform.SWITCH.value: {
399+
CONF_COMMAND_TOPIC: PlatformField(
400+
TEXT_SELECTOR, True, valid_publish_topic, "invalid_publish_topic"
401+
),
402+
CONF_COMMAND_TEMPLATE: PlatformField(
403+
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
404+
),
405+
CONF_STATE_TOPIC: PlatformField(
406+
TEXT_SELECTOR, False, valid_subscribe_topic, "invalid_subscribe_topic"
407+
),
408+
CONF_VALUE_TEMPLATE: PlatformField(
409+
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
410+
),
411+
CONF_RETAIN: PlatformField(BOOLEAN_SELECTOR, False, bool),
412+
CONF_OPTIMISTIC: PlatformField(BOOLEAN_SELECTOR, False, bool),
413+
},
384414
}
385415
ENTITY_CONFIG_VALIDATOR: dict[
386416
str,
387417
Callable[[dict[str, Any], dict[str, str], list[str] | None], dict[str, Any]] | None,
388418
] = {
389419
Platform.NOTIFY.value: None,
390420
Platform.SENSOR.value: validate_sensor_state_and_device_class_config,
421+
Platform.SWITCH.value: None,
391422
}
392423

393424
MQTT_DEVICE_SCHEMA = vol.Schema(

homeassistant/components/mqtt/strings.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
"value_template": "Value template",
236236
"last_reset_value_template": "Last reset value template",
237237
"force_update": "Force update",
238+
"optimistic": "Optimistic",
238239
"retain": "Retain",
239240
"qos": "QoS"
240241
},
@@ -245,6 +246,7 @@
245246
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value.",
246247
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
247248
"force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
249+
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
248250
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
249251
"qos": "The QoS value {platform} entity should use."
250252
},
@@ -452,10 +454,17 @@
452454
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
453455
}
454456
},
457+
"device_class_switch": {
458+
"options": {
459+
"outlet": "[%key:component::switch::entity_component::outlet::name%]",
460+
"switch": "[%key:component::switch::title%]"
461+
}
462+
},
455463
"platform": {
456464
"options": {
457465
"notify": "Notify",
458-
"sensor": "Sensor"
466+
"sensor": "Sensor",
467+
"switch": "Switch"
459468
}
460469
},
461470
"set_ca_cert": {

tests/components/mqtt/common.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,20 @@
131131
"qos": 0,
132132
},
133133
}
134+
MOCK_SUBENTRY_SWITCH_COMPONENT = {
135+
"3faf1318016c46c5aea26707eeb6f12e": {
136+
"platform": "switch",
137+
"name": "Outlet",
138+
"device_class": "outlet",
139+
"qos": 0,
140+
"command_topic": "test-topic",
141+
"state_topic": "test-topic",
142+
"command_template": "{{ value }}",
143+
"value_template": "{{ value_json.value }}",
144+
"entity_picture": "https://example.com/3faf1318016c46c5aea26707eeb6f12e",
145+
"optimistic": True,
146+
},
147+
}
134148

135149
# Bogus light component just for code coverage
136150
# Note that light cannot be setup through the UI yet
@@ -230,7 +244,17 @@
230244
},
231245
"components": MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET,
232246
}
233-
247+
MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS = {
248+
"device": {
249+
"name": "Test switch",
250+
"sw_version": "1.0",
251+
"hw_version": "2.1 rev a",
252+
"model": "Model XL",
253+
"model_id": "mn002",
254+
"configuration_url": "https://example.com",
255+
},
256+
"components": MOCK_SUBENTRY_SWITCH_COMPONENT,
257+
}
234258
MOCK_SUBENTRY_DATA_BAD_COMPONENT_SCHEMA = {
235259
"device": {
236260
"name": "Milk notifier",
@@ -253,7 +277,8 @@
253277
},
254278
"components": MOCK_SUBENTRY_NOTIFY_COMPONENT1
255279
| MOCK_SUBENTRY_NOTIFY_COMPONENT2
256-
| MOCK_SUBENTRY_LIGHT_COMPONENT,
280+
| MOCK_SUBENTRY_LIGHT_COMPONENT
281+
| MOCK_SUBENTRY_SWITCH_COMPONENT,
257282
} | MOCK_SUBENTRY_AVAILABILITY_DATA
258283
_SENTINEL = object()
259284

tests/components/mqtt/test_config_flow.py

+38-9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
4040
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE,
4141
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS,
42+
MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS,
4243
)
4344

4445
from tests.common import MockConfigEntry, MockMqttReasonCode
@@ -2616,6 +2617,7 @@ async def test_migrate_of_incompatible_config_entry(
26162617
@pytest.mark.parametrize(
26172618
(
26182619
"config_subentries_data",
2620+
"mock_device_user_input",
26192621
"mock_entity_user_input",
26202622
"mock_entity_details_user_input",
26212623
"mock_entity_details_failed_user_input",
@@ -2626,6 +2628,7 @@ async def test_migrate_of_incompatible_config_entry(
26262628
[
26272629
(
26282630
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
2631+
{"name": "Milk notifier"},
26292632
{"name": "Milkman alert"},
26302633
None,
26312634
None,
@@ -2645,6 +2648,7 @@ async def test_migrate_of_incompatible_config_entry(
26452648
),
26462649
(
26472650
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME,
2651+
{"name": "Milk notifier"},
26482652
{},
26492653
None,
26502654
None,
@@ -2664,6 +2668,7 @@ async def test_migrate_of_incompatible_config_entry(
26642668
),
26652669
(
26662670
MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
2671+
{"name": "Test sensor"},
26672672
{"name": "Energy"},
26682673
{"device_class": "enum", "options": ["low", "medium", "high"]},
26692674
(
@@ -2720,6 +2725,7 @@ async def test_migrate_of_incompatible_config_entry(
27202725
),
27212726
(
27222727
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS,
2728+
{"name": "Test sensor"},
27232729
{"name": "Energy"},
27242730
{
27252731
"state_class": "measurement",
@@ -2731,18 +2737,48 @@ async def test_migrate_of_incompatible_config_entry(
27312737
(),
27322738
"Test sensor Energy",
27332739
),
2740+
(
2741+
MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS,
2742+
{"name": "Test switch"},
2743+
{"name": "Outlet"},
2744+
{"device_class": "outlet"},
2745+
(),
2746+
{
2747+
"command_topic": "test-topic",
2748+
"command_template": "{{ value }}",
2749+
"state_topic": "test-topic",
2750+
"value_template": "{{ value_json.value }}",
2751+
"optimistic": True,
2752+
},
2753+
(
2754+
(
2755+
{"command_topic": "test-topic#invalid"},
2756+
{"command_topic": "invalid_publish_topic"},
2757+
),
2758+
(
2759+
{
2760+
"command_topic": "test-topic",
2761+
"state_topic": "test-topic#invalid",
2762+
},
2763+
{"state_topic": "invalid_subscribe_topic"},
2764+
),
2765+
),
2766+
"Test switch Outlet",
2767+
),
27342768
],
27352769
ids=[
27362770
"notify_with_entity_name",
27372771
"notify_no_entity_name",
27382772
"sensor_options",
27392773
"sensor_total",
2774+
"switch",
27402775
],
27412776
)
27422777
async def test_subentry_configflow(
27432778
hass: HomeAssistant,
27442779
mqtt_mock_entry: MqttMockHAClientGenerator,
27452780
config_subentries_data: dict[str, Any],
2781+
mock_device_user_input: dict[str, Any],
27462782
mock_entity_user_input: dict[str, Any],
27472783
mock_entity_details_user_input: dict[str, Any],
27482784
mock_entity_details_failed_user_input: tuple[
@@ -2753,7 +2789,7 @@ async def test_subentry_configflow(
27532789
entity_name: str,
27542790
) -> None:
27552791
"""Test the subentry ConfigFlow."""
2756-
device_name = config_subentries_data["device"]["name"]
2792+
device_name = mock_device_user_input["name"]
27572793
component = next(iter(config_subentries_data["components"].values()))
27582794

27592795
await mqtt_mock_entry()
@@ -2780,14 +2816,7 @@ async def test_subentry_configflow(
27802816

27812817
result = await hass.config_entries.subentries.async_configure(
27822818
result["flow_id"],
2783-
user_input={
2784-
"name": device_name,
2785-
"sw_version": "1.0",
2786-
"hw_version": "2.1 rev a",
2787-
"model": "Model XL",
2788-
"model_id": "mn002",
2789-
"configuration_url": "https://example.com",
2790-
},
2819+
user_input=mock_device_user_input,
27912820
)
27922821
assert result["type"] is FlowResultType.FORM
27932822
assert result["step_id"] == "entity"

0 commit comments

Comments
 (0)