1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00

Add MQTT object_id option (#58728)

* Add MQTT object_id option

* Add MQTT object_id option

* Add MQTT object_id option

* Add MQTT object_id option - Fix light and vacuum

* Add MQTT object_id option - Fix light and vacuum

* Add MQTT object_id option - Fix lock

* Add MQTT object_id option - Fix device

* Add MQTT object_id option - Fix device

* Update tests/components/mqtt/test_discovery.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Change deprecated method

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Damien Duboeuf 2021-11-08 14:02:18 +01:00 committed by GitHub
parent 5151c4d99b
commit 67c2747027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 232 additions and 6 deletions

View File

@ -19,6 +19,7 @@ from .const import ( # noqa: F401
CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
DOMAIN,
ENTITY_ID_FORMAT,
SOURCE_TYPE_BLUETOOTH,
SOURCE_TYPE_BLUETOOTH_LE,
SOURCE_TYPE_GPS,

View File

@ -6,6 +6,7 @@ from typing import Final
LOGGER: Final = logging.getLogger(__package__)
DOMAIN: Final = "device_tracker"
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
PLATFORM_TYPE_LEGACY: Final = "legacy"
PLATFORM_TYPE_ENTITY: Final = "entity_platform"

View File

@ -47,6 +47,8 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
ENTITY_ID_FORMAT = DOMAIN + ".{}"
DEVICE_CLASSES = [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))

View File

@ -97,6 +97,7 @@ ABBREVIATIONS = {
"mode_stat_tpl": "mode_state_template",
"modes": "modes",
"name": "name",
"obj_id": "object_id",
"off_dly": "off_delay",
"on_cmd_type": "on_command_type",
"ops": "options",

View File

@ -126,6 +126,7 @@ async def _async_setup_entity(
class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
"""Representation of a MQTT alarm status."""
_entity_id_format = alarm.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -87,6 +87,8 @@ async def _async_setup_entity(
class MqttBinarySensor(MqttEntity, BinarySensorEntity):
"""Representation a binary sensor that is updated by MQTT."""
_entity_id_format = binary_sensor.ENTITY_ID_FORMAT
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT binary sensor."""
self._state = None

View File

@ -66,6 +66,7 @@ async def _async_setup_entity(
class MqttCamera(MqttEntity, Camera):
"""representation of a MQTT camera."""
_entity_id_format = camera.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_CAMERA_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -297,6 +297,7 @@ async def _async_setup_entity(
class MqttClimate(MqttEntity, ClimateEntity):
"""Representation of an MQTT climate device."""
_entity_id_format = climate.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -224,6 +224,7 @@ async def _async_setup_entity(
class MqttCover(MqttEntity, CoverEntity):
"""Representation of a cover that can be controlled using MQTT."""
_entity_id_format = cover.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_COVER_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -58,6 +58,8 @@ async def _async_setup_entity(
class MqttDeviceTracker(MqttEntity, TrackerEntity):
"""Representation of a device tracker using MQTT."""
_entity_id_format = device_tracker.ENTITY_ID_FORMAT
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the tracker."""
self._location_name = None

View File

@ -230,6 +230,7 @@ async def _async_setup_entity(
class MqttFan(MqttEntity, FanEntity):
"""A MQTT fan component."""
_entity_id_format = fan.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -163,6 +163,7 @@ async def _async_setup_entity(
class MqttHumidifier(MqttEntity, HumidifierEntity):
"""A MQTT humidifier component."""
_entity_id_format = humidifier.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -29,6 +29,7 @@ from homeassistant.components.light import (
COLOR_MODE_UNKNOWN,
COLOR_MODE_WHITE,
COLOR_MODE_XY,
ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
@ -239,6 +240,7 @@ async def async_setup_entity_basic(
class MqttLight(MqttEntity, LightEntity, RestoreEntity):
"""Representation of a MQTT light."""
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -24,6 +24,7 @@ from homeassistant.components.light import (
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_XY,
ENTITY_ID_FORMAT,
FLASH_LONG,
FLASH_SHORT,
SUPPORT_BRIGHTNESS,
@ -167,6 +168,7 @@ async def async_setup_entity_json(
class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
"""Representation of a MQTT JSON light."""
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -11,6 +11,7 @@ from homeassistant.components.light import (
ATTR_HS_COLOR,
ATTR_TRANSITION,
ATTR_WHITE_VALUE,
ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
@ -97,6 +98,7 @@ async def async_setup_entity_template(
class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
"""Representation of a MQTT Template light."""
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -78,6 +78,7 @@ async def _async_setup_entity(
class MqttLock(MqttEntity, LockEntity):
"""Representation of a lock that can be toggled using MQTT."""
_entity_id_format = lock.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -28,7 +28,12 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA, DeviceInfo, Entity
from homeassistant.helpers.entity import (
ENTITY_CATEGORIES_SCHEMA,
DeviceInfo,
Entity,
async_generate_entity_id,
)
from homeassistant.helpers.typing import ConfigType
from . import DATA_MQTT, debug_info, publish, subscription
@ -82,6 +87,7 @@ CONF_VIA_DEVICE = "via_device"
CONF_DEPRECATED_VIA_HUB = "via_hub"
CONF_SUGGESTED_AREA = "suggested_area"
CONF_CONFIGURATION_URL = "configuration_url"
CONF_OBJECT_ID = "object_id"
MQTT_ATTRIBUTES_BLOCKED = {
"assumed_state",
@ -183,6 +189,7 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend(
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
vol.Optional(CONF_OBJECT_ID): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
@ -210,6 +217,14 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema):
)
def init_entity_id_from_config(hass, entity, config, entity_id_format):
"""Set entity_id from object_id if defined in config."""
if CONF_OBJECT_ID in config:
entity.entity_id = async_generate_entity_id(
entity_id_format, config[CONF_OBJECT_ID], None, hass
)
class MqttAttributes(Entity):
"""Mixin used for platforms that support JSON attributes."""
@ -588,6 +603,8 @@ class MqttEntity(
):
"""Representation of an MQTT entity."""
_entity_id_format: str
def __init__(self, hass, config, config_entry, discovery_data):
"""Init the MQTT Entity."""
self.hass = hass
@ -598,12 +615,21 @@ class MqttEntity(
# Load config
self._setup_from_config(self._config)
# Initialize entity_id from config
self._init_entity_id()
# Initialize mixin classes
MqttAttributes.__init__(self, config)
MqttAvailability.__init__(self, config)
MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update)
MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry)
def _init_entity_id(self):
"""Set entity_id from object_id if defined in config."""
init_entity_id_from_config(
self.hass, self, self._config, self._entity_id_format
)
async def async_added_to_hass(self):
"""Subscribe mqtt events."""
await super().async_added_to_hass()

View File

@ -114,6 +114,7 @@ async def _async_setup_entity(
class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
"""representation of an MQTT number."""
_entity_id_format = number.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_NUMBER_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -15,10 +15,12 @@ from . import PLATFORMS
from .. import mqtt
from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN
from .mixins import (
CONF_OBJECT_ID,
MQTT_AVAILABILITY_SCHEMA,
MqttAvailability,
MqttDiscoveryUpdate,
async_setup_entry_helper,
init_entity_id_from_config,
)
DEFAULT_NAME = "MQTT Scene"
@ -32,6 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
vol.Optional(CONF_PAYLOAD_ON): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_OBJECT_ID): cv.string,
}
).extend(MQTT_AVAILABILITY_SCHEMA.schema)
@ -43,22 +46,22 @@ async def async_setup_platform(
):
"""Set up MQTT scene through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(async_add_entities, config)
await _async_setup_entity(hass, async_add_entities, config)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT scene dynamically through MQTT discovery."""
setup = functools.partial(
_async_setup_entity, async_add_entities, config_entry=config_entry
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
await async_setup_entry_helper(hass, scene.DOMAIN, setup, DISCOVERY_SCHEMA)
async def _async_setup_entity(
async_add_entities, config, config_entry=None, discovery_data=None
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
"""Set up the MQTT scene."""
async_add_entities([MqttScene(config, config_entry, discovery_data)])
async_add_entities([MqttScene(hass, config, config_entry, discovery_data)])
class MqttScene(
@ -68,8 +71,11 @@ class MqttScene(
):
"""Representation of a scene that can be activated using MQTT."""
def __init__(self, config, config_entry, discovery_data):
_entity_id_format = scene.DOMAIN + ".{}"
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT scene."""
self.hass = hass
self._state = False
self._sub_state = None
@ -78,9 +84,18 @@ class MqttScene(
# Load config
self._setup_from_config(config)
# Initialize entity_id from config
self._init_entity_id()
MqttAvailability.__init__(self, config)
MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update)
def _init_entity_id(self):
"""Set entity_id from object_id if defined in config."""
init_entity_id_from_config(
self.hass, self, self._config, self._entity_id_format
)
async def async_added_to_hass(self):
"""Subscribe to MQTT events."""
await super().async_added_to_hass()

View File

@ -90,6 +90,8 @@ async def _async_setup_entity(
class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
"""representation of an MQTT select."""
_entity_id_format = select.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_SELECT_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -11,6 +11,7 @@ from homeassistant.components import sensor
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA,
ENTITY_ID_FORMAT,
STATE_CLASSES_SCHEMA,
SensorEntity,
)
@ -132,6 +133,7 @@ async def _async_setup_entity(
class MqttSensor(MqttEntity, SensorEntity):
"""Representation of a sensor that can be updated using MQTT."""
_entity_id_format = ENTITY_ID_FORMAT
_attr_last_reset = None
_attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED

View File

@ -84,6 +84,7 @@ async def _async_setup_entity(
class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity):
"""Representation of a switch that can be toggled using MQTT."""
_entity_id_format = switch.ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_SWITCH_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -5,6 +5,7 @@ import voluptuous as vol
from homeassistant.components.vacuum import (
ATTR_STATUS,
ENTITY_ID_FORMAT,
SUPPORT_BATTERY,
SUPPORT_CLEAN_SPOT,
SUPPORT_FAN_SPEED,
@ -169,6 +170,7 @@ async def async_setup_entity_legacy(
class MqttVacuum(MqttEntity, VacuumEntity):
"""Representation of a MQTT-controlled legacy vacuum."""
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -4,6 +4,7 @@ import json
import voluptuous as vol
from homeassistant.components.vacuum import (
ENTITY_ID_FORMAT,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
@ -149,6 +150,7 @@ async def async_setup_entity_state(
class MqttStateVacuum(MqttEntity, StateVacuumEntity):
"""Representation of a MQTT-controlled state vacuum."""
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
def __init__(self, hass, config, config_entry, discovery_data):

View File

@ -40,6 +40,7 @@ from homeassistant.loader import bind_hass
_LOGGER = logging.getLogger(__name__)
DOMAIN = "vacuum"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = timedelta(seconds=20)
ATTR_BATTERY_ICON = "battery_icon"

View File

@ -191,6 +191,158 @@ async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog):
assert ("alarm_control_panel", "bla") in hass.data[ALREADY_DISCOVERED]
@pytest.mark.parametrize(
"topic, config, entity_id, name, domain",
[
(
"homeassistant/alarm_control_panel/object/bla/config",
'{ "name": "Hello World 1", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"alarm_control_panel.hello_id",
"Hello World 1",
"alarm_control_panel",
),
(
"homeassistant/binary_sensor/object/bla/config",
'{ "name": "Hello World 2", "obj_id": "hello_id", "state_topic": "test-topic" }',
"binary_sensor.hello_id",
"Hello World 2",
"binary_sensor",
),
(
"homeassistant/camera/object/bla/config",
'{ "name": "Hello World 3", "obj_id": "hello_id", "state_topic": "test-topic", "topic": "test-topic" }',
"camera.hello_id",
"Hello World 3",
"camera",
),
(
"homeassistant/climate/object/bla/config",
'{ "name": "Hello World 4", "obj_id": "hello_id", "state_topic": "test-topic" }',
"climate.hello_id",
"Hello World 4",
"climate",
),
(
"homeassistant/cover/object/bla/config",
'{ "name": "Hello World 5", "obj_id": "hello_id", "state_topic": "test-topic" }',
"cover.hello_id",
"Hello World 5",
"cover",
),
(
"homeassistant/fan/object/bla/config",
'{ "name": "Hello World 6", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"fan.hello_id",
"Hello World 6",
"fan",
),
(
"homeassistant/humidifier/object/bla/config",
'{ "name": "Hello World 7", "obj_id": "hello_id", "state_topic": "test-topic", "target_humidity_command_topic": "test-topic", "command_topic": "test-topic" }',
"humidifier.hello_id",
"Hello World 7",
"humidifier",
),
(
"homeassistant/number/object/bla/config",
'{ "name": "Hello World 8", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"number.hello_id",
"Hello World 8",
"number",
),
(
"homeassistant/scene/object/bla/config",
'{ "name": "Hello World 9", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"scene.hello_id",
"Hello World 9",
"scene",
),
(
"homeassistant/select/object/bla/config",
'{ "name": "Hello World 10", "obj_id": "hello_id", "state_topic": "test-topic", "options": [ "opt1", "opt2" ], "command_topic": "test-topic" }',
"select.hello_id",
"Hello World 10",
"select",
),
(
"homeassistant/sensor/object/bla/config",
'{ "name": "Hello World 11", "obj_id": "hello_id", "state_topic": "test-topic" }',
"sensor.hello_id",
"Hello World 11",
"sensor",
),
(
"homeassistant/switch/object/bla/config",
'{ "name": "Hello World 12", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"switch.hello_id",
"Hello World 12",
"switch",
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 13", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"light.hello_id",
"Hello World 13",
"light",
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 14", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic", "schema": "json" }',
"light.hello_id",
"Hello World 14",
"light",
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 15", "obj_id": "hello_id", "state_topic": "test-topic", "command_off_template": "template", "command_on_template": "template", "command_topic": "test-topic", "schema": "template" }',
"light.hello_id",
"Hello World 15",
"light",
),
(
"homeassistant/vacuum/object/bla/config",
'{ "name": "Hello World 16", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "state" }',
"vacuum.hello_id",
"Hello World 16",
"vacuum",
),
(
"homeassistant/vacuum/object/bla/config",
'{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "legacy" }',
"vacuum.hello_id",
"Hello World 17",
"vacuum",
),
(
"homeassistant/lock/object/bla/config",
'{ "name": "Hello World 18", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"lock.hello_id",
"Hello World 18",
"lock",
),
(
"homeassistant/device_tracker/object/bla/config",
'{ "name": "Hello World 19", "obj_id": "hello_id", "state_topic": "test-topic" }',
"device_tracker.hello_id",
"Hello World 19",
"device_tracker",
),
],
)
async def test_discovery_with_object_id(
hass, mqtt_mock, caplog, topic, config, entity_id, name, domain
):
"""Test discovering an MQTT entity with object_id."""
async_fire_mqtt_message(hass, topic, config)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert state.name == name
assert (domain, "object bla") in hass.data[ALREADY_DISCOVERED]
async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog):
"""Test sending in correct JSON with optional node_id included."""
async_fire_mqtt_message(