Add MQTT fan as entity platform on MQTT subentries (#144698)

This commit is contained in:
Jan Bouwhuis 2025-05-23 17:40:54 +02:00 committed by GitHub
parent e22ea85e84
commit 2a38f03ec9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 676 additions and 41 deletions

View File

@ -143,6 +143,10 @@ from .const import (
CONF_COMMAND_ON_TEMPLATE,
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_DIRECTION_COMMAND_TEMPLATE,
CONF_DIRECTION_COMMAND_TOPIC,
CONF_DIRECTION_STATE_TOPIC,
CONF_DIRECTION_VALUE_TEMPLATE,
CONF_DISCOVERY_PREFIX,
CONF_EFFECT_COMMAND_TEMPLATE,
CONF_EFFECT_COMMAND_TOPIC,
@ -169,15 +173,32 @@ from .const import (
CONF_OFF_DELAY,
CONF_ON_COMMAND_TYPE,
CONF_OPTIONS,
CONF_OSCILLATION_COMMAND_TEMPLATE,
CONF_OSCILLATION_COMMAND_TOPIC,
CONF_OSCILLATION_STATE_TOPIC,
CONF_OSCILLATION_VALUE_TEMPLATE,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_CLOSE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_PAYLOAD_OPEN,
CONF_PAYLOAD_OSCILLATION_OFF,
CONF_PAYLOAD_OSCILLATION_ON,
CONF_PAYLOAD_PRESS,
CONF_PAYLOAD_RESET_PERCENTAGE,
CONF_PAYLOAD_RESET_PRESET_MODE,
CONF_PAYLOAD_STOP,
CONF_PAYLOAD_STOP_TILT,
CONF_PERCENTAGE_COMMAND_TEMPLATE,
CONF_PERCENTAGE_COMMAND_TOPIC,
CONF_PERCENTAGE_STATE_TOPIC,
CONF_PERCENTAGE_VALUE_TEMPLATE,
CONF_POSITION_CLOSED,
CONF_POSITION_OPEN,
CONF_PRESET_MODE_COMMAND_TEMPLATE,
CONF_PRESET_MODE_COMMAND_TOPIC,
CONF_PRESET_MODE_STATE_TOPIC,
CONF_PRESET_MODE_VALUE_TEMPLATE,
CONF_PRESET_MODES_LIST,
CONF_QOS,
CONF_RED_TEMPLATE,
CONF_RETAIN,
@ -196,6 +217,8 @@ from .const import (
CONF_SCHEMA,
CONF_SET_POSITION_TEMPLATE,
CONF_SET_POSITION_TOPIC,
CONF_SPEED_RANGE_MAX,
CONF_SPEED_RANGE_MIN,
CONF_STATE_CLOSED,
CONF_STATE_CLOSING,
CONF_STATE_OPEN,
@ -239,7 +262,10 @@ from .const import (
DEFAULT_PAYLOAD_OFF,
DEFAULT_PAYLOAD_ON,
DEFAULT_PAYLOAD_OPEN,
DEFAULT_PAYLOAD_OSCILLATE_OFF,
DEFAULT_PAYLOAD_OSCILLATE_ON,
DEFAULT_PAYLOAD_PRESS,
DEFAULT_PAYLOAD_RESET,
DEFAULT_PAYLOAD_STOP,
DEFAULT_PORT,
DEFAULT_POSITION_CLOSED,
@ -247,6 +273,8 @@ from .const import (
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_QOS,
DEFAULT_SPEED_RANGE_MAX,
DEFAULT_SPEED_RANGE_MIN,
DEFAULT_STATE_STOPPED,
DEFAULT_TILT_CLOSED_POSITION,
DEFAULT_TILT_MAX,
@ -353,6 +381,7 @@ SUBENTRY_PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.NOTIFY,
Platform.SENSOR,
@ -437,6 +466,17 @@ TIMEOUT_SELECTOR = NumberSelector(
# Cover specific selectors
POSITION_SELECTOR = NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX))
# Fan specific selectors
FAN_SPEED_RANGE_MIN_SELECTOR = vol.All(
NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1)),
vol.Coerce(int),
)
FAN_SPEED_RANGE_MAX_SELECTOR = vol.All(
NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=2)),
vol.Coerce(int),
)
PRESET_MODES_SELECTOR = OPTIONS_SELECTOR
# Switch specific selectors
SWITCH_DEVICE_CLASS_SELECTOR = SelectSelector(
SelectSelectorConfig(
@ -537,6 +577,29 @@ def validate_cover_platform_config(
return errors
@callback
def validate_fan_platform_config(config: dict[str, Any]) -> dict[str, str]:
"""Validate the fan config options."""
errors: dict[str, str] = {}
if (
CONF_SPEED_RANGE_MIN in config
and CONF_SPEED_RANGE_MAX in config
and config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]
):
errors["fan_speed_settings"] = (
"fan_speed_range_max_must_be_greater_than_speed_range_min"
)
if (
CONF_PRESET_MODES_LIST in config
and config.get(CONF_PAYLOAD_RESET_PRESET_MODE) in config[CONF_PRESET_MODES_LIST]
):
errors["fan_preset_mode_settings"] = (
"fan_preset_mode_reset_in_preset_modes_list"
)
return errors
@callback
def validate_sensor_platform_config(
config: dict[str, Any],
@ -597,9 +660,12 @@ class PlatformField:
required: bool
validator: Callable[..., Any] | None = None
error: str | None = None
default: str | int | bool | None | vol.Undefined = vol.UNDEFINED
default: (
str | int | bool | None | Callable[[dict[str, Any]], Any] | vol.Undefined
) = vol.UNDEFINED
is_schema_default: bool = False
exclude_from_reconfig: bool = False
exclude_from_config: bool = False
conditions: tuple[dict[str, Any], ...] | None = None
custom_filtering: bool = False
section: str | None = None
@ -634,7 +700,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
return errors
COMMON_ENTITY_FIELDS = {
COMMON_ENTITY_FIELDS: dict[str, PlatformField] = {
CONF_PLATFORM: PlatformField(
selector=SUBENTRY_PLATFORM_SELECTOR,
required=True,
@ -651,7 +717,7 @@ COMMON_ENTITY_FIELDS = {
),
}
PLATFORM_ENTITY_FIELDS = {
PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
Platform.BINARY_SENSOR.value: {
CONF_DEVICE_CLASS: PlatformField(
selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR,
@ -670,6 +736,32 @@ PLATFORM_ENTITY_FIELDS = {
required=False,
),
},
Platform.FAN.value: {
"fan_feature_speed": PlatformField(
selector=BOOLEAN_SELECTOR,
required=False,
exclude_from_config=True,
default=lambda config: bool(config.get(CONF_PERCENTAGE_COMMAND_TOPIC)),
),
"fan_feature_preset_modes": PlatformField(
selector=BOOLEAN_SELECTOR,
required=False,
exclude_from_config=True,
default=lambda config: bool(config.get(CONF_PRESET_MODE_COMMAND_TOPIC)),
),
"fan_feature_oscillation": PlatformField(
selector=BOOLEAN_SELECTOR,
required=False,
exclude_from_config=True,
default=lambda config: bool(config.get(CONF_OSCILLATION_COMMAND_TOPIC)),
),
"fan_feature_direction": PlatformField(
selector=BOOLEAN_SELECTOR,
required=False,
exclude_from_config=True,
default=lambda config: bool(config.get(CONF_DIRECTION_COMMAND_TOPIC)),
),
},
Platform.NOTIFY.value: {},
Platform.SENSOR.value: {
CONF_DEVICE_CLASS: PlatformField(
@ -715,7 +807,7 @@ PLATFORM_ENTITY_FIELDS = {
),
},
}
PLATFORM_MQTT_FIELDS = {
PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = {
Platform.BINARY_SENSOR.value: {
CONF_STATE_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
@ -951,6 +1043,226 @@ PLATFORM_MQTT_FIELDS = {
section="cover_tilt_settings",
),
},
Platform.FAN.value: {
CONF_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=True,
validator=valid_publish_topic,
error="invalid_publish_topic",
),
CONF_COMMAND_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
),
CONF_STATE_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=False,
validator=valid_subscribe_topic,
error="invalid_subscribe_topic",
),
CONF_VALUE_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
),
CONF_PAYLOAD_OFF: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_OFF,
),
CONF_PAYLOAD_ON: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_ON,
),
CONF_RETAIN: PlatformField(
selector=BOOLEAN_SELECTOR, required=False, validator=bool
),
CONF_OPTIMISTIC: PlatformField(
selector=BOOLEAN_SELECTOR, required=False, validator=bool
),
CONF_PERCENTAGE_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=True,
validator=valid_publish_topic,
error="invalid_publish_topic",
section="fan_speed_settings",
conditions=({"fan_feature_speed": True},),
),
CONF_PERCENTAGE_COMMAND_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
section="fan_speed_settings",
conditions=({"fan_feature_speed": True},),
),
CONF_PERCENTAGE_STATE_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=False,
validator=valid_subscribe_topic,
error="invalid_subscribe_topic",
section="fan_speed_settings",
conditions=({"fan_feature_speed": True},),
),
CONF_PERCENTAGE_VALUE_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
section="fan_speed_settings",
conditions=({"fan_feature_speed": True},),
),
CONF_SPEED_RANGE_MIN: PlatformField(
selector=FAN_SPEED_RANGE_MIN_SELECTOR,
required=False,
validator=int,
default=DEFAULT_SPEED_RANGE_MIN,
section="fan_speed_settings",
conditions=({"fan_feature_speed": True},),
),
CONF_SPEED_RANGE_MAX: PlatformField(
selector=FAN_SPEED_RANGE_MAX_SELECTOR,
required=False,
validator=int,
default=DEFAULT_SPEED_RANGE_MAX,
section="fan_speed_settings",
conditions=({"fan_feature_speed": True},),
),
CONF_PAYLOAD_RESET_PERCENTAGE: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_RESET,
section="fan_speed_settings",
conditions=({"fan_feature_speed": True},),
),
CONF_PRESET_MODES_LIST: PlatformField(
selector=PRESET_MODES_SELECTOR,
required=True,
section="fan_preset_mode_settings",
conditions=({"fan_feature_preset_modes": True},),
),
CONF_PRESET_MODE_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=True,
validator=valid_publish_topic,
error="invalid_publish_topic",
section="fan_preset_mode_settings",
conditions=({"fan_feature_preset_modes": True},),
),
CONF_PRESET_MODE_COMMAND_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
section="fan_preset_mode_settings",
conditions=({"fan_feature_preset_modes": True},),
),
CONF_PRESET_MODE_STATE_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=False,
validator=valid_subscribe_topic,
error="invalid_subscribe_topic",
section="fan_preset_mode_settings",
conditions=({"fan_feature_preset_modes": True},),
),
CONF_PRESET_MODE_VALUE_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
section="fan_preset_mode_settings",
conditions=({"fan_feature_preset_modes": True},),
),
CONF_PAYLOAD_RESET_PRESET_MODE: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_RESET,
section="fan_preset_mode_settings",
conditions=({"fan_feature_preset_modes": True},),
),
CONF_OSCILLATION_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=True,
validator=valid_publish_topic,
error="invalid_publish_topic",
section="fan_oscillation_settings",
conditions=({"fan_feature_oscillation": True},),
),
CONF_OSCILLATION_COMMAND_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
section="fan_oscillation_settings",
conditions=({"fan_feature_oscillation": True},),
),
CONF_OSCILLATION_STATE_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=False,
validator=valid_subscribe_topic,
error="invalid_subscribe_topic",
section="fan_oscillation_settings",
conditions=({"fan_feature_oscillation": True},),
),
CONF_OSCILLATION_VALUE_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
section="fan_oscillation_settings",
conditions=({"fan_feature_oscillation": True},),
),
CONF_PAYLOAD_OSCILLATION_OFF: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_OSCILLATE_OFF,
section="fan_oscillation_settings",
conditions=({"fan_feature_oscillation": True},),
),
CONF_PAYLOAD_OSCILLATION_ON: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_OSCILLATE_ON,
section="fan_oscillation_settings",
conditions=({"fan_feature_oscillation": True},),
),
CONF_DIRECTION_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=True,
validator=valid_publish_topic,
error="invalid_publish_topic",
section="fan_direction_settings",
conditions=({"fan_feature_direction": True},),
),
CONF_DIRECTION_COMMAND_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
section="fan_direction_settings",
conditions=({"fan_feature_direction": True},),
),
CONF_DIRECTION_STATE_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=False,
validator=valid_subscribe_topic,
error="invalid_subscribe_topic",
section="fan_direction_settings",
conditions=({"fan_feature_direction": True},),
),
CONF_DIRECTION_VALUE_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
section="fan_direction_settings",
conditions=({"fan_feature_direction": True},),
),
},
Platform.NOTIFY.value: {
CONF_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
@ -1510,6 +1822,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
Platform.BINARY_SENSOR.value: None,
Platform.BUTTON.value: None,
Platform.COVER.value: validate_cover_platform_config,
Platform.FAN.value: validate_fan_platform_config,
Platform.LIGHT.value: validate_light_platform_config,
Platform.NOTIFY.value: None,
Platform.SENSOR.value: validate_sensor_platform_config,
@ -1667,6 +1980,14 @@ def data_schema_from_fields(
device_data: MqttDeviceData | None = None,
) -> vol.Schema:
"""Generate custom data schema from platform fields or device data."""
def get_default(field_details: PlatformField) -> Any:
if callable(field_details.default):
if TYPE_CHECKING:
assert component_data is not None
return field_details.default(component_data)
return field_details.default
if device_data is not None:
component_data_with_user_input: dict[str, Any] | None = dict(device_data)
if TYPE_CHECKING:
@ -1693,7 +2014,7 @@ def data_schema_from_fields(
if field_details.required
else vol.Optional(
field_name,
default=field_details.default
default=get_default(field_details)
if field_details.default is not None
else vol.UNDEFINED,
): field_details.selector(component_data_with_user_input) # type: ignore[operator]
@ -2581,13 +2902,21 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
"""Update component data defaults."""
for component_data in self._subentry_data["components"].values():
platform = component_data[CONF_PLATFORM]
subentry_default_data = subentry_schema_default_data_from_fields(
platform_fields: dict[str, PlatformField] = (
COMMON_ENTITY_FIELDS
| PLATFORM_ENTITY_FIELDS[platform]
| PLATFORM_MQTT_FIELDS[platform],
| PLATFORM_MQTT_FIELDS[platform]
)
subentry_default_data = subentry_schema_default_data_from_fields(
platform_fields,
component_data,
)
component_data.update(subentry_default_data)
for key, platform_field in platform_fields.items():
if not platform_field.exclude_from_config:
continue
if key in component_data:
component_data.pop(key)
@callback
def _async_create_subentry(

View File

@ -78,6 +78,10 @@ CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template"
CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"
CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template"
CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic"
CONF_DIRECTION_COMMAND_TEMPLATE = "direction_command_template"
CONF_DIRECTION_COMMAND_TOPIC = "direction_command_topic"
CONF_DIRECTION_STATE_TOPIC = "direction_state_topic"
CONF_DIRECTION_VALUE_TEMPLATE = "direction_value_template"
CONF_ENABLED_BY_DEFAULT = "enabled_by_default"
CONF_EFFECT_COMMAND_TEMPLATE = "effect_command_template"
CONF_EFFECT_COMMAND_TOPIC = "effect_command_topic"
@ -109,16 +113,33 @@ CONF_MODE_STATE_TEMPLATE = "mode_state_template"
CONF_MODE_STATE_TOPIC = "mode_state_topic"
CONF_OFF_DELAY = "off_delay"
CONF_ON_COMMAND_TYPE = "on_command_type"
CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"
CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template"
CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"
CONF_PAYLOAD_CLOSE = "payload_close"
CONF_PAYLOAD_OPEN = "payload_open"
CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"
CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"
CONF_PAYLOAD_PRESS = "payload_press"
CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage"
CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode"
CONF_PAYLOAD_STOP = "payload_stop"
CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt"
CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"
CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"
CONF_POSITION_CLOSED = "position_closed"
CONF_POSITION_OPEN = "position_open"
CONF_POWER_COMMAND_TOPIC = "power_command_topic"
CONF_POWER_COMMAND_TEMPLATE = "power_command_template"
CONF_PRECISION = "precision"
CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"
CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"
CONF_PRESET_MODES_LIST = "preset_modes"
CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"
CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"
CONF_RED_TEMPLATE = "red_template"
CONF_RGB_COMMAND_TEMPLATE = "rgb_command_template"
CONF_RGB_COMMAND_TOPIC = "rgb_command_topic"
@ -134,6 +155,8 @@ CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic"
CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template"
CONF_SET_POSITION_TEMPLATE = "set_position_template"
CONF_SET_POSITION_TOPIC = "set_position_topic"
CONF_SPEED_RANGE_MAX = "speed_range_max"
CONF_SPEED_RANGE_MIN = "speed_range_min"
CONF_STATE_CLOSED = "state_closed"
CONF_STATE_CLOSING = "state_closing"
CONF_STATE_OPEN = "state_open"
@ -204,8 +227,11 @@ DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline"
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_PAYLOAD_OPEN = "OPEN"
DEFAULT_PAYLOAD_OSCILLATE_OFF = "oscillate_off"
DEFAULT_PAYLOAD_OSCILLATE_ON = "oscillate_on"
DEFAULT_PAYLOAD_PRESS = "PRESS"
DEFAULT_PAYLOAD_STOP = "STOP"
DEFAULT_PAYLOAD_RESET = "None"
DEFAULT_PORT = 1883
DEFAULT_RETAIN = False
DEFAULT_TILT_CLOSED_POSITION = 0
@ -218,6 +244,8 @@ DEFAULT_WS_PATH = "/"
DEFAULT_POSITION_CLOSED = 0
DEFAULT_POSITION_OPEN = 100
DEFAULT_RETAIN = False
DEFAULT_SPEED_RANGE_MAX = 100
DEFAULT_SPEED_RANGE_MIN = 1
DEFAULT_STATE_STOPPED = "stopped"
DEFAULT_WHITE_SCALE = 255

View File

@ -43,8 +43,38 @@ from .config import MQTT_RW_SCHEMA
from .const import (
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_DIRECTION_COMMAND_TEMPLATE,
CONF_DIRECTION_COMMAND_TOPIC,
CONF_DIRECTION_STATE_TOPIC,
CONF_DIRECTION_VALUE_TEMPLATE,
CONF_OSCILLATION_COMMAND_TEMPLATE,
CONF_OSCILLATION_COMMAND_TOPIC,
CONF_OSCILLATION_STATE_TOPIC,
CONF_OSCILLATION_VALUE_TEMPLATE,
CONF_PAYLOAD_OSCILLATION_OFF,
CONF_PAYLOAD_OSCILLATION_ON,
CONF_PAYLOAD_RESET_PERCENTAGE,
CONF_PAYLOAD_RESET_PRESET_MODE,
CONF_PERCENTAGE_COMMAND_TEMPLATE,
CONF_PERCENTAGE_COMMAND_TOPIC,
CONF_PERCENTAGE_STATE_TOPIC,
CONF_PERCENTAGE_VALUE_TEMPLATE,
CONF_PRESET_MODE_COMMAND_TEMPLATE,
CONF_PRESET_MODE_COMMAND_TOPIC,
CONF_PRESET_MODE_STATE_TOPIC,
CONF_PRESET_MODE_VALUE_TEMPLATE,
CONF_PRESET_MODES_LIST,
CONF_SPEED_RANGE_MAX,
CONF_SPEED_RANGE_MIN,
CONF_STATE_TOPIC,
CONF_STATE_VALUE_TEMPLATE,
DEFAULT_PAYLOAD_OFF,
DEFAULT_PAYLOAD_ON,
DEFAULT_PAYLOAD_OSCILLATE_OFF,
DEFAULT_PAYLOAD_OSCILLATE_ON,
DEFAULT_PAYLOAD_RESET,
DEFAULT_SPEED_RANGE_MAX,
DEFAULT_SPEED_RANGE_MIN,
PAYLOAD_NONE,
)
from .entity import MqttEntity, async_setup_entity_entry_helper
@ -59,39 +89,7 @@ from .util import valid_publish_topic, valid_subscribe_topic
PARALLEL_UPDATES = 0
CONF_DIRECTION_STATE_TOPIC = "direction_state_topic"
CONF_DIRECTION_COMMAND_TOPIC = "direction_command_topic"
CONF_DIRECTION_VALUE_TEMPLATE = "direction_value_template"
CONF_DIRECTION_COMMAND_TEMPLATE = "direction_command_template"
CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"
CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"
CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage"
CONF_SPEED_RANGE_MIN = "speed_range_min"
CONF_SPEED_RANGE_MAX = "speed_range_max"
CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"
CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"
CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"
CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"
CONF_PRESET_MODES_LIST = "preset_modes"
CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode"
CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"
CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"
CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template"
CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"
CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"
DEFAULT_NAME = "MQTT Fan"
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_PAYLOAD_RESET = "None"
DEFAULT_SPEED_RANGE_MIN = 1
DEFAULT_SPEED_RANGE_MAX = 100
OSCILLATE_ON_PAYLOAD = "oscillate_on"
OSCILLATE_OFF_PAYLOAD = "oscillate_off"
MQTT_FAN_ATTRIBUTES_BLOCKED = frozenset(
{
@ -165,10 +163,10 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(
CONF_PAYLOAD_OSCILLATION_OFF, default=OSCILLATE_OFF_PAYLOAD
CONF_PAYLOAD_OSCILLATION_OFF, default=DEFAULT_PAYLOAD_OSCILLATE_OFF
): cv.string,
vol.Optional(
CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD
CONF_PAYLOAD_OSCILLATION_ON, default=DEFAULT_PAYLOAD_OSCILLATE_ON
): cv.string,
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
}

View File

@ -214,6 +214,10 @@
"description": "Please configure specific details for {platform} entity \"{entity}\":",
"data": {
"device_class": "Device class",
"fan_feature_speed": "Speed support",
"fan_feature_preset_modes": "Preset modes support",
"fan_feature_oscillation": "Oscillation support",
"fan_feature_direction": "Direction support",
"options": "Add option",
"schema": "Schema",
"state_class": "State class",
@ -222,6 +226,10 @@
},
"data_description": {
"device_class": "The Device class of the {platform} entity. [Learn more.]({url}#device_class)",
"fan_feature_speed": "The fan supports multiple speeds.",
"fan_feature_preset_modes": "The fan supports preset modes.",
"fan_feature_oscillation": "The fan supports oscillation.",
"fan_feature_direction": "The fan supports direction.",
"options": "Options for allowed sensor state values. The sensors Device class must be set to Enumeration. The 'Options' setting cannot be used together with State class or Unit of measurement.",
"schema": "The schema to use. [Learn more.]({url}#comparison-of-light-mqtt-schemas)",
"state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)",
@ -404,6 +412,80 @@
"brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value."
}
},
"fan_direction_settings": {
"name": "Direction settings",
"data": {
"direction_command_topic": "Direction command topic",
"direction_command_template": "Direction command template",
"direction_state_topic": "Direction state topic",
"direction_value_template": "Direction value template"
},
"data_description": {
"direction_command_topic": "The MQTT topic to publish commands to change the fan direction payload, either `forward` or `reverse`. Use the direction command template to customize the payload. [Learn more.]({url}#direction_command_topic)",
"direction_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the direction command topic. The template variable `value` will be either `forward` or `reverse`.",
"direction_state_topic": "The MQTT topic subscribed to receive fan direction state. Accepted state payloads are `forward` or `reverse`. [Learn more.]({url}#direction_state_topic)",
"direction_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan direction state value. The template should return either `forward` or `reverse`. When the template returns an empty string, the direction will be ignored."
}
},
"fan_oscillation_settings": {
"name": "Oscillation settings",
"data": {
"oscillation_command_topic": "Oscillation command topic",
"oscillation_command_template": "Oscillation command template",
"oscillation_state_topic": "Oscillation state topic",
"oscillation_value_template": "Oscillation value template",
"payload_oscillation_off": "Payload \"oscillation off\"",
"payload_oscillation_on": "Payload \"oscillation on\""
},
"data_description": {
"oscillation_command_topic": "The MQTT topic to publish commands to change the fan oscillation state. [Learn more.]({url}#oscillation_command_topic)",
"oscillation_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the oscillation command topic.",
"oscillation_state_topic": "The MQTT topic subscribed to receive fan oscillation state. [Learn more.]({url}#oscillation_state_topic)",
"oscillation_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan oscillation state value.",
"payload_oscillation_off": "The payload that represents the oscillation \"off\" state.",
"payload_oscillation_on": "The payload that represents the oscillation \"on\" state."
}
},
"fan_preset_mode_settings": {
"name": "Preset mode settings",
"data": {
"payload_reset_preset_mode": "Payload \"reset preset mode\"",
"preset_modes": "Preset modes",
"preset_mode_command_topic": "Preset mode command topic",
"preset_mode_command_template": "Preset mode command template",
"preset_mode_state_topic": "Preset mode state topic",
"preset_mode_value_template": "Preset mode value template"
},
"data_description": {
"payload_reset_preset_mode": "A special payload that resets the fan preset mode state attribute to unknown when received at the preset mode state topic.",
"preset_modes": "List of preset modes this fan is capable of running at. Common examples include auto, smart, whoosh, eco and breeze.",
"preset_mode_command_topic": "The MQTT topic to publish commands to change the fan preset mode. [Learn more.]({url}#preset_mode_command_topic)",
"preset_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the preset mode command topic.",
"preset_mode_state_topic": "The MQTT topic subscribed to receive fan preset mode. [Learn more.]({url}#preset_mode_state_topic)",
"preset_mode_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan preset mode value."
}
},
"fan_speed_settings": {
"name": "Speed settings",
"data": {
"payload_reset_percentage": "Payload \"reset percentage\"",
"percentage_command_topic": "Percentage command topic",
"percentage_command_template": "Percentage command template",
"percentage_state_topic": "Percentage state topic",
"percentage_value_template": "Percentage value template",
"speed_range_min": "Speed range min",
"speed_range_max": "Speed range max"
},
"data_description": {
"payload_reset_percentage": "A special payload that resets the fan speed percentage state attribute to unknown when received at the percentage state topic.",
"percentage_command_topic": "The MQTT topic to publish commands to change the fan speed state based on a percentage. [Learn more.]({url}#percentage_command_topic)",
"percentage_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the percentage command topic.",
"percentage_state_topic": "The MQTT topic subscribed to receive fan speed based on percentage. [Learn more.]({url}#percentage_state_topic)",
"percentage_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the speed percentage value.",
"speed_range_min": "The minimum of numeric output range (off not included, so speed_range_min - 1 represents 0 %). The number of speeds within the \"speed range\" / 100 will determine the percentage step.",
"speed_range_max": "The maximum of numeric output range (representing 100 %). The number of speeds within the \"speed range\" / 100 will determine the percentage step."
}
},
"light_color_mode_settings": {
"name": "Color mode settings",
"data": {
@ -551,6 +633,8 @@
"cover_tilt_command_template_must_be_used_with_tilt_command_topic": "The tilt command template must be used with the tilt command topic",
"cover_tilt_status_template_must_be_used_with_tilt_status_topic": "The tilt value template must be used with the tilt status topic",
"cover_value_template_must_be_used_with_state_topic": "The value template must be used with the state topic option",
"fan_speed_range_max_must_be_greater_than_speed_range_min": "Speed range max must be greater than speed range min",
"fan_preset_mode_reset_in_preset_modes_list": "Payload \"preset mode reset\" is not a valid preset mode",
"invalid_input": "Invalid value",
"invalid_subscribe_topic": "Invalid subscribe topic",
"invalid_template": "Invalid template",
@ -817,6 +901,7 @@
"binary_sensor": "[%key:component::binary_sensor::title%]",
"button": "[%key:component::button::title%]",
"cover": "[%key:component::cover::title%]",
"fan": "[%key:component::fan::title%]",
"light": "[%key:component::light::title%]",
"notify": "[%key:component::notify::title%]",
"sensor": "[%key:component::sensor::title%]",

View File

@ -127,6 +127,44 @@ MOCK_SUBENTRY_COVER_COMPONENT = {
"entity_picture": "https://example.com/b37acf667fa04c688ad7dfb27de2178b",
},
}
MOCK_SUBENTRY_FAN_COMPONENT = {
"717f924ae9ca4fe9864d845d75d23c9f": {
"platform": "fan",
"name": "Breezer",
"command_topic": "test-topic",
"state_topic": "test-topic",
"command_template": "{{ value }}",
"value_template": "{{ value_json.value }}",
"percentage_command_topic": "test-topic/pct",
"percentage_state_topic": "test-topic/pct",
"percentage_command_template": "{{ value }}",
"percentage_value_template": "{{ value_json.percentage }}",
"payload_reset_percentage": "None",
"preset_modes": ["eco", "auto"],
"preset_mode_command_topic": "test-topic/prm",
"preset_mode_state_topic": "test-topic/prm",
"preset_mode_command_template": "{{ value }}",
"preset_mode_value_template": "{{ value_json.preset_mode }}",
"payload_reset_preset_mode": "None",
"oscillation_command_topic": "test-topic/osc",
"oscillation_state_topic": "test-topic/osc",
"oscillation_command_template": "{{ value }}",
"oscillation_value_template": "{{ value_json.oscillation }}",
"payload_oscillation_off": "oscillate_off",
"payload_oscillation_on": "oscillate_on",
"direction_command_topic": "test-topic/dir",
"direction_state_topic": "test-topic/dir",
"direction_command_template": "{{ value }}",
"direction_value_template": "{{ value_json.direction }}",
"payload_off": "OFF",
"payload_on": "ON",
"entity_picture": "https://example.com/717f924ae9ca4fe9864d845d75d23c9f",
"optimistic": False,
"retain": False,
"speed_range_max": 100,
"speed_range_min": 1,
},
}
MOCK_SUBENTRY_NOTIFY_COMPONENT1 = {
"363a7ecad6be4a19b939a016ea93e994": {
"platform": "notify",
@ -264,6 +302,10 @@ MOCK_COVER_SUBENTRY_DATA_SINGLE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_COVER_COMPONENT,
}
MOCK_FAN_SUBENTRY_DATA_SINGLE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_FAN_COMPONENT,
}
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}},
"components": MOCK_SUBENTRY_NOTIFY_COMPONENT1,

View File

@ -36,6 +36,7 @@ from .common import (
MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE,
MOCK_BUTTON_SUBENTRY_DATA_SINGLE,
MOCK_COVER_SUBENTRY_DATA_SINGLE,
MOCK_FAN_SUBENTRY_DATA_SINGLE,
MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE,
MOCK_NOTIFY_SUBENTRY_DATA_MULTI,
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME,
@ -2785,6 +2786,157 @@ async def test_migrate_of_incompatible_config_entry(
),
"Milk notifier Blind",
),
(
MOCK_FAN_SUBENTRY_DATA_SINGLE,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{"name": "Breezer"},
{
"fan_feature_speed": True,
"fan_feature_preset_modes": True,
"fan_feature_oscillation": True,
"fan_feature_direction": True,
},
(),
{
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"fan_speed_settings": {
"percentage_command_template": "{{ value }}",
"percentage_command_topic": "test-topic/pct",
"percentage_state_topic": "test-topic/pct",
"percentage_value_template": "{{ value_json.percentage }}",
"speed_range_min": 1,
"speed_range_max": 100,
"payload_reset_percentage": "None",
},
"fan_preset_mode_settings": {
"preset_modes": ["eco", "auto"],
"preset_mode_command_template": "{{ value }}",
"preset_mode_command_topic": "test-topic/prm",
"preset_mode_state_topic": "test-topic/prm",
"preset_mode_value_template": "{{ value_json.preset_mode }}",
"payload_reset_preset_mode": "None",
},
"fan_oscillation_settings": {
"oscillation_command_template": "{{ value }}",
"oscillation_command_topic": "test-topic/osc",
"oscillation_state_topic": "test-topic/osc",
"oscillation_value_template": "{{ value_json.oscillation }}",
},
"fan_direction_settings": {
"direction_command_template": "{{ value }}",
"direction_command_topic": "test-topic/dir",
"direction_state_topic": "test-topic/dir",
"direction_value_template": "{{ value_json.direction }}",
},
"retain": False,
"optimistic": False,
},
(
(
{
"command_topic": "test-topic#invalid",
"fan_speed_settings": {
"percentage_command_topic": "test-topic#invalid",
},
"fan_preset_mode_settings": {
"preset_modes": ["eco", "auto"],
"preset_mode_command_topic": "test-topic#invalid",
},
"fan_oscillation_settings": {
"oscillation_command_topic": "test-topic#invalid",
},
"fan_direction_settings": {
"direction_command_topic": "test-topic#invalid",
},
},
{
"command_topic": "invalid_publish_topic",
"fan_preset_mode_settings": "invalid_publish_topic",
"fan_speed_settings": "invalid_publish_topic",
"fan_oscillation_settings": "invalid_publish_topic",
"fan_direction_settings": "invalid_publish_topic",
},
),
(
{
"command_topic": "test-topic",
"state_topic": "test-topic#invalid",
"fan_speed_settings": {
"percentage_command_topic": "test-topic",
"percentage_state_topic": "test-topic#invalid",
},
"fan_preset_mode_settings": {
"preset_modes": ["eco", "auto"],
"preset_mode_command_topic": "test-topic",
"preset_mode_state_topic": "test-topic#invalid",
},
"fan_oscillation_settings": {
"oscillation_command_topic": "test-topic",
"oscillation_state_topic": "test-topic#invalid",
},
"fan_direction_settings": {
"direction_command_topic": "test-topic",
"direction_state_topic": "test-topic#invalid",
},
},
{
"state_topic": "invalid_subscribe_topic",
"fan_preset_mode_settings": "invalid_subscribe_topic",
"fan_speed_settings": "invalid_subscribe_topic",
"fan_oscillation_settings": "invalid_subscribe_topic",
"fan_direction_settings": "invalid_subscribe_topic",
},
),
(
{
"command_topic": "test-topic",
"fan_speed_settings": {
"percentage_command_topic": "test-topic",
},
"fan_preset_mode_settings": {
"preset_modes": ["None", "auto"],
"preset_mode_command_topic": "test-topic",
},
"fan_oscillation_settings": {
"oscillation_command_topic": "test-topic",
},
"fan_direction_settings": {
"direction_command_topic": "test-topic",
},
},
{
"fan_preset_mode_settings": "fan_preset_mode_reset_in_preset_modes_list",
},
),
(
{
"command_topic": "test-topic",
"fan_speed_settings": {
"percentage_command_topic": "test-topic",
"speed_range_min": 100,
"speed_range_max": 10,
},
"fan_preset_mode_settings": {
"preset_modes": ["eco", "auto"],
"preset_mode_command_topic": "test-topic",
},
"fan_oscillation_settings": {
"oscillation_command_topic": "test-topic",
},
"fan_direction_settings": {
"direction_command_topic": "test-topic",
},
},
{
"fan_speed_settings": "fan_speed_range_max_must_be_greater_than_speed_range_min",
},
),
),
"Milk notifier Breezer",
),
(
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
{"name": "Milk notifier", "mqtt_settings": {"qos": 1}},
@ -2971,6 +3123,7 @@ async def test_migrate_of_incompatible_config_entry(
"binary_sensor",
"button",
"cover",
"fan",
"notify_with_entity_name",
"notify_no_entity_name",
"sensor_options",