Add Tasmota sensor (#41483)

* Add Tasmota sensor

* Remove useless try-except

* Bump hatasmota to 0.0.11

* Apply suggestions from code review

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Sort dict constants

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Erik Montnemery 2020-10-12 07:27:06 +02:00 committed by GitHub
parent 59edb25e4e
commit 84cb00bb4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 604 additions and 36 deletions

View file

@ -11,7 +11,9 @@ from hatasmota.discovery import (
unique_id_from_hash, unique_id_from_hash,
) )
import homeassistant.components.sensor as sensor
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN from .const import DOMAIN
@ -117,8 +119,38 @@ async def async_start(
for (tasmota_entity_config, discovery_hash) in tasmota_entities: for (tasmota_entity_config, discovery_hash) in tasmota_entities:
await _discover_entity(tasmota_entity_config, discovery_hash, platform) await _discover_entity(tasmota_entity_config, discovery_hash, platform)
async def async_sensors_discovered(sensors, mac):
"""Handle discovery of (additional) sensors."""
platform = sensor.DOMAIN
await _load_platform(platform)
device_registry = await hass.helpers.device_registry.async_get_registry()
entity_registry = await hass.helpers.entity_registry.async_get_registry()
device = device_registry.async_get_device(set(), {("mac", mac)})
if device is None:
_LOGGER.warning("Got sensors for unknown device mac: %s", mac)
return
orphaned_entities = {
entry.unique_id
for entry in async_entries_for_device(entity_registry, device.id)
if entry.domain == sensor.DOMAIN and entry.platform == DOMAIN
}
for (tasmota_sensor_config, discovery_hash) in sensors:
if tasmota_sensor_config:
orphaned_entities.discard(tasmota_sensor_config.unique_id)
await _discover_entity(tasmota_sensor_config, discovery_hash, platform)
for unique_id in orphaned_entities:
entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
if entity_id:
_LOGGER.debug("Removing entity: %s %s", platform, entity_id)
entity_registry.async_remove(entity_id)
hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
hass.data[CONFIG_ENTRY_IS_SETUP] = set() hass.data[CONFIG_ENTRY_IS_SETUP] = set()
tasmota_discovery = TasmotaDiscovery(discovery_topic, tasmota_mqtt) tasmota_discovery = TasmotaDiscovery(discovery_topic, tasmota_mqtt)
await tasmota_discovery.start_discovery(async_device_discovered, None) await tasmota_discovery.start_discovery(
async_device_discovered, async_sensors_discovered
)

View file

@ -3,7 +3,7 @@
"name": "Tasmota (beta)", "name": "Tasmota (beta)",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tasmota", "documentation": "https://www.home-assistant.io/integrations/tasmota",
"requirements": ["hatasmota==0.0.10"], "requirements": ["hatasmota==0.0.11"],
"dependencies": ["mqtt"], "dependencies": ["mqtt"],
"mqtt": ["tasmota/discovery/#"], "mqtt": ["tasmota/discovery/#"],
"codeowners": ["@emontnemery"] "codeowners": ["@emontnemery"]

View file

@ -0,0 +1,161 @@
"""Support for Tasmota sensors."""
import logging
from typing import Optional
from hatasmota.const import (
SENSOR_AMBIENT,
SENSOR_APPARENT_POWERUSAGE,
SENSOR_BATTERY,
SENSOR_CCT,
SENSOR_CO2,
SENSOR_COLOR_BLUE,
SENSOR_COLOR_GREEN,
SENSOR_COLOR_RED,
SENSOR_CURRENT,
SENSOR_DEWPOINT,
SENSOR_DISTANCE,
SENSOR_ECO2,
SENSOR_FREQUENCY,
SENSOR_HUMIDITY,
SENSOR_ILLUMINANCE,
SENSOR_MOISTURE,
SENSOR_PB0_3,
SENSOR_PB0_5,
SENSOR_PB1,
SENSOR_PB2_5,
SENSOR_PB5,
SENSOR_PB10,
SENSOR_PM1,
SENSOR_PM2_5,
SENSOR_PM10,
SENSOR_POWERFACTOR,
SENSOR_POWERUSAGE,
SENSOR_PRESSURE,
SENSOR_PRESSUREATSEALEVEL,
SENSOR_PROXIMITY,
SENSOR_REACTIVE_POWERUSAGE,
SENSOR_TEMPERATURE,
SENSOR_TODAY,
SENSOR_TOTAL,
SENSOR_TOTAL_START_TIME,
SENSOR_TVOC,
SENSOR_VOLTAGE,
SENSOR_WEIGHT,
SENSOR_YESTERDAY,
)
from homeassistant.components import sensor
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN as TASMOTA_DOMAIN
from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
_LOGGER = logging.getLogger(__name__)
SENSOR_DEVICE_CLASS_MAP = {
SENSOR_AMBIENT: DEVICE_CLASS_ILLUMINANCE,
SENSOR_APPARENT_POWERUSAGE: DEVICE_CLASS_POWER,
SENSOR_BATTERY: DEVICE_CLASS_BATTERY,
SENSOR_HUMIDITY: DEVICE_CLASS_HUMIDITY,
SENSOR_ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE,
SENSOR_POWERUSAGE: DEVICE_CLASS_POWER,
SENSOR_PRESSURE: DEVICE_CLASS_PRESSURE,
SENSOR_PRESSUREATSEALEVEL: DEVICE_CLASS_PRESSURE,
SENSOR_REACTIVE_POWERUSAGE: DEVICE_CLASS_POWER,
SENSOR_TEMPERATURE: DEVICE_CLASS_TEMPERATURE,
SENSOR_TODAY: DEVICE_CLASS_POWER,
SENSOR_TOTAL: DEVICE_CLASS_POWER,
SENSOR_YESTERDAY: DEVICE_CLASS_POWER,
}
SENSOR_ICON_MAP = {
SENSOR_CCT: "mdi:temperature-kelvin",
SENSOR_CO2: "mdi:molecule-co2",
SENSOR_COLOR_BLUE: "mdi:palette",
SENSOR_COLOR_GREEN: "mdi:palette",
SENSOR_COLOR_RED: "mdi:palette",
SENSOR_CURRENT: "mdi:alpha-a-circle-outline",
SENSOR_DEWPOINT: "mdi:weather-rainy",
SENSOR_DISTANCE: "mdi:leak",
SENSOR_ECO2: "mdi:molecule-co2",
SENSOR_FREQUENCY: "mdi:current-ac",
SENSOR_MOISTURE: "mdi:cup-water",
SENSOR_PB0_3: "mdi:flask",
SENSOR_PB0_5: "mdi:flask",
SENSOR_PB10: "mdi:flask",
SENSOR_PB1: "mdi:flask",
SENSOR_PB2_5: "mdi:flask",
SENSOR_PB5: "mdi:flask",
SENSOR_PM10: "mdi:air-filter",
SENSOR_PM1: "mdi:air-filter",
SENSOR_PM2_5: "mdi:air-filter",
SENSOR_POWERFACTOR: "mdi:alpha-f-circle-outline",
SENSOR_PROXIMITY: "mdi:ruler",
SENSOR_TOTAL_START_TIME: "mdi:progress-clock",
SENSOR_TVOC: "mdi:air-filter",
SENSOR_VOLTAGE: "mdi:alpha-v-circle-outline",
SENSOR_WEIGHT: "mdi:scale",
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Tasmota sensor dynamically through discovery."""
async def async_discover_sensor(tasmota_entity, discovery_hash):
"""Discover and add a Tasmota sensor."""
async_add_entities(
[
TasmotaSensor(
tasmota_entity=tasmota_entity, discovery_hash=discovery_hash
)
]
)
async_dispatcher_connect(
hass,
TASMOTA_DISCOVERY_ENTITY_NEW.format(sensor.DOMAIN, TASMOTA_DOMAIN),
async_discover_sensor,
)
class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, Entity):
"""Representation of a Tasmota sensor."""
def __init__(self, **kwds):
"""Initialize the Tasmota sensor."""
self._state = False
super().__init__(
discovery_update=self.discovery_update,
**kwds,
)
@property
def device_class(self) -> Optional[str]:
"""Return the device class of the sensor."""
return SENSOR_DEVICE_CLASS_MAP.get(self._tasmota_entity.quantity)
@property
def icon(self):
"""Return the icon."""
return SENSOR_ICON_MAP.get(self._tasmota_entity.quantity)
@property
def state(self):
"""Return the state of the entity."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return self._tasmota_entity.unit

View file

@ -729,7 +729,7 @@ hass-nabucasa==0.37.0
hass_splunk==0.1.1 hass_splunk==0.1.1
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.0.10 hatasmota==0.0.11
# homeassistant.components.jewish_calendar # homeassistant.components.jewish_calendar
hdate==0.9.5 hdate==0.9.5

View file

@ -361,7 +361,7 @@ hangups==0.4.11
hass-nabucasa==0.37.0 hass-nabucasa==0.37.0
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.0.10 hatasmota==0.0.11
# homeassistant.components.jewish_calendar # homeassistant.components.jewish_calendar
hdate==0.9.5 hdate==0.9.5

View file

@ -57,7 +57,13 @@ DEFAULT_CONFIG = {
async def help_test_availability_when_connection_lost( async def help_test_availability_when_connection_lost(
hass, mqtt_client_mock, mqtt_mock, domain, config hass,
mqtt_client_mock,
mqtt_mock,
domain,
config,
sensor_config=None,
entity_id="test",
): ):
"""Test availability after MQTT disconnection. """Test availability after MQTT disconnection.
@ -69,6 +75,13 @@ async def help_test_availability_when_connection_lost(
json.dumps(config), json.dumps(config),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
# Device online # Device online
async_fire_mqtt_message( async_fire_mqtt_message(
@ -76,7 +89,8 @@ async def help_test_availability_when_connection_lost(
get_topic_tele_will(config), get_topic_tele_will(config),
config_get_state_online(config), config_get_state_online(config),
) )
state = hass.states.get(f"{domain}.test")
state = hass.states.get(f"{domain}.{entity_id}")
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
# Disconnected from MQTT server -> state changed to unavailable # Disconnected from MQTT server -> state changed to unavailable
@ -85,7 +99,7 @@ async def help_test_availability_when_connection_lost(
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# Reconnected to MQTT server -> state still unavailable # Reconnected to MQTT server -> state still unavailable
@ -94,7 +108,7 @@ async def help_test_availability_when_connection_lost(
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# Receive LWT again # Receive LWT again
@ -103,7 +117,7 @@ async def help_test_availability_when_connection_lost(
get_topic_tele_will(config), get_topic_tele_will(config),
config_get_state_online(config), config_get_state_online(config),
) )
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
@ -112,6 +126,8 @@ async def help_test_availability(
mqtt_mock, mqtt_mock,
domain, domain,
config, config,
sensor_config=None,
entity_id="test",
): ):
"""Test availability. """Test availability.
@ -123,8 +139,15 @@ async def help_test_availability(
json.dumps(config), json.dumps(config),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
async_fire_mqtt_message( async_fire_mqtt_message(
@ -133,7 +156,7 @@ async def help_test_availability(
config_get_state_online(config), config_get_state_online(config),
) )
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
async_fire_mqtt_message( async_fire_mqtt_message(
@ -142,7 +165,7 @@ async def help_test_availability(
config_get_state_offline(config), config_get_state_offline(config),
) )
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
@ -151,6 +174,8 @@ async def help_test_availability_discovery_update(
mqtt_mock, mqtt_mock,
domain, domain,
config, config,
sensor_config=None,
entity_id="test",
): ):
"""Test update of discovered TasmotaAvailability. """Test update of discovered TasmotaAvailability.
@ -180,16 +205,23 @@ async def help_test_availability_discovery_update(
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config1[CONF_MAC]}/config", data1) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config1[CONF_MAC]}/config", data1)
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(hass, availability_topic1, online1) async_fire_mqtt_message(hass, availability_topic1, online1)
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
async_fire_mqtt_message(hass, availability_topic1, offline1) async_fire_mqtt_message(hass, availability_topic1, offline1)
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# Change availability settings # Change availability settings
@ -200,17 +232,24 @@ async def help_test_availability_discovery_update(
async_fire_mqtt_message(hass, availability_topic1, online1) async_fire_mqtt_message(hass, availability_topic1, online1)
async_fire_mqtt_message(hass, availability_topic1, online2) async_fire_mqtt_message(hass, availability_topic1, online2)
async_fire_mqtt_message(hass, availability_topic2, online1) async_fire_mqtt_message(hass, availability_topic2, online1)
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# Verify we are subscribing to the new topic # Verify we are subscribing to the new topic
async_fire_mqtt_message(hass, availability_topic2, online2) async_fire_mqtt_message(hass, availability_topic2, online2)
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
async def help_test_availability_poll_state( async def help_test_availability_poll_state(
hass, mqtt_client_mock, mqtt_mock, domain, config, poll_topic, poll_payload hass,
mqtt_client_mock,
mqtt_mock,
domain,
config,
poll_topic,
poll_payload,
sensor_config=None,
): ):
"""Test polling of state when device is available. """Test polling of state when device is available.
@ -222,6 +261,13 @@ async def help_test_availability_poll_state(
json.dumps(config), json.dumps(config),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
mqtt_mock.async_publish.reset_mock() mqtt_mock.async_publish.reset_mock()
# Device online, verify poll for state # Device online, verify poll for state
@ -265,7 +311,16 @@ async def help_test_availability_poll_state(
async def help_test_discovery_removal( async def help_test_discovery_removal(
hass, mqtt_mock, caplog, domain, config1, config2 hass,
mqtt_mock,
caplog,
domain,
config1,
config2,
sensor_config1=None,
sensor_config2=None,
entity_id="test",
name="Test",
): ):
"""Test removal of discovered entity.""" """Test removal of discovered entity."""
device_reg = await hass.helpers.device_registry.async_get_registry() device_reg = await hass.helpers.device_registry.async_get_registry()
@ -277,34 +332,56 @@ async def help_test_discovery_removal(
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config1[CONF_MAC]}/config", data1) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config1[CONF_MAC]}/config", data1)
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config1:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config1[CONF_MAC]}/sensors",
json.dumps(sensor_config1),
)
await hass.async_block_till_done()
# Verify device and entity registry entries are created # Verify device and entity registry entries are created
device_entry = device_reg.async_get_device(set(), {("mac", config1[CONF_MAC])}) device_entry = device_reg.async_get_device(set(), {("mac", config1[CONF_MAC])})
assert device_entry is not None assert device_entry is not None
entity_entry = entity_reg.async_get(f"{domain}.test") entity_entry = entity_reg.async_get(f"{domain}.{entity_id}")
assert entity_entry is not None assert entity_entry is not None
# Verify state is added # Verify state is added
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state is not None assert state is not None
assert state.name == "Test" assert state.name == name
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config2[CONF_MAC]}/config", data2) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config2[CONF_MAC]}/config", data2)
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config1:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config2[CONF_MAC]}/sensors",
json.dumps(sensor_config2),
)
await hass.async_block_till_done()
# Verify entity registry entries are cleared # Verify entity registry entries are cleared
device_entry = device_reg.async_get_device(set(), {("mac", config2[CONF_MAC])}) device_entry = device_reg.async_get_device(set(), {("mac", config2[CONF_MAC])})
assert device_entry is not None assert device_entry is not None
entity_entry = entity_reg.async_get(f"{domain}.test") entity_entry = entity_reg.async_get(f"{domain}.{entity_id}")
assert entity_entry is None assert entity_entry is None
# Verify state is removed # Verify state is removed
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state is None assert state is None
async def help_test_discovery_update_unchanged( async def help_test_discovery_update_unchanged(
hass, mqtt_mock, caplog, domain, config, discovery_update hass,
mqtt_mock,
caplog,
domain,
config,
discovery_update,
sensor_config=None,
entity_id="test",
name="Test",
): ):
"""Test update of discovered component without changes. """Test update of discovered component without changes.
@ -313,18 +390,33 @@ async def help_test_discovery_update_unchanged(
config1 = copy.deepcopy(config) config1 = copy.deepcopy(config)
config2 = copy.deepcopy(config) config2 = copy.deepcopy(config)
config2[CONF_PREFIX][PREFIX_CMND] = "cmnd2" config2[CONF_PREFIX][PREFIX_CMND] = "cmnd2"
config2[CONF_PREFIX][PREFIX_TELE] = "tele2"
data1 = json.dumps(config1) data1 = json.dumps(config1)
data2 = json.dumps(config2) data2 = json.dumps(config2)
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1)
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state is not None assert state is not None
assert state.name == "Test" assert state.name == name
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1)
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
assert not discovery_update.called assert not discovery_update.called
@ -334,7 +426,9 @@ async def help_test_discovery_update_unchanged(
assert discovery_update.called assert discovery_update.called
async def help_test_discovery_device_remove(hass, mqtt_mock, domain, unique_id, config): async def help_test_discovery_device_remove(
hass, mqtt_mock, domain, unique_id, config, sensor_config=None
):
"""Test domain entity is removed when device is removed.""" """Test domain entity is removed when device is removed."""
device_reg = await hass.helpers.device_registry.async_get_registry() device_reg = await hass.helpers.device_registry.async_get_registry()
entity_reg = await hass.helpers.entity_registry.async_get_registry() entity_reg = await hass.helpers.entity_registry.async_get_registry()
@ -344,6 +438,13 @@ async def help_test_discovery_device_remove(hass, mqtt_mock, domain, unique_id,
data = json.dumps(config) data = json.dumps(config)
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
device = device_reg.async_get_device(set(), {("mac", config[CONF_MAC])}) device = device_reg.async_get_device(set(), {("mac", config[CONF_MAC])})
assert device is not None assert device is not None
@ -358,7 +459,7 @@ async def help_test_discovery_device_remove(hass, mqtt_mock, domain, unique_id,
async def help_test_entity_id_update_subscriptions( async def help_test_entity_id_update_subscriptions(
hass, mqtt_mock, domain, config, topics=None hass, mqtt_mock, domain, config, topics=None, sensor_config=None, entity_id="test"
): ):
"""Test MQTT subscriptions are managed when entity_id is updated.""" """Test MQTT subscriptions are managed when entity_id is updated."""
entity_reg = await hass.helpers.entity_registry.async_get_registry() entity_reg = await hass.helpers.entity_registry.async_get_registry()
@ -370,22 +471,31 @@ async def help_test_entity_id_update_subscriptions(
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
if not topics: if not topics:
topics = [get_topic_tele_state(config), get_topic_tele_will(config)] topics = [get_topic_tele_state(config), get_topic_tele_will(config)]
assert len(topics) > 0 assert len(topics) > 0
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state is not None assert state is not None
assert mqtt_mock.async_subscribe.call_count == len(topics) assert mqtt_mock.async_subscribe.call_count == len(topics)
for topic in topics: for topic in topics:
mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY)
mqtt_mock.async_subscribe.reset_mock() mqtt_mock.async_subscribe.reset_mock()
entity_reg.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") entity_reg.async_update_entity(
f"{domain}.{entity_id}", new_entity_id=f"{domain}.milk"
)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state is None assert state is None
state = hass.states.get(f"{domain}.milk") state = hass.states.get(f"{domain}.milk")
@ -394,7 +504,9 @@ async def help_test_entity_id_update_subscriptions(
mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY)
async def help_test_entity_id_update_discovery_update(hass, mqtt_mock, domain, config): async def help_test_entity_id_update_discovery_update(
hass, mqtt_mock, domain, config, sensor_config=None, entity_id="test"
):
"""Test MQTT discovery update after entity_id is updated.""" """Test MQTT discovery update after entity_id is updated."""
entity_reg = await hass.helpers.entity_registry.async_get_registry() entity_reg = await hass.helpers.entity_registry.async_get_registry()
@ -405,16 +517,25 @@ async def help_test_entity_id_update_discovery_update(hass, mqtt_mock, domain, c
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
await hass.async_block_till_done() await hass.async_block_till_done()
if sensor_config:
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, topic, config_get_state_online(config)) async_fire_mqtt_message(hass, topic, config_get_state_online(config))
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
async_fire_mqtt_message(hass, topic, config_get_state_offline(config)) async_fire_mqtt_message(hass, topic, config_get_state_offline(config))
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.{entity_id}")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
entity_reg.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") entity_reg.async_update_entity(
f"{domain}.{entity_id}", new_entity_id=f"{domain}.milk"
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get(f"{domain}.milk") assert hass.states.get(f"{domain}.milk")

View file

@ -0,0 +1,254 @@
"""The tests for the Tasmota sensor platform."""
import copy
import json
from hatasmota.utils import (
get_topic_stat_status,
get_topic_tele_sensor,
get_topic_tele_will,
)
from homeassistant.components import sensor
from homeassistant.components.tasmota.const import DEFAULT_PREFIX
from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN
from .test_common import (
DEFAULT_CONFIG,
help_test_availability,
help_test_availability_discovery_update,
help_test_availability_poll_state,
help_test_availability_when_connection_lost,
help_test_discovery_device_remove,
help_test_discovery_removal,
help_test_discovery_update_unchanged,
help_test_entity_id_update_discovery_update,
help_test_entity_id_update_subscriptions,
)
from tests.async_mock import patch
from tests.common import async_fire_mqtt_message
DEFAULT_SENSOR_CONFIG = {
"sn": {
"Time": "2020-09-25T12:47:15",
"DHT11": {"Temperature": None},
"TempUnit": "C",
}
}
async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
"""Test state update via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
mac = config["mac"]
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/config",
json.dumps(config),
)
await hass.async_block_till_done()
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
state = hass.states.get("sensor.dht11_temperature")
assert state.state == "unavailable"
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
state = hass.states.get("sensor.dht11_temperature")
assert state.state == STATE_UNKNOWN
assert not state.attributes.get(ATTR_ASSUMED_STATE)
# Test periodic state update
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/SENSOR", '{"DHT11":{"Temperature":20.5}}'
)
state = hass.states.get("sensor.dht11_temperature")
assert state.state == "20.5"
# Test polled state update
async_fire_mqtt_message(
hass,
"tasmota_49A3BC/stat/STATUS8",
'{"StatusSNS":{"DHT11":{"Temperature":20.0}}}',
)
state = hass.states.get("sensor.dht11_temperature")
assert state.state == "20.0"
async def test_attributes(hass, mqtt_mock, setup_tasmota):
"""Test correct attributes for sensors."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = {
"sn": {
"DHT11": {"Temperature": None},
"Beer": {"CarbonDioxide": None},
"TempUnit": "C",
}
}
mac = config["mac"]
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/config",
json.dumps(config),
)
await hass.async_block_till_done()
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/sensors",
json.dumps(sensor_config),
)
await hass.async_block_till_done()
state = hass.states.get("sensor.dht11_temperature")
assert state.attributes.get("device_class") == "temperature"
assert state.attributes.get("friendly_name") == "DHT11 Temperature"
assert state.attributes.get("icon") is None
assert state.attributes.get("unit_of_measurement") == "C"
state = hass.states.get("sensor.beer_CarbonDioxide")
assert state.attributes.get("device_class") is None
assert state.attributes.get("friendly_name") == "Beer CarbonDioxide"
assert state.attributes.get("icon") == "mdi:molecule-co2"
assert state.attributes.get("unit_of_measurement") == "ppm"
async def test_availability_when_connection_lost(
hass, mqtt_client_mock, mqtt_mock, setup_tasmota
):
"""Test availability after MQTT disconnection."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
await help_test_availability_when_connection_lost(
hass,
mqtt_client_mock,
mqtt_mock,
sensor.DOMAIN,
config,
sensor_config,
"dht11_temperature",
)
async def test_availability(hass, mqtt_mock, setup_tasmota):
"""Test availability."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
await help_test_availability(
hass, mqtt_mock, sensor.DOMAIN, config, sensor_config, "dht11_temperature"
)
async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota):
"""Test availability discovery update."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
await help_test_availability_discovery_update(
hass, mqtt_mock, sensor.DOMAIN, config, sensor_config, "dht11_temperature"
)
async def test_availability_poll_state(
hass, mqtt_client_mock, mqtt_mock, setup_tasmota
):
"""Test polling after MQTT connection (re)established."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
poll_topic = "tasmota_49A3BC/cmnd/STATUS"
await help_test_availability_poll_state(
hass,
mqtt_client_mock,
mqtt_mock,
sensor.DOMAIN,
config,
poll_topic,
"8",
sensor_config,
)
async def test_discovery_removal_sensor(hass, mqtt_mock, caplog, setup_tasmota):
"""Test removal of discovered sensor."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config1 = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
await help_test_discovery_removal(
hass,
mqtt_mock,
caplog,
sensor.DOMAIN,
config,
config,
sensor_config1,
{},
"dht11_temperature",
"DHT11 Temperature",
)
async def test_discovery_update_unchanged_sensor(
hass, mqtt_mock, caplog, setup_tasmota
):
"""Test update of discovered sensor."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
with patch(
"homeassistant.components.tasmota.sensor.TasmotaSensor.discovery_update"
) as discovery_update:
await help_test_discovery_update_unchanged(
hass,
mqtt_mock,
caplog,
sensor.DOMAIN,
config,
discovery_update,
sensor_config,
"dht11_temperature",
"DHT11 Temperature",
)
async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota):
"""Test device registry remove."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
unique_id = f"{DEFAULT_CONFIG['mac']}_sensor_sensor_DHT11_Temperature"
await help_test_discovery_device_remove(
hass, mqtt_mock, sensor.DOMAIN, unique_id, config, sensor_config
)
async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota):
"""Test MQTT subscriptions are managed when entity_id is updated."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
topics = [
get_topic_tele_sensor(config),
get_topic_stat_status(config, 8),
get_topic_tele_will(config),
]
await help_test_entity_id_update_subscriptions(
hass,
mqtt_mock,
sensor.DOMAIN,
config,
topics,
sensor_config,
"dht11_temperature",
)
async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota):
"""Test MQTT discovery update when entity_id is updated."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
await help_test_entity_id_update_discovery_update(
hass, mqtt_mock, sensor.DOMAIN, config, sensor_config, "dht11_temperature"
)