Skip to content

Commit dcce889

Browse files
committed
Add sensor as entity platform on MQTT subentries
1 parent e082098 commit dcce889

File tree

9 files changed

+630
-113
lines changed

9 files changed

+630
-113
lines changed

homeassistant/components/mqtt/config_flow.py

+225-35
Large diffs are not rendered by default.

homeassistant/components/mqtt/const.py

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic"
6464
CONF_ENABLED_BY_DEFAULT = "enabled_by_default"
6565
CONF_ENTITY_PICTURE = "entity_picture"
66+
CONF_EXPIRE_AFTER = "expire_after"
67+
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
6668
CONF_MAX_KELVIN = "max_kelvin"
6769
CONF_MIN_KELVIN = "min_kelvin"
6870
CONF_MODE_COMMAND_TEMPLATE = "mode_command_template"
@@ -82,6 +84,7 @@
8284
CONF_STATE_CLOSING = "state_closing"
8385
CONF_STATE_OPEN = "state_open"
8486
CONF_STATE_OPENING = "state_opening"
87+
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
8588
CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template"
8689
CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic"
8790
CONF_TEMP_STATE_TEMPLATE = "temperature_state_template"

homeassistant/components/mqtt/entity.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
async_subscribe_topics_internal,
124124
async_unsubscribe_topics,
125125
)
126-
from .util import mqtt_config_entry_enabled
126+
from .util import learn_more_url, mqtt_config_entry_enabled
127127

128128
_LOGGER = logging.getLogger(__name__)
129129

