Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add switch as entity platform on MQTT subentries #140658

Merged
merged 1 commit into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion homeassistant/components/mqtt/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
ConfigEntry,
Expand All @@ -55,6 +56,7 @@
CONF_DISCOVERY,
CONF_HOST,
CONF_NAME,
CONF_OPTIMISTIC,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_PLATFORM,
Expand Down Expand Up @@ -233,7 +235,7 @@
)

# Subentry selectors
SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH]
SUBENTRY_PLATFORM_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[platform.value for platform in SUBENTRY_PLATFORMS],
Expand Down Expand Up @@ -286,6 +288,15 @@
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0)
)

# Switch specific selectors
SWITCH_DEVICE_CLASS_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[device_class.value for device_class in SwitchDeviceClass],
mode=SelectSelectorMode.DROPDOWN,
translation_key="device_class_switch",
)
)


@callback
def validate_sensor_platform_config(
Expand Down Expand Up @@ -390,6 +401,9 @@ def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
conditions=({"device_class": "enum"},),
),
},
Platform.SWITCH.value: {
CONF_DEVICE_CLASS: PlatformField(SWITCH_DEVICE_CLASS_SELECTOR, False, str),
},
}
PLATFORM_MQTT_FIELDS = {
Platform.NOTIFY.value: {
Expand Down Expand Up @@ -419,13 +433,30 @@ def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
EXPIRE_AFTER_SELECTOR, False, cv.positive_int, section="advanced_settings"
),
},
Platform.SWITCH.value: {
CONF_COMMAND_TOPIC: PlatformField(
TEXT_SELECTOR, True, valid_publish_topic, "invalid_publish_topic"
),
CONF_COMMAND_TEMPLATE: PlatformField(
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
),
CONF_STATE_TOPIC: PlatformField(
TEXT_SELECTOR, False, valid_subscribe_topic, "invalid_subscribe_topic"
),
CONF_VALUE_TEMPLATE: PlatformField(
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
),
CONF_RETAIN: PlatformField(BOOLEAN_SELECTOR, False, bool),
CONF_OPTIMISTIC: PlatformField(BOOLEAN_SELECTOR, False, bool),
},
}
ENTITY_CONFIG_VALIDATOR: dict[
str,
Callable[[dict[str, Any]], dict[str, str]] | None,
] = {
Platform.NOTIFY.value: None,
Platform.SENSOR.value: validate_sensor_platform_config,
Platform.SWITCH.value: None,
}

MQTT_DEVICE_PLATFORM_FIELDS = {
Expand Down
17 changes: 14 additions & 3 deletions homeassistant/components/mqtt/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@
"value_template": "Value template",
"last_reset_value_template": "Last reset value template",
"force_update": "Force update",
"retain": "Retain"
"optimistic": "Optimistic",
"retain": "Retain",
"qos": "QoS"
},
"data_description": {
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
Expand All @@ -255,7 +257,9 @@
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value.",
"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)",
"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)",
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker."
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
"qos": "The QoS value {platform} entity should use."
},
"sections": {
"advanced_settings": {
Expand Down Expand Up @@ -462,10 +466,17 @@
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
}
},
"device_class_switch": {
"options": {
"outlet": "[%key:component::switch::entity_component::outlet::name%]",
"switch": "[%key:component::switch::title%]"
}
},
"platform": {
"options": {
"notify": "Notify",
"sensor": "Sensor"
"sensor": "Sensor",
"switch": "Switch"
}
},
"set_ca_cert": {
Expand Down
28 changes: 26 additions & 2 deletions tests/components/mqtt/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@
"entity_picture": "https://example.com/e9261f6feed443e7b7d5f3fbe2a47412",
},
}
MOCK_SUBENTRY_SWITCH_COMPONENT = {
"3faf1318016c46c5aea26707eeb6f12e": {
"platform": "switch",
"name": "Outlet",
"device_class": "outlet",
"command_topic": "test-topic",
"state_topic": "test-topic",
"command_template": "{{ value }}",
"value_template": "{{ value_json.value }}",
"entity_picture": "https://example.com/3faf1318016c46c5aea26707eeb6f12e",
"optimistic": True,
},
}

# Bogus light component just for code coverage
# Note that light cannot be setup through the UI yet
Expand Down Expand Up @@ -223,7 +236,17 @@
},
"components": MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET,
}

MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS = {
"device": {
"name": "Test switch",
"sw_version": "1.0",
"hw_version": "2.1 rev a",
"model": "Model XL",
"model_id": "mn002",
"configuration_url": "https://example.com",
},
"components": MOCK_SUBENTRY_SWITCH_COMPONENT,
}
MOCK_SUBENTRY_DATA_BAD_COMPONENT_SCHEMA = {
"device": {
"name": "Milk notifier",
Expand All @@ -246,7 +269,8 @@
},
"components": MOCK_SUBENTRY_NOTIFY_COMPONENT1
| MOCK_SUBENTRY_NOTIFY_COMPONENT2
| MOCK_SUBENTRY_LIGHT_COMPONENT,
| MOCK_SUBENTRY_LIGHT_COMPONENT
| MOCK_SUBENTRY_SWITCH_COMPONENT,
} | MOCK_SUBENTRY_AVAILABILITY_DATA
_SENTINEL = object()

Expand Down
30 changes: 30 additions & 0 deletions tests/components/mqtt/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE,
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS,
MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS,
)

from tests.common import MockConfigEntry, MockMqttReasonCode
Expand Down Expand Up @@ -2733,12 +2734,41 @@ async def test_migrate_of_incompatible_config_entry(
(),
"Test sensor Energy",
),
(
MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS,
{"name": "Test switch", "mqtt_settings": {"qos": 0}},
{"name": "Outlet"},
{"device_class": "outlet"},
(),
{
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"optimistic": True,
},
(
(
{"command_topic": "test-topic#invalid"},
{"command_topic": "invalid_publish_topic"},
),
(
{
"command_topic": "test-topic",
"state_topic": "test-topic#invalid",
},
{"state_topic": "invalid_subscribe_topic"},
),
),
"Test switch Outlet",
),
],
ids=[
"notify_with_entity_name",
"notify_no_entity_name",
"sensor_options",
"sensor_total",
"switch",
],
)
async def test_subentry_configflow(
Expand Down