Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
64 changes: 62 additions & 2 deletions homeassistant/components/mqtt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@
from homeassistant import config_entries
from homeassistant.const import (
CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME,
CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP)
CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, CONF_TYPE, CONF_NAME,
CONF_DEVICE)
from homeassistant.core import Event, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, device_registry

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'homeassistant.helpers.device_registry' imported but unused

from homeassistant.helpers import template
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import (
Expand Down Expand Up @@ -73,6 +74,13 @@
CONF_QOS = 'qos'
CONF_RETAIN = 'retain'

CONF_IDENTIFIERS = 'identifiers'
CONF_CONNECTIONS = 'connections'
CONF_IDENTIFIER = 'identifier'
CONF_MANUFACTURER = 'manufacturer'
CONF_MODEL = 'model'
CONF_SW_VERSION = 'sw_version'

PROTOCOL_31 = '3.1'
PROTOCOL_311 = '3.1.1'

Expand Down Expand Up @@ -144,6 +152,15 @@ def valid_publish_topic(value: Any) -> str:
return value


def validate_device_has_at_least_one_identifier(value: ConfigType) -> \
ConfigType:
"""Validate that a device info entry has at least one identifying value."""
if not value.get(CONF_IDENTIFIERS) and not value.get(CONF_CONNECTIONS):
raise vol.Invalid("Device must has at least one identifying value in"
"'identifiers' and/or 'connections'")
return value


_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))

CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \
Expand Down Expand Up @@ -198,6 +215,20 @@ def valid_publish_topic(value: Any) -> str:
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
})

MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(vol.Schema({
vol.Optional(CONF_IDENTIFIERS, default=list):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CONNECTIONS, default=list):
vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_TYPE): vol.Any(*device_registry.CONNECTION_KEYS),
vol.Required(CONF_IDENTIFIER): cv.string,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't really like the name of this option (identifier) in this context. Its name (base off of https://developers.home-assistant.io/docs/en/device_registry_index.html) is very close to the identifiers option but it has a totally different format. Should this be renamed to value or should it rather stick to the dev docs' identifier?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this a dict to begin with?

@OttoWinter OttoWinter Oct 4, 2018

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Well primarily to make it more consistent with the device registry naming. One option would be:

{
  ...
  "device": {
    "connections": {
        "mac": "F7:19:18:29:93:1A",
        "other": "connection_id"
    }
  }
}

but that would for example not support devices with multiple MAC addresses (probably doesn't come up too often, but I think it's good to support it from the start to prevent breaking changes).

The other option would be:

{
  ...
  "device": {
    "connections": [
      {"mac": "F7:19:18:29:93:1A"},
      {"mac": "E8:2A:29:3A:A4:2B"}
    ]
  }
}

or:

{
  ...
  "device": {
    "connections": [
      ["mac", "F7:19:18:29:93:1A"],
      ["mac", "E8:2A:29:3A:A4:2B"]
    ]
  }
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like the last one. That is also how we store it internally.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok, updated to that.

})]),
vol.Optional(CONF_MANUFACTURER): cv.string,
vol.Optional(CONF_MODEL): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_SW_VERSION): cv.string,
}), validate_device_has_at_least_one_identifier)
Comment thread
MartinHjelmare marked this conversation as resolved.

MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE)

# Sensor type platforms subscribe to MQTT events
Expand Down Expand Up @@ -868,3 +899,32 @@ def discovery_callback(payload):
self.hass,
MQTT_DISCOVERY_UPDATED.format(self._discovery_hash),
discovery_callback)


class MqttEntityDeviceInfo(Entity):
"""Mixin used for mqtt platforms that support the device registry."""

def __init__(self, device_config: Optional[ConfigType]) -> None:
"""Initialize the device mixin."""
self._device_config = device_config

@property
def device_info(self):
"""Return a device description for device registry."""
if not self._device_config:
return None

return {
'identifiers': {
(DOMAIN, id_)
Comment thread
OttoWinter marked this conversation as resolved.
for id_ in self._device_config[CONF_IDENTIFIERS]
},
'connections': {
(connection[CONF_TYPE], connection[CONF_IDENTIFIER])
for connection in self._device_config[CONF_CONNECTIONS]
},
'manufacturer': self._device_config.get(CONF_MANUFACTURER),
'model': self._device_config.get(CONF_MODEL),
'name': self._device_config.get(CONF_NAME),
'sw_version': self._device_config.get(CONF_SW_VERSION),
}
12 changes: 8 additions & 4 deletions homeassistant/components/sensor/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability, MqttDiscoveryUpdate)
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN,
CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS)
CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS, CONF_DEVICE)
from homeassistant.helpers.entity import Entity
from homeassistant.components import mqtt
import homeassistant.helpers.config_validation as cv
Expand Down Expand Up @@ -51,6 +51,7 @@
# Integrations shouldn't never expose unique_id through configuration
# this here is an exception because MQTT is a msg transport, not a protocol
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)


