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 device registry to MQTT climate #19332

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 18 additions & 5 deletions homeassistant/components/climate/mqtt.py
Expand Up @@ -18,12 +18,13 @@
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE)
ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_ON,
STATE_OFF)
from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate,
subscription)
MqttEntityDeviceInfo, subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
Expand Down Expand Up @@ -78,6 +79,8 @@
CONF_MAX_TEMP = 'max_temp'
CONF_TEMP_STEP = 'temp_step'

CONF_UNIQUE_ID = 'unique_id'

TEMPLATE_KEYS = (
CONF_POWER_STATE_TEMPLATE,
CONF_MODE_STATE_TEMPLATE,
Expand Down Expand Up @@ -139,8 +142,9 @@

vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float)

vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float),
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 @@ -174,12 +178,14 @@ async def _async_setup_entity(hass, config, async_add_entities,
)])


class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
ClimateDevice):
"""Representation of an MQTT climate device."""

def __init__(self, hass, config, discovery_hash):
"""Initialize the climate device."""
self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
self._sub_state = None

self.hass = hass
Expand All @@ -201,11 +207,13 @@ def __init__(self, hass, config, discovery_hash):
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE)

MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update)
MqttEntityDeviceInfo.__init__(self, device_config)

async def async_added_to_hass(self):
"""Handle being added to home assistant."""
Expand Down Expand Up @@ -467,6 +475,11 @@ def name(self):
"""Return the name of the climate device."""
return self._config.get(CONF_NAME)

@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id

@property
def temperature_unit(self):
"""Return the unit of measurement."""
Expand Down
108 changes: 104 additions & 4 deletions tests/components/climate/test_mqtt.py
@@ -1,6 +1,8 @@
"""The tests for the mqtt climate component."""
import unittest
import copy
import json
import unittest
from unittest.mock import ANY

import pytest
import voluptuous as vol
Expand All @@ -16,9 +18,10 @@
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE,
SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.components.mqtt.discovery import async_start
from tests.common import (get_test_home_assistant, mock_mqtt_component,
async_fire_mqtt_message, fire_mqtt_message,
mock_component, MockConfigEntry)
from tests.common import (
async_fire_mqtt_message, async_mock_mqtt_component, async_setup_component,
fire_mqtt_message, get_test_home_assistant, mock_component,
mock_mqtt_component, MockConfigEntry, mock_registry)
from tests.components.climate import common

ENTITY_CLIMATE = 'climate.test'
Expand Down Expand Up @@ -671,6 +674,29 @@ def test_temp_step_custom(self):
assert 0.01 == temp_step


async def test_unique_id(hass):
"""Test unique id option only creates one climate per unique_id."""
await async_mock_mqtt_component(hass)
assert await async_setup_component(hass, climate.DOMAIN, {
climate.DOMAIN: [{
'platform': 'mqtt',
'name': 'Test 1',
'status_topic': 'test-topic',
'command_topic': 'test_topic',
'unique_id': 'TOTALLY_UNIQUE'
}, {
'platform': 'mqtt',
'name': 'Test 2',
'status_topic': 'test-topic',
'command_topic': 'test_topic',
'unique_id': 'TOTALLY_UNIQUE'
}]
})
async_fire_mqtt_message(hass, 'test-topic', 'payload')
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(climate.DOMAIN)) == 1


async def test_discovery_removal_climate(hass, mqtt_mock, caplog):
"""Test removal of discovered climate."""
entry = MockConfigEntry(domain=mqtt.DOMAIN)
Expand Down Expand Up @@ -721,3 +747,77 @@ async def test_discovery_update_climate(hass, mqtt_mock, caplog):

state = hass.states.get('climate.milk')
assert state is None


async def test_entity_device_info_with_identifier(hass, mqtt_mock):
"""Test MQTT climate 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',
'command_topic': 'test-topic',
'device': {
'identifiers': ['helloworld'],
'connections': [
["mac", "02:5b:26:a8:dc:12"],
Copy link
Member

Choose a reason for hiding this comment

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

Funny how my macbook's MAC-address is now all over the HA codebase 😆

],
'manufacturer': 'Whatever',
'name': 'Beer',
'model': 'Glass',
'sw_version': '0.1-beta',
},
'unique_id': 'veryunique'
})
async_fire_mqtt_message(hass, 'homeassistant/climate/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")}
assert device.manufacturer == 'Whatever'
assert device.name == 'Beer'
assert device.model == 'Glass'
assert device.sw_version == '0.1-beta'


async def test_entity_id_update(hass, mqtt_mock):
"""Test MQTT subscriptions are managed when entity_id is updated."""
registry = mock_registry(hass, {})
mock_mqtt = await async_mock_mqtt_component(hass)
assert await async_setup_component(hass, climate.DOMAIN, {
climate.DOMAIN: [{
'platform': 'mqtt',
'name': 'beer',
'mode_state_topic': 'test-topic',
'availability_topic': 'avty-topic',
'unique_id': 'TOTALLY_UNIQUE'
}]
})

state = hass.states.get('climate.beer')
assert state is not None
assert mock_mqtt.async_subscribe.call_count == 2
mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8')
mock_mqtt.async_subscribe.assert_any_call('avty-topic', ANY, 0, 'utf-8')
mock_mqtt.async_subscribe.reset_mock()

registry.async_update_entity('climate.beer', new_entity_id='climate.milk')
await hass.async_block_till_done()
await hass.async_block_till_done()

state = hass.states.get('climate.beer')
assert state is None

state = hass.states.get('climate.milk')
assert state is not None
assert mock_mqtt.async_subscribe.call_count == 2
mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8')
mock_mqtt.async_subscribe.assert_any_call('avty-topic', ANY, 0, 'utf-8')