@@ -346,17 +346,14 @@ def _async_setup_entities() -> None:
346346
line = getattr(yaml_config, "__line__", "?")
347347
issue_id = hex(hash(frozenset(yaml_config)))
348348
yaml_config_str = yaml_dump(yaml_config)
349-
learn_more_url = (
350-
f"https://www.home-assistant.io/integrations/{domain}.mqtt/"
351-
)
352349
async_create_issue(
353350
hass,
354351
DOMAIN,
355352
issue_id,
356353
issue_domain=domain,
357354
is_fixable=False,
358355
severity=IssueSeverity.ERROR,
359-
learn_more_url=learn_more_url,
356+
learn_more_url=learn_more_url(domain),
360357
translation_placeholders={
361358
"domain": domain,
362359
"config_file": config_file,

homeassistant/components/mqtt/sensor.py

+9-39
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
RestoreSensor,
1818
SensorDeviceClass,
1919
SensorExtraStoredData,
20-
SensorStateClass,
2120
)
2221
from homeassistant.config_entries import ConfigEntry
2322
from homeassistant.const import (
@@ -39,20 +38,23 @@
3938

4039
from . import subscription
4140
from .config import MQTT_RO_SCHEMA
42-
from .const import CONF_OPTIONS, CONF_STATE_TOPIC, PAYLOAD_NONE
41+
from .const import (
42+
CONF_EXPIRE_AFTER,
43+
CONF_LAST_RESET_VALUE_TEMPLATE,
44+
CONF_OPTIONS,
45+
CONF_STATE_TOPIC,
46+
CONF_SUGGESTED_DISPLAY_PRECISION,
47+
PAYLOAD_NONE,
48+
)
4349
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
4450
from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage
4551
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
46-
from .util import check_state_too_long
52+
from .util import check_state_too_long, validate_sensor_state_and_device_class_config
4753

4854
_LOGGER = logging.getLogger(__name__)
4955

5056
PARALLEL_UPDATES = 0
5157

52-
CONF_EXPIRE_AFTER = "expire_after"
53-
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
54-
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
55-
5658
MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
5759
{
5860
sensor.ATTR_LAST_RESET,
@@ -78,38 +80,6 @@
7880
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
7981

8082

81-
def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigType:
82-
"""Validate the sensor options, state and device class config."""
83-
if (
84-
CONF_LAST_RESET_VALUE_TEMPLATE in config
85-
and (state_class := config.get(CONF_STATE_CLASS)) != SensorStateClass.TOTAL
86-
):
87-
raise vol.Invalid(
88-
f"The option `{CONF_LAST_RESET_VALUE_TEMPLATE}` cannot be used "
89-
f"together with state class `{state_class}`"
90-
)
91-
92-
# Only allow `options` to be set for `enum` sensors
93-
# to limit the possible sensor values
94-
if (options := config.get(CONF_OPTIONS)) is not None:
95-
if not options:
96-
raise vol.Invalid("An empty options list is not allowed")
97-
if config.get(CONF_STATE_CLASS) or config.get(CONF_UNIT_OF_MEASUREMENT):
98-
raise vol.Invalid(
99-
f"Specifying `{CONF_OPTIONS}` is not allowed together with "
100-
f"the `{CONF_STATE_CLASS}` or `{CONF_UNIT_OF_MEASUREMENT}` option"
101-
)
102-
103-
if (device_class := config.get(CONF_DEVICE_CLASS)) != SensorDeviceClass.ENUM:
104-
raise vol.Invalid(
105-
f"The option `{CONF_OPTIONS}` must be used "
106-
f"together with device class `{SensorDeviceClass.ENUM}`, "
107-
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
108-
)
109-
110-
return config
111-
112-
11383
PLATFORM_SCHEMA_MODERN = vol.All(
11484
_PLATFORM_SCHEMA_BASE,
11585
validate_sensor_state_and_device_class_config,

homeassistant/components/mqtt/strings.json

+103-3
Original file line numberDiff line numberDiff line change
@@ -196,18 +196,46 @@
196196
"component": "Select the entity you want to update."
197197
}
198198
},
199+
"entity_platform_config": {
200+
"title": "Configure MQTT device \"{mqtt_device}\"",
201+
"description": "Please configure specific details for {platform} entity \"{entity}\":",
202+
"data": {
203+
"device_class": "Device class",
204+
"state_class": "State class",
205+
"unit_of_measurement": "Unit of measurement",
206+
"suggested_display_precision": "Suggested display precision",
207+
"options": "Options"
208+
},
209+
"data_description": {
210+
"device_class": "The type/class of the {platform} entity to set the icon in the frontend. [Learn more..]({url}#device_class)",
211+
"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)",
212+
"unit_of_measurement": "Defines the units of measurement of the sensor, if any.",
213+
"suggested_display_precision": "The number of decimals which should be used in the sensor’s state after rounding. [Learn more..]({url}#suggested_display_precision)",
214+
"options": "List of allowed sensor state value. The sensor’s device_class must be set to Enumeration. The options option cannot be used together with State Class or Unit of measurement."
215+
}
216+
},
199217
"mqtt_platform_config": {
200218
"title": "Configure MQTT device \"{mqtt_device}\"",
201219
"description": "Please configure MQTT specific details for {platform} entity \"{entity}\":",
202220
"data": {
203221
"command_topic": "Command topic",
204222
"command_template": "Command template",
223+
"state_topic": "State topic",
224+
"value_template": "Value template",
225+
"last_reset_value_template": "Last reset value template",
226+
"expire_after": "Expire after",
227+
"force_update": "Force update",
205228
"retain": "Retain",
206229
"qos": "QoS"
207230
},
208231
"data_description": {
209-
"command_topic": "The publishing topic that will be used to control the {platform} entity.",
232+
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more..]({url}#command_topic)",
210233
"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.",
234+
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more..]({url}#state_topic)",
235+
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value.",
236+
"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 temaplate is set, the State class option must be Total. [Learn more..]({url}#last_reset_value_template)",
237+
"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)",
238+
"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)",
211239
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
212240
"qos": "The QoS value {platform} entity should use."
213241
}
@@ -223,7 +251,11 @@
223251
"invalid_input": "Invalid value",
224252
"invalid_subscribe_topic": "Invalid subscribe topic",
225253
"invalid_template": "Invalid template",
226-
"invalid_url": "Invalid URL"
254+
"invalid_uom": "The 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",
255+
"invalid_url": "Invalid URL",
256+
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
257+
"options_not_allowed_with_state_class_or_uom": "The 'Options' setting is not allowed when state class or unit of measurement are used",
258+
"options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class'"
227259
}
228260
}
229261
},
@@ -340,9 +372,70 @@
340372
}
341373
},
342374
"selector": {
375+
"device_class": {
376+
"options": {
377+
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
378+
"area": "[%key:component::sensor::entity_component::area::name%]",
379+
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
380+
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
381+
"battery": "[%key:component::sensor::entity_component::battery::name%]",
382+
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
383+
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
384+
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
385+
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
386+
"current": "[%key:component::sensor::entity_component::current::name%]",
387+
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
388+
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
389+
"date": "[%key:component::sensor::entity_component::date::name%]",
390+
"distance": "[%key:component::sensor::entity_component::distance::name%]",
391+
"duration": "[%key:component::sensor::entity_component::duration::name%]",
392+
"energy": "[%key:component::sensor::entity_component::energy::name%]",
393+
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
394+
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
395+
"enum": "Enumeration",
396+
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
397+
"gas": "[%key:component::sensor::entity_component::gas::name%]",
398+
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
399+
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
400+
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
401+
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
402+
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
403+
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
404+
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
405+
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
406+
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
407+
"ph": "[%key:component::sensor::entity_component::ph::name%]",
408+
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
409+
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
410+
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
411+
"power": "[%key:component::sensor::entity_component::power::name%]",
412+
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
413+
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
414+
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
415+
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
416+
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
417+
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
418+
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
419+
"speed": "[%key:component::sensor::entity_component::speed::name%]",
420+
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
421+
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
422+
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
423+
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
424+
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
425+
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
426+
"volume": "[%key:component::sensor::entity_component::volume::name%]",
427+
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
428+
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
429+
"water": "[%key:component::sensor::entity_component::water::name%]",
430+
"weight": "[%key:component::sensor::entity_component::weight::name%]",
431+
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
432+
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
433+
}
434+
},
343435
"platform": {
344436
"options": {
345-
"notify": "Notify"
437+
"notify": "Notify",
438+
"sensor": "Sensor"
346439
}
347440
},
348441
"set_ca_cert": {
@@ -351,6 +444,13 @@
351444
"auto": "Auto",
352445
"custom": "Custom"
353446
}
447+
},
448+
"state_class": {
449+
"options": {
450+
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
451+
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
452+
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
453+
}
354454
}
355455
},
356456
"services": {

homeassistant/components/mqtt/util.py

+78-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,20 @@
1313

1414
import voluptuous as vol
1515

16+
from homeassistant.components.sensor import (
17+
CONF_STATE_CLASS,
18+
DEVICE_CLASS_UNITS,
19+
SensorDeviceClass,
20+
SensorStateClass,
21+
)
1622
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
17-
from homeassistant.const import MAX_LENGTH_STATE_STATE, STATE_UNKNOWN, Platform
23+
from homeassistant.const import (
24+
CONF_DEVICE_CLASS,
25+
CONF_UNIT_OF_MEASUREMENT,
26+
MAX_LENGTH_STATE_STATE,
27+
STATE_UNKNOWN,
28+
Platform,
29+
)
1830
from homeassistant.core import HomeAssistant, callback
1931
from homeassistant.exceptions import HomeAssistantError
2032
from homeassistant.helpers import config_validation as cv, template
@@ -29,6 +41,8 @@
2941
CONF_CERTIFICATE,
3042
CONF_CLIENT_CERT,
3143
CONF_CLIENT_KEY,
44+
CONF_LAST_RESET_VALUE_TEMPLATE,
45+
CONF_OPTIONS,
3246
DEFAULT_ENCODING,
3347
DEFAULT_QOS,
3448
DEFAULT_RETAIN,
@@ -411,3 +425,66 @@ def migrate_certificate_file_to_content(file_name_or_auto: str) -> str | None:
411425
return certificate_file.read()
412426
except OSError:
413427
return None
428+
429+
430+
@callback
431+
def learn_more_url(platform: str) -> str:
432+
"""Return the URL for the platform specific MQTT documentation."""
433+
return f"https://www.home-assistant.io/integrations/{platform}.mqtt/"
434+
435+
436+
@callback
437+
def validate_sensor_state_and_device_class_config(
438+
config: ConfigType, errors: dict[str, str] | None = None
439+
) -> ConfigType:
440+
"""Validate the sensor options, state and device class config."""
441+
if (
442+
CONF_LAST_RESET_VALUE_TEMPLATE in config
443+
and (state_class := config.get(CONF_STATE_CLASS)) != SensorStateClass.TOTAL
444+
):
445+
if errors is None:
446+
raise vol.Invalid(
447+
f"The option `{CONF_LAST_RESET_VALUE_TEMPLATE}` cannot be used "
448+
f"together with state class `{state_class}`"
449+
)
450+
errors[CONF_LAST_RESET_VALUE_TEMPLATE] = "last_reset_not_with_state_class_total"
451+
452+
# Only allow `options` to be set for `enum` sensors
453+
# to limit the possible sensor values
454+
if (options := config.get(CONF_OPTIONS)) is not None:
455+
if not options:
456+
raise vol.Invalid("An empty options list is not allowed")
457+
if config.get(CONF_STATE_CLASS) or config.get(CONF_UNIT_OF_MEASUREMENT):
458+
if errors is None:
459+
raise vol.Invalid(
460+
f"Specifying `{CONF_OPTIONS}` is not allowed together with "
461+
f"the `{CONF_STATE_CLASS}` or `{CONF_UNIT_OF_MEASUREMENT}` option"
462+
)
463+
errors[CONF_OPTIONS] = "options_not_allowed_with_state_class_or_uom"
464+
465+
if (device_class := config.get(CONF_DEVICE_CLASS)) != SensorDeviceClass.ENUM:
466+
if errors is None:
467+
raise vol.Invalid(
468+
f"The option `{CONF_OPTIONS}` must be used "
469+
f"together with device class `{SensorDeviceClass.ENUM}`, "
470+
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
471+
)
472+
errors[CONF_OPTIONS] = "options_device_class_enum"
473+
474+
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
475+
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
476+
) is None:
477+
return config
478+
479+
if (
480+
device_class in DEVICE_CLASS_UNITS
481+
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
482+
):
483+
if errors is None:
484+
raise vol.Invalid(
485+
f"The unit of measurement `{unit_of_measurement}` is not valid "
486+
f"together with device class `{device_class}`"
487+
)
488+
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
489+
490+
return config

0 commit comments

Comments
 (0)