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 sensor as entity platform on MQTT subentries #139899

Merged
merged 27 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f6e962b
Add sensor as entity platform on MQTT subentries
jbouwh Mar 15, 2025
4dff885
Fix typo
jbouwh Mar 17, 2025
f34271e
Improve device class data description
jbouwh Mar 17, 2025
89faadb
Tweak
jbouwh Mar 17, 2025
d74be02
Rework reconfig calculation
jbouwh Mar 17, 2025
3f0701a
Filter out last_reset_value_template if state class is not total
jbouwh Mar 17, 2025
e6e0477
Collapse expire after as advanced setting
jbouwh Mar 21, 2025
19f6cc0
Update suggested_display_precision translation strings
jbouwh Mar 21, 2025
d9e45cf
Make options and last_reset_template conditional, use sections for ad…
jbouwh Mar 22, 2025
55f027a
Merge branch 'dev' into mqtt-subentry-sensor
jbouwh Mar 22, 2025
3cb26a6
Ensure options are removed properly
jbouwh Mar 23, 2025
a00ffac
Improve sensor options label, ensure UOM is set when device class has…
jbouwh Mar 24, 2025
4b1f968
Merge branch 'dev' into mqtt-subentry-sensor
jbouwh Mar 24, 2025
de10613
Merge branch 'dev' into mqtt-subentry-sensor
jbouwh Mar 24, 2025
54f138a
Use helper to apply suggested values from component config
jbouwh Mar 24, 2025
cec98b0
Rename to `Add option`
jbouwh Mar 24, 2025
d5df517
Fix schema builder not hiding empty sections and removing fields excl…
jbouwh Mar 25, 2025
00fa76c
Do not hide advanced settings if values are available or are defaults
jbouwh Mar 25, 2025
018b87f
Merge branch 'dev' into mqtt-subentry-sensor
jbouwh Mar 25, 2025
56ccce5
Improve spelling and Learn more links
jbouwh Mar 25, 2025
4e9cf0d
Improve unit of measurement validation
jbouwh Mar 25, 2025
2a7667b
Fix UOM selector and translation strings
jbouwh Mar 25, 2025
303a599
Address comments from code review
jbouwh Mar 25, 2025
70a9561
Remove stale comment
jbouwh Mar 25, 2025
4bd9634
Rename selector constant, split validator
jbouwh Mar 26, 2025
09a2cd2
Simplify config validator
jbouwh Mar 26, 2025
cd5a865
Return tuple with config and errors for config validation
jbouwh Mar 26, 2025
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
419 changes: 376 additions & 43 deletions homeassistant/components/mqtt/config_flow.py

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions homeassistant/components/mqtt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@
CONF_EFFECT_TEMPLATE = "effect_template"
CONF_EFFECT_VALUE_TEMPLATE = "effect_value_template"
CONF_ENTITY_PICTURE = "entity_picture"
CONF_EXPIRE_AFTER = "expire_after"
CONF_FLASH_TIME_LONG = "flash_time_long"
CONF_FLASH_TIME_SHORT = "flash_time_short"
CONF_GREEN_TEMPLATE = "green_template"
CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
CONF_HS_STATE_TOPIC = "hs_state_topic"
CONF_HS_VALUE_TEMPLATE = "hs_value_template"
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
CONF_MAX_KELVIN = "max_kelvin"
CONF_MAX_MIREDS = "max_mireds"
CONF_MIN_KELVIN = "min_kelvin"
Expand Down Expand Up @@ -128,6 +130,7 @@
CONF_STATE_CLOSING = "state_closing"
CONF_STATE_OPEN = "state_open"
CONF_STATE_OPENING = "state_opening"
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
CONF_SUPPORTED_COLOR_MODES = "supported_color_modes"
CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template"
CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic"
Expand Down
7 changes: 2 additions & 5 deletions homeassistant/components/mqtt/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
async_subscribe_topics_internal,
async_unsubscribe_topics,
)
from .util import mqtt_config_entry_enabled
from .util import learn_more_url, mqtt_config_entry_enabled

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -346,17 +346,14 @@ def _async_setup_entities() -> None:
line = getattr(yaml_config, "__line__", "?")
issue_id = hex(hash(frozenset(yaml_config)))
yaml_config_str = yaml_dump(yaml_config)
learn_more_url = (
f"https://www.home-assistant.io/integrations/{domain}.mqtt/"
)
async_create_issue(
hass,
DOMAIN,
issue_id,
issue_domain=domain,
is_fixable=False,
severity=IssueSeverity.ERROR,
learn_more_url=learn_more_url,
learn_more_url=learn_more_url(domain),
translation_placeholders={
"domain": domain,
"config_file": config_file,
Expand Down
66 changes: 10 additions & 56 deletions homeassistant/components/mqtt/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
RestoreSensor,
SensorDeviceClass,
SensorExtraStoredData,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
Expand All @@ -41,20 +40,24 @@

from . import subscription
from .config import MQTT_RO_SCHEMA
from .const import CONF_OPTIONS, CONF_STATE_TOPIC, DOMAIN, PAYLOAD_NONE
from .const import (
CONF_EXPIRE_AFTER,
CONF_LAST_RESET_VALUE_TEMPLATE,
CONF_OPTIONS,
CONF_STATE_TOPIC,
CONF_SUGGESTED_DISPLAY_PRECISION,
DOMAIN,
PAYLOAD_NONE,
)
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import check_state_too_long
from .util import check_state_too_long, validate_sensor_state_and_device_class_config

_LOGGER = logging.getLogger(__name__)

PARALLEL_UPDATES = 0

CONF_EXPIRE_AFTER = "expire_after"
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"

MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
{
sensor.ATTR_LAST_RESET,
Expand Down Expand Up @@ -84,55 +87,6 @@
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)


def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigType:
"""Validate the sensor options, state and device class config."""
if (
CONF_LAST_RESET_VALUE_TEMPLATE in config
and (state_class := config.get(CONF_STATE_CLASS)) != SensorStateClass.TOTAL
):
raise vol.Invalid(
f"The option `{CONF_LAST_RESET_VALUE_TEMPLATE}` cannot be used "
f"together with state class `{state_class}`"
)

# Only allow `options` to be set for `enum` sensors
# to limit the possible sensor values
if (options := config.get(CONF_OPTIONS)) is not None:
if not options:
raise vol.Invalid("An empty options list is not allowed")
if config.get(CONF_STATE_CLASS) or config.get(CONF_UNIT_OF_MEASUREMENT):
raise vol.Invalid(
f"Specifying `{CONF_OPTIONS}` is not allowed together with "
f"the `{CONF_STATE_CLASS}` or `{CONF_UNIT_OF_MEASUREMENT}` option"
)

if (device_class := config.get(CONF_DEVICE_CLASS)) != SensorDeviceClass.ENUM:
raise vol.Invalid(
f"The option `{CONF_OPTIONS}` must be used "
f"together with device class `{SensorDeviceClass.ENUM}`, "
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
)

if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None:
return config

if (
device_class in DEVICE_CLASS_UNITS
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
):
_LOGGER.warning(
"The unit of measurement `%s` is not valid "
"together with device class `%s`. "
"this will stop working in HA Core 2025.7.0",
unit_of_measurement,
device_class,
)

return config


PLATFORM_SCHEMA_MODERN = vol.All(
_PLATFORM_SCHEMA_BASE,
validate_sensor_state_and_device_class_config,
Expand Down
125 changes: 122 additions & 3 deletions homeassistant/components/mqtt/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,20 +198,66 @@
"component": "Select the entity you want to update."
}
},
"entity_platform_config": {
"title": "Configure MQTT device \"{mqtt_device}\"",
"description": "Please configure specific details for {platform} entity \"{entity}\":",
"data": {
"device_class": "Device class",
"state_class": "State class",
"unit_of_measurement": "Unit of measurement",
"options": "Add option"
},
"data_description": {
"device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)",
"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)",
"unit_of_measurement": "Defines the unit of measurement of the sensor, if any.",
"options": "Options for allowed sensor state values. The sensor’s device_class must be set to Enumeration. The options option cannot be used together with State Class or Unit of measurement."
},
"sections": {
"advanced_settings": {
"name": "Advanced options",
"data": {
"suggested_display_precision": "Suggested display precision"
},
"data_description": {
"suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)"
}
}
}
},
"mqtt_platform_config": {
"title": "Configure MQTT device \"{mqtt_device}\"",
"description": "Please configure MQTT specific details for {platform} entity \"{entity}\":",
"data": {
"command_topic": "Command topic",
"command_template": "Command template",
"state_topic": "State topic",
"value_template": "Value template",
"last_reset_value_template": "Last reset value template",
"force_update": "Force update",
"retain": "Retain",
"qos": "QoS"
},
"data_description": {
"command_topic": "The publishing topic that will be used to control the {platform} entity.",
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
"command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.",
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
"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.",
"qos": "The QoS value {platform} entity should use."
},
"sections": {
"advanced_settings": {
"name": "Advanced settings",
"data": {
"expire_after": "Expire after"
},
"data_description": {
"expire_after": "If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. Default the sensors state never expires. [Learn more.]({url}#expire_after)"
}
}
}
}
},
Expand All @@ -225,7 +271,12 @@
"invalid_input": "Invalid value",
"invalid_subscribe_topic": "Invalid subscribe topic",
"invalid_template": "Invalid template",
"invalid_url": "Invalid URL"
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not allowed with the selected device class, please use the correct device class, or pick a valid unit of measurement from the list",
"invalid_url": "Invalid URL",
"options_not_allowed_with_state_class_or_uom": "The 'Options' setting is not allowed when state class or unit of measurement are used",
"options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class'. If you continue, the existing options will be reset",
"options_with_enum_device_class": "Configure options for the enumeration sensor",
"uom_required_for_device_class": "The selected device device class requires a unit"
}
}
},
Expand Down Expand Up @@ -342,9 +393,70 @@
}
},
"selector": {
"device_class_sensor": {
"options": {
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"area": "[%key:component::sensor::entity_component::area::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
"battery": "[%key:component::sensor::entity_component::battery::name%]",
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
"current": "[%key:component::sensor::entity_component::current::name%]",
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
"date": "[%key:component::sensor::entity_component::date::name%]",
"distance": "[%key:component::sensor::entity_component::distance::name%]",
"duration": "[%key:component::sensor::entity_component::duration::name%]",
"energy": "[%key:component::sensor::entity_component::energy::name%]",
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
"enum": "Enumeration",
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
"gas": "[%key:component::sensor::entity_component::gas::name%]",
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
"ph": "[%key:component::sensor::entity_component::ph::name%]",
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
"power": "[%key:component::sensor::entity_component::power::name%]",
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
"speed": "[%key:component::sensor::entity_component::speed::name%]",
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
"volume": "[%key:component::sensor::entity_component::volume::name%]",
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
"water": "[%key:component::sensor::entity_component::water::name%]",
"weight": "[%key:component::sensor::entity_component::weight::name%]",
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
}
},
Comment on lines +396 to +455
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a plan for how we can avoid having to add this list to integrations? Can we maybe allow a translation_domain or something on selector?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have asked @joostlek if he had an idea, else I'll pick up this up in a follow up PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, it has to be done in a separate PR.

"platform": {
"options": {
"notify": "Notify"
"notify": "Notify",
"sensor": "Sensor"
}
},
"set_ca_cert": {
Expand All @@ -353,6 +465,13 @@
"auto": "Auto",
"custom": "Custom"
}
},
"state_class": {
"options": {
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
}
}
},
"services": {
Expand Down
Loading