Compare commits

...

2 Commits

Author SHA1 Message Date
Jan Bouwhuis 2b80a66026 Apply suggestion from @jbouwh 2026-05-20 23:23:57 +02:00
jbouwh 9815dd74fd Add subentry support for MQTT date, datetime and time entities 2026-05-19 20:05:08 +00:00
6 changed files with 247 additions and 2 deletions
@@ -351,6 +351,7 @@ from .const import (
CONF_TILT_STATE_OPTIMISTIC,
CONF_TILT_STATUS_TEMPLATE,
CONF_TILT_STATUS_TOPIC,
CONF_TIMEZONE,
CONF_TLS_INSECURE,
CONF_TRANSITION,
CONF_TRANSPORT,
@@ -458,6 +459,8 @@ SUBENTRY_PLATFORMS = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.DATE,
Platform.DATETIME,
Platform.FAN,
Platform.IMAGE,
Platform.LIGHT,
@@ -469,6 +472,7 @@ SUBENTRY_PLATFORMS = [
Platform.SIREN,
Platform.SWITCH,
Platform.TEXT,
Platform.TIME,
Platform.VALVE,
Platform.WATER_HEATER,
]
@@ -482,6 +486,10 @@ PWD_NOT_CHANGED = "__**password_not_changed**__"
DEVELOPER_DOCUMENTATION_URL = "https://developers.home-assistant.io/"
USER_DOCUMENTATION_URL = "https://www.home-assistant.io/"
TZ_ZONE_ABBR_URL = (
"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
"#Time_zone_abbreviations"
)
INTEGRATION_URL = f"{USER_DOCUMENTATION_URL}integrations/{DOMAIN}/"
TEMPLATING_URL = f"{USER_DOCUMENTATION_URL}docs/configuration/templating/"
@@ -501,6 +509,7 @@ TRANSLATION_DESCRIPTION_PLACEHOLDERS = {
"available_state_classes_url": AVAILABLE_STATE_CLASSES_URL,
"naming_entities_url": NAMING_ENTITIES_URL,
"registry_properties_url": REGISTRY_PROPERTIES_URL,
"tz_abbr_url": TZ_ZONE_ABBR_URL,
}
# Common selectors
@@ -1234,6 +1243,8 @@ ENTITY_CONFIG_VALIDATOR: dict[
Platform.BUTTON: None,
Platform.CLIMATE: validate_climate_platform_config,
Platform.COVER: validate_cover_platform_config,
Platform.DATE: None,
Platform.DATETIME: None,
Platform.FAN: validate_fan_platform_config,
Platform.IMAGE: None,
Platform.LIGHT: validate_light_platform_config,
@@ -1245,6 +1256,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
Platform.SIREN: None,
Platform.SWITCH: None,
Platform.TEXT: validate_text_platform_config,
Platform.TIME: None,
Platform.VALVE: None,
Platform.WATER_HEATER: validate_water_heater_platform_config,
}
@@ -1410,6 +1422,8 @@ PLATFORM_ENTITY_FIELDS: dict[Platform, dict[str, PlatformField]] = {
required=False,
),
},
Platform.DATE: {},
Platform.DATETIME: {},
Platform.FAN: {
"fan_feature_speed": PlatformField(
selector=BOOLEAN_SELECTOR,
@@ -1514,6 +1528,7 @@ PLATFORM_ENTITY_FIELDS: dict[Platform, dict[str, PlatformField]] = {
),
},
Platform.TEXT: {},
Platform.TIME: {},
Platform.VALVE: {
CONF_DEVICE_CLASS: PlatformField(
selector=VALVE_DEVICE_CLASS_SELECTOR, required=False, default=None
@@ -2363,6 +2378,61 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
section="cover_tilt_settings",
),
},
Platform.DATE: {
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_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
},
Platform.DATETIME: {
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_TIMEZONE: PlatformField(selector=TEXT_SELECTOR, required=False),
CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
},
Platform.FAN: {
CONF_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
@@ -3470,6 +3540,33 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
section="text_advanced_settings",
),
},
Platform.TIME: {
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_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
},
Platform.VALVE: {
CONF_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
+1
View File
@@ -55,6 +55,7 @@ CONF_RETAIN = ATTR_RETAIN
CONF_SCHEMA = "schema"
CONF_STATE_TOPIC = "state_topic"
CONF_STATE_VALUE_TEMPLATE = "state_value_template"
CONF_TIMEZONE = "timezone"
CONF_TOPIC = "topic"
CONF_TRANSPORT = "transport"
CONF_WS_PATH = "ws_path"
+1 -2
View File
@@ -27,6 +27,7 @@ from .const import (
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_STATE_TOPIC,
CONF_TIMEZONE,
PAYLOAD_NONE,
)
from .entity import MqttEntity, async_setup_entity_entry_helper
@@ -40,8 +41,6 @@ from .schemas import MQTT_ENTITY_COMMON_SCHEMA
_LOGGER = logging.getLogger(__name__)
CONF_TIMEZONE = "timezone"
PARALLEL_UPDATES = 0
DEFAULT_NAME = "MQTT Date/Time"
@@ -376,6 +376,7 @@
"support_duration": "Duration support",
"support_volume_set": "Set volume support",
"supported_color_modes": "Supported color modes",
"timezone": "Time zone",
"url_template": "URL template",
"url_topic": "URL topic",
"value_template": "Value template"
@@ -428,6 +429,7 @@
"support_duration": "The siren supports setting a duration in second. The `duration` variable will become available for use in the \"Command template\" setting. [Learn more.]({url}#support_duration)",
"support_volume_set": "The siren supports setting a volume. The `volume_level` variable will become available for use in the \"Command template\" setting. [Learn more.]({url}#support_volume_set)",
"supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
"timezone": "Set to a valid [IANA time zone identifier]({tz_abbr_url}). Do not set this option if the date/time structure is providing time zone information via the status update.",
"url_template": "[Template]({value_templating_url}) to extract an URL from the received URL topic payload value. [Learn more.]({url}#url_template)",
"url_topic": "The MQTT topic subscribed to receive messages containing the image URL. [Learn more.]({url}#url_topic)",
"value_template": "Defines a [template]({value_templating_url}) to extract the {platform} entity value. [Learn more.]({url}#value_template)"
@@ -1463,6 +1465,8 @@
"button": "[%key:component::button::title%]",
"climate": "[%key:component::climate::title%]",
"cover": "[%key:component::cover::title%]",
"date": "[%key:component::date::title%]",
"datetime": "[%key:component::datetime::title%]",
"fan": "[%key:component::fan::title%]",
"image": "[%key:component::image::title%]",
"light": "[%key:component::light::title%]",
@@ -1474,6 +1478,7 @@
"siren": "[%key:component::siren::title%]",
"switch": "[%key:component::switch::title%]",
"text": "[%key:component::text::title%]",
"time": "[%key:component::time::title%]",
"valve": "[%key:component::valve::title%]",
"water_heater": "[%key:component::water_heater::title%]"
}
+52
View File
@@ -319,6 +319,33 @@ MOCK_SUBENTRY_COVER_COMPONENT = {
"entity_picture": "https://example.com/b37acf667fa04c688ad7dfb27de2178b",
},
}
MOCK_SUBENTRY_DATE_COMPONENT = {
"aa261f6feed443e7b7d5f3fbe2a47411": {
"platform": "date",
"name": "Delivery day",
"entity_category": None,
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"retain": False,
"entity_picture": "https://example.com/aa261f6feed443e7b7d5f3fbe2a47411",
},
}
MOCK_SUBENTRY_DATETIME_COMPONENT = {
"aa261f6feed443e7b7d5f3fbe2a47412": {
"platform": "datetime",
"name": "Maintenance service",
"entity_category": None,
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"timezone": "GMT",
"retain": False,
"entity_picture": "https://example.com/aa261f6feed443e7b7d5f3fbe2a47412",
},
}
MOCK_SUBENTRY_FAN_COMPONENT = {
"717f924ae9ca4fe9864d845d75d23c9f": {
"platform": "fan",
@@ -653,6 +680,19 @@ MOCK_SUBENTRY_TEXT_COMPONENT = {
"entity_picture": "https://example.com/09261f6feed443e7b7d5f3fbe2a47413",
},
}
MOCK_SUBENTRY_TIME_COMPONENT = {
"aa261f6feed443e7b7d5f3fbe2a47413": {
"platform": "time",
"name": "Happy hour",
"entity_category": None,
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"retain": False,
"entity_picture": "https://example.com/aa261f6feed443e7b7d5f3fbe2a47413",
},
}
MOCK_SUBENTRY_VALVE_COMPONENT_STATE = {
"09261f6feed443e7b7d5f32345a47413": {
"platform": "valve",
@@ -789,6 +829,14 @@ MOCK_COVER_SUBENTRY_DATA = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_COVER_COMPONENT,
}
MOCK_DATE_SUBENTRY_DATA = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_DATE_COMPONENT,
}
MOCK_DATETIME_SUBENTRY_DATA = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_DATETIME_COMPONENT,
}
MOCK_FAN_SUBENTRY_DATA = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_FAN_COMPONENT,
@@ -865,6 +913,10 @@ MOCK_TEXT_SUBENTRY_DATA = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_TEXT_COMPONENT,
}
MOCK_TIME_SUBENTRY_DATA = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_TIME_COMPONENT,
}
MOCK_VALVE_SUBENTRY_DATA_STATE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_VALVE_COMPONENT_STATE,
+91
View File
@@ -46,6 +46,8 @@ from .common import (
MOCK_CLIMATE_NO_TARGET_TEMP_SUBENTRY_DATA,
MOCK_CLIMATE_SUBENTRY_DATA,
MOCK_COVER_SUBENTRY_DATA,
MOCK_DATE_SUBENTRY_DATA,
MOCK_DATETIME_SUBENTRY_DATA,
MOCK_FAN_SUBENTRY_DATA,
MOCK_IMAGE_SUBENTRY_DATA_IMAGE_DATA,
MOCK_IMAGE_SUBENTRY_DATA_IMAGE_URL,
@@ -66,6 +68,7 @@ from .common import (
MOCK_SIREN_SUBENTRY_DATA,
MOCK_SWITCH_SUBENTRY_DATA,
MOCK_TEXT_SUBENTRY_DATA,
MOCK_TIME_SUBENTRY_DATA,
MOCK_VALVE_SUBENTRY_DATA_POSITION,
MOCK_VALVE_SUBENTRY_DATA_STATE,
MOCK_WATER_HEATER_SUBENTRY_DATA,
@@ -3163,6 +3166,65 @@ async def test_migrate_of_incompatible_config_entry(
"Milk notifier Blind",
id="cover",
),
pytest.param(
MOCK_DATE_SUBENTRY_DATA,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{"name": "Delivery day"},
{},
(),
{
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"retain": False,
},
(
(
{"command_topic": "test-topic#invalid"},
{"command_topic": "invalid_publish_topic"},
),
(
{
"command_topic": "test-topic",
"state_topic": "test-topic#invalid",
},
{"state_topic": "invalid_subscribe_topic"},
),
),
"Milk notifier Delivery day",
id="date",
),
pytest.param(
MOCK_DATETIME_SUBENTRY_DATA,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{"name": "Maintenance service"},
{},
(),
{
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"timezone": "GMT",
"retain": False,
},
(
(
{"command_topic": "test-topic#invalid"},
{"command_topic": "invalid_publish_topic"},
),
(
{
"command_topic": "test-topic",
"state_topic": "test-topic#invalid",
},
{"state_topic": "invalid_subscribe_topic"},
),
),
"Milk notifier Maintenance service",
id="datetime",
),
pytest.param(
MOCK_FAN_SUBENTRY_DATA,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
@@ -3850,6 +3912,35 @@ async def test_migrate_of_incompatible_config_entry(
"Milk notifier MOTD",
id="text",
),
pytest.param(
MOCK_TIME_SUBENTRY_DATA,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{"name": "Happy hour"},
{},
(),
{
"command_topic": "test-topic",
"command_template": "{{ value }}",
"state_topic": "test-topic",
"value_template": "{{ value_json.value }}",
"retain": False,
},
(
(
{"command_topic": "test-topic#invalid"},
{"command_topic": "invalid_publish_topic"},
),
(
{
"command_topic": "test-topic",
"state_topic": "test-topic#invalid",
},
{"state_topic": "invalid_subscribe_topic"},
),
),
"Milk notifier Happy hour",
id="time",
),
pytest.param(
MOCK_VALVE_SUBENTRY_DATA_STATE,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},