Expand Down Expand Up @@ -95,22 +96,25 @@ async def _async_setup_entity(hass: HomeAssistantType, config: ConfigType,
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_DEVICE),
discovery_hash,
)])


class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, Entity):
class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
Entity):
"""Representation of a sensor that can be updated using MQTT."""

def __init__(self, name, state_topic, qos, unit_of_measurement,
force_update, expire_after, icon, device_class: Optional[str],
value_template, json_attributes, unique_id: Optional[str],
availability_topic, payload_available, payload_not_available,
discovery_hash):
device_config: Optional[ConfigType], discovery_hash):
"""Initialize the sensor."""
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
MqttEntityDeviceInfo.__init__(self, device_config)
self._state = STATE_UNKNOWN
self._name = name
self._state_topic = state_topic
Expand Down
1 change: 1 addition & 0 deletions homeassistant/helpers/device_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

CONNECTION_NETWORK_MAC = 'mac'
CONNECTION_ZIGBEE = 'zigbee'
CONNECTION_KEYS = [CONNECTION_NETWORK_MAC, CONNECTION_ZIGBEE]
Comment thread
OttoWinter marked this conversation as resolved.
Outdated


@attr.s(slots=True, frozen=True)
Expand Down
49 changes: 49 additions & 0 deletions tests/components/mqtt/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,55 @@ def test_validate_publish_topic(self):
# Topic names beginning with $ SHOULD NOT be used, but can
mqtt.valid_publish_topic('$SYS/')

def test_entity_device_info_schema(self):
"""Test MQTT entity device info validation."""
# just identifier
mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({
'identifiers': ['abcd']
})
# just connection
mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({
'connections': [{
'type': 'mac',
'identifier': '02:5b:26:a8:dc:12'
}]
})
# full device info
mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({
'identifiers': ['helloworld', 'hello'],
'connections': [{
"type": "mac",
"identifier": "02:5b:26:a8:dc:12",
}, {
"type": "zigbee",
"identifier": "zigbee_id"
}],
'manufacturer': 'Whatever',
'name': 'Beer',
'model': 'Glass',
'sw_version': '0.1-beta',
})
# no identifiers
self.assertRaises(vol.Invalid, mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, {
'manufacturer': 'Whatever',
'name': 'Beer',
'model': 'Glass',
'sw_version': '0.1-beta',
})
# empty identifiers
self.assertRaises(vol.Invalid, mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, {
'identifiers': [],
'connections': [],
'name': 'Beer',
})
# invalid connection type
self.assertRaises(vol.Invalid, mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, {
'connections': [{
'type': 'invalid',
'identifier': 'empty'
}]
})


# pylint: disable=invalid-name
class TestMQTTCallbacks(unittest.TestCase):
Expand Down
40 changes: 40 additions & 0 deletions tests/components/sensor/test_mqtt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The tests for the MQTT sensor platform."""
import json
import unittest

from datetime import timedelta, datetime
Expand Down Expand Up @@ -411,3 +412,42 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog):
await hass.async_block_till_done()
state = hass.states.get('sensor.beer')
assert state is None


async def test_entity_device_info_with_identifier(hass, mqtt_mock):
"""Test MQTT sensor device registry integration."""
entry = MockConfigEntry(domain=mqtt.DOMAIN)
entry.add_to_hass(hass)
await async_start(hass, 'homeassistant', {}, entry)
registry = await hass.helpers.device_registry.async_get_registry()

data = json.dumps({
'platform': 'mqtt',
'name': 'Test 1',
'state_topic': 'test-topic',
'device': {
'identifiers': ['helloworld'],
'connections': [{
"type": "mac",
"identifier": "02:5b:26:a8:dc:12",
}],
'manufacturer': 'Whatever',
'name': 'Beer',
'model': 'Glass',
'sw_version': '0.1-beta',
},
'unique_id': 'veryunique'
})
async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config',
data)
await hass.async_block_till_done()
await hass.async_block_till_done()

device = registry.async_get_device({('mqtt', 'helloworld')}, set())
assert device is not None
assert device.identifiers == {('mqtt', 'helloworld')}
assert device.connections == {('mac', "02:5b:26:a8:dc:12")}
Comment thread
OttoWinter marked this conversation as resolved.
assert device.manufacturer == 'Whatever'
assert device.name == 'Beer'
assert device.model == 'Glass'
assert device.sw_version == '0.1-beta'