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:
parent
59edb25e4e
commit
84cb00bb4e
7 changed files with 604 additions and 36 deletions
|
@ -11,7 +11,9 @@ from hatasmota.discovery import (
|
|||
unique_id_from_hash,
|
||||
)
|
||||
|
||||
import homeassistant.components.sensor as sensor
|
||||
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 .const import DOMAIN
|
||||
|
@ -117,8 +119,38 @@ async def async_start(
|
|||
for (tasmota_entity_config, discovery_hash) in tasmota_entities:
|
||||
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[CONFIG_ENTRY_IS_SETUP] = set()
|
||||
|
||||
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
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Tasmota (beta)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tasmota",
|
||||
"requirements": ["hatasmota==0.0.10"],
|
||||
"requirements": ["hatasmota==0.0.11"],
|
||||
"dependencies": ["mqtt"],
|
||||
"mqtt": ["tasmota/discovery/#"],
|
||||
"codeowners": ["@emontnemery"]
|
||||
|
|
161
homeassistant/components/tasmota/sensor.py
Normal file
161
homeassistant/components/tasmota/sensor.py
Normal 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
|
|
@ -729,7 +729,7 @@ hass-nabucasa==0.37.0
|
|||
hass_splunk==0.1.1
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.0.10
|
||||
hatasmota==0.0.11
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate==0.9.5
|
||||
|
|
|
@ -361,7 +361,7 @@ hangups==0.4.11
|
|||
hass-nabucasa==0.37.0
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.0.10
|
||||
hatasmota==0.0.11
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate==0.9.5
|
||||
|
|
|
@ -57,7 +57,13 @@ DEFAULT_CONFIG = {
|
|||
|
||||
|
||||
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.
|
||||
|
||||
|
@ -69,6 +75,13 @@ async def help_test_availability_when_connection_lost(
|
|||
json.dumps(config),
|
||||
)
|
||||
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
|
||||
async_fire_mqtt_message(
|
||||
|
@ -76,7 +89,8 @@ async def help_test_availability_when_connection_lost(
|
|||
get_topic_tele_will(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
|
||||
|
||||
# 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()
|
||||
state = hass.states.get(f"{domain}.test")
|
||||
state = hass.states.get(f"{domain}.{entity_id}")
|
||||
assert state.state == STATE_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()
|
||||
state = hass.states.get(f"{domain}.test")
|
||||
state = hass.states.get(f"{domain}.{entity_id}")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Receive LWT again
|
||||
|
@ -103,7 +117,7 @@ async def help_test_availability_when_connection_lost(
|
|||
get_topic_tele_will(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
|
||||
|
||||
|
||||
|
@ -112,6 +126,8 @@ async def help_test_availability(
|
|||
mqtt_mock,
|
||||
domain,
|
||||
config,
|
||||
sensor_config=None,
|
||||
entity_id="test",
|
||||
):
|
||||
"""Test availability.
|
||||
|
||||
|
@ -123,8 +139,15 @@ async def help_test_availability(
|
|||
json.dumps(config),
|
||||
)
|
||||
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
|
||||
|
||||
async_fire_mqtt_message(
|
||||
|
@ -133,7 +156,7 @@ async def help_test_availability(
|
|||
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
|
||||
|
||||
async_fire_mqtt_message(
|
||||
|
@ -142,7 +165,7 @@ async def help_test_availability(
|
|||
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
|
||||
|
||||
|
||||
|
@ -151,6 +174,8 @@ async def help_test_availability_discovery_update(
|
|||
mqtt_mock,
|
||||
domain,
|
||||
config,
|
||||
sensor_config=None,
|
||||
entity_id="test",
|
||||
):
|
||||
"""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)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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, online2)
|
||||
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
|
||||
|
||||
# Verify we are subscribing to the new topic
|
||||
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
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
@ -222,6 +261,13 @@ async def help_test_availability_poll_state(
|
|||
json.dumps(config),
|
||||
)
|
||||
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()
|
||||
|
||||
# Device online, verify poll for state
|
||||
|
@ -265,7 +311,16 @@ async def help_test_availability_poll_state(
|
|||
|
||||
|
||||
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."""
|
||||
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)
|
||||
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
|
||||
device_entry = device_reg.async_get_device(set(), {("mac", config1[CONF_MAC])})
|
||||
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
|
||||
|
||||
# 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.name == "Test"
|
||||
assert state.name == name
|
||||
|
||||
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config2[CONF_MAC]}/config", data2)
|
||||
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
|
||||
device_entry = device_reg.async_get_device(set(), {("mac", config2[CONF_MAC])})
|
||||
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
|
||||
|
||||
# Verify state is removed
|
||||
state = hass.states.get(f"{domain}.test")
|
||||
state = hass.states.get(f"{domain}.{entity_id}")
|
||||
assert state is None
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
@ -313,18 +390,33 @@ async def help_test_discovery_update_unchanged(
|
|||
config1 = copy.deepcopy(config)
|
||||
config2 = copy.deepcopy(config)
|
||||
config2[CONF_PREFIX][PREFIX_CMND] = "cmnd2"
|
||||
config2[CONF_PREFIX][PREFIX_TELE] = "tele2"
|
||||
data1 = json.dumps(config1)
|
||||
data2 = json.dumps(config2)
|
||||
|
||||
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1)
|
||||
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.name == "Test"
|
||||
assert state.name == name
|
||||
|
||||
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1)
|
||||
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
|
||||
|
||||
|
@ -334,7 +426,9 @@ async def help_test_discovery_update_unchanged(
|
|||
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."""
|
||||
device_reg = await hass.helpers.device_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)
|
||||
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
|
||||
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])})
|
||||
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(
|
||||
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."""
|
||||
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)
|
||||
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:
|
||||
topics = [get_topic_tele_state(config), get_topic_tele_will(config)]
|
||||
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 mqtt_mock.async_subscribe.call_count == len(topics)
|
||||
for topic in topics:
|
||||
mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY)
|
||||
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()
|
||||
|
||||
state = hass.states.get(f"{domain}.test")
|
||||
state = hass.states.get(f"{domain}.{entity_id}")
|
||||
assert state is None
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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."""
|
||||
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)
|
||||
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))
|
||||
state = hass.states.get(f"{domain}.test")
|
||||
state = hass.states.get(f"{domain}.{entity_id}")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
assert hass.states.get(f"{domain}.milk")
|
||||
|
||||
|
|
254
tests/components/tasmota/test_sensor.py
Normal file
254
tests/components/tasmota/test_sensor.py
Normal 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"
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue