Add switch as entity platform on MQTT subentries (#140658)

This commit is contained in:
Jan Bouwhuis 2025-03-26 16:46:44 +01:00 committed by GitHub
parent 57f65c205e
commit febc455bc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 102 additions and 6 deletions

View File

@ -33,6 +33,7 @@ from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorStateClass, SensorStateClass,
) )
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.config_entries import ( from homeassistant.config_entries import (
SOURCE_RECONFIGURE, SOURCE_RECONFIGURE,
ConfigEntry, ConfigEntry,
@ -55,6 +56,7 @@ from homeassistant.const import (
CONF_DISCOVERY, CONF_DISCOVERY,
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
CONF_OPTIMISTIC,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PAYLOAD, CONF_PAYLOAD,
CONF_PLATFORM, CONF_PLATFORM,
@ -233,7 +235,7 @@ KEY_UPLOAD_SELECTOR = FileSelector(
) )
# Subentry selectors # Subentry selectors
SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH]
SUBENTRY_PLATFORM_SELECTOR = SelectSelector( SUBENTRY_PLATFORM_SELECTOR = SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(
options=[platform.value for platform in SUBENTRY_PLATFORMS], options=[platform.value for platform in SUBENTRY_PLATFORMS],
@ -286,6 +288,15 @@ EXPIRE_AFTER_SELECTOR = NumberSelector(
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0) 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 @callback
def validate_sensor_platform_config( def validate_sensor_platform_config(
@ -390,6 +401,9 @@ PLATFORM_ENTITY_FIELDS = {
conditions=({"device_class": "enum"},), conditions=({"device_class": "enum"},),
), ),
}, },
Platform.SWITCH.value: {
CONF_DEVICE_CLASS: PlatformField(SWITCH_DEVICE_CLASS_SELECTOR, False, str),
},
} }
PLATFORM_MQTT_FIELDS = { PLATFORM_MQTT_FIELDS = {
Platform.NOTIFY.value: { Platform.NOTIFY.value: {
@ -419,6 +433,22 @@ PLATFORM_MQTT_FIELDS = {
EXPIRE_AFTER_SELECTOR, False, cv.positive_int, section="advanced_settings" 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[ ENTITY_CONFIG_VALIDATOR: dict[
str, str,
@ -426,6 +456,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
] = { ] = {
Platform.NOTIFY.value: None, Platform.NOTIFY.value: None,
Platform.SENSOR.value: validate_sensor_platform_config, Platform.SENSOR.value: validate_sensor_platform_config,
Platform.SWITCH.value: None,
} }
MQTT_DEVICE_PLATFORM_FIELDS = { MQTT_DEVICE_PLATFORM_FIELDS = {

View File

@ -246,7 +246,9 @@
"value_template": "Value template", "value_template": "Value template",
"last_reset_value_template": "Last reset value template", "last_reset_value_template": "Last reset value template",
"force_update": "Force update", "force_update": "Force update",
"retain": "Retain" "optimistic": "Optimistic",
"retain": "Retain",
"qos": "QoS"
}, },
"data_description": { "data_description": {
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)", "command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
@ -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.", "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)", "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 hasnt changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", "force_update": "Sends update events even if the value hasnt 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": { "sections": {
"advanced_settings": { "advanced_settings": {
@ -462,10 +466,17 @@
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" "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": { "platform": {
"options": { "options": {
"notify": "Notify", "notify": "Notify",
"sensor": "Sensor" "sensor": "Sensor",
"switch": "Switch"
} }
}, },
"set_ca_cert": { "set_ca_cert": {

View File

@ -125,6 +125,19 @@ MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET = {
"entity_picture": "https://example.com/e9261f6feed443e7b7d5f3fbe2a47412", "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 # Bogus light component just for code coverage
# Note that light cannot be setup through the UI yet # Note that light cannot be setup through the UI yet
@ -223,7 +236,17 @@ MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE = {
}, },
"components": MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET, "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 = { MOCK_SUBENTRY_DATA_BAD_COMPONENT_SCHEMA = {
"device": { "device": {
"name": "Milk notifier", "name": "Milk notifier",
@ -246,7 +269,8 @@ MOCK_SUBENTRY_DATA_SET_MIX = {
}, },
"components": MOCK_SUBENTRY_NOTIFY_COMPONENT1 "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1
| MOCK_SUBENTRY_NOTIFY_COMPONENT2 | MOCK_SUBENTRY_NOTIFY_COMPONENT2
| MOCK_SUBENTRY_LIGHT_COMPONENT, | MOCK_SUBENTRY_LIGHT_COMPONENT
| MOCK_SUBENTRY_SWITCH_COMPONENT,
} | MOCK_SUBENTRY_AVAILABILITY_DATA } | MOCK_SUBENTRY_AVAILABILITY_DATA
_SENTINEL = object() _SENTINEL = object()

View File

@ -39,6 +39,7 @@ from .common import (
MOCK_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE, MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE,
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS, MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS,
MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS,
) )
from tests.common import MockConfigEntry, MockMqttReasonCode from tests.common import MockConfigEntry, MockMqttReasonCode
@ -2733,12 +2734,41 @@ async def test_migrate_of_incompatible_config_entry(
(), (),
"Test sensor Energy", "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=[ ids=[
"notify_with_entity_name", "notify_with_entity_name",
"notify_no_entity_name", "notify_no_entity_name",
"sensor_options", "sensor_options",
"sensor_total", "sensor_total",
"switch",
], ],
) )
async def test_subentry_configflow( async def test_subentry_configflow(