Bump hatasmota to 0.0.10, minor refactor of discovery (#41331)

* Bump hatasmota to 0.0.10, minor refactor of discovery

* Update tests

* Add missing docstrings
This commit is contained in:
Erik Montnemery 2020-10-06 22:32:36 +02:00 committed by GitHub
parent 9b947e08bf
commit 42fb0e9545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 155 additions and 138 deletions

View File

@ -2,13 +2,13 @@
import asyncio
import logging
from hatasmota.const import CONF_RELAY
from hatasmota.discovery import (
TasmotaDiscovery,
get_device_config as tasmota_get_device_config,
get_entities_for_platform as tasmota_get_entities_for_platform,
get_entity as tasmota_get_entity,
has_entities_with_platform as tasmota_has_entities_with_platform,
unique_id_from_hash,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -18,16 +18,16 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SUPPORTED_COMPONENTS = {
"switch": CONF_RELAY,
}
SUPPORTED_PLATFORMS = [
"switch",
]
ALREADY_DISCOVERED = "tasmota_discovered_components"
CONFIG_ENTRY_IS_SETUP = "tasmota_config_entry_is_setup"
DATA_CONFIG_ENTRY_LOCK = "tasmota_config_entry_lock"
TASMOTA_DISCOVERY_DEVICE = "tasmota_discovery_device"
TASMOTA_DISCOVERY_ENTITY_NEW = "tasmota_discovery_entity_new_{}"
TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}"
TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}_{}"
def clear_discovery_hash(hass, discovery_hash):
@ -45,6 +45,52 @@ async def async_start(
) -> bool:
"""Start MQTT Discovery."""
async def _load_platform(platform):
"""Load a Tasmota platform if not already done."""
async with hass.data[DATA_CONFIG_ENTRY_LOCK]:
config_entries_key = f"{platform}.tasmota"
if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]:
await hass.config_entries.async_forward_entry_setup(
config_entry, platform
)
hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)
async def _discover_entity(tasmota_entity_config, discovery_hash, platform):
"""Handle adding or updating a discovered entity."""
if not tasmota_entity_config:
# Entity disabled, clean up entity registry
entity_registry = await hass.helpers.entity_registry.async_get_registry()
unique_id = unique_id_from_hash(discovery_hash)
entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
if entity_id:
_LOGGER.debug("Removing entity: %s %s", platform, discovery_hash)
entity_registry.async_remove(entity_id)
return
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
_LOGGER.debug(
"Entity already added, sending update: %s %s",
platform,
discovery_hash,
)
async_dispatcher_send(
hass,
TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
tasmota_entity_config,
)
else:
_LOGGER.debug("Adding new entity: %s %s", platform, discovery_hash)
tasmota_entity = tasmota_get_entity(tasmota_entity_config, tasmota_mqtt)
hass.data[ALREADY_DISCOVERED][discovery_hash] = None
async_dispatcher_send(
hass,
TASMOTA_DISCOVERY_ENTITY_NEW.format(platform),
tasmota_entity,
discovery_hash,
)
async def async_device_discovered(payload, mac):
"""Process the received message."""
@ -57,65 +103,21 @@ async def async_start(
hass, TASMOTA_DISCOVERY_DEVICE, tasmota_device_config, mac
)
async with hass.data[DATA_CONFIG_ENTRY_LOCK]:
for component, component_key in SUPPORTED_COMPONENTS.items():
if not tasmota_has_entities_with_platform(payload, component_key):
continue
config_entries_key = f"{component}.tasmota"
if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]:
await hass.config_entries.async_forward_entry_setup(
config_entry, component
)
hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)
if not payload:
return
for component, component_key in SUPPORTED_COMPONENTS.items():
tasmota_entities = tasmota_get_entities_for_platform(payload, component_key)
for (idx, tasmota_entity_config) in enumerate(tasmota_entities):
discovery_hash = (mac, component, idx)
if not tasmota_entity_config:
# Entity disabled, clean up entity registry
entity_registry = (
await hass.helpers.entity_registry.async_get_registry()
)
unique_id = "{}_{}_{}".format(*discovery_hash)
entity_id = entity_registry.async_get_entity_id(
component, DOMAIN, unique_id
)
if entity_id:
_LOGGER.debug(
"Removing entity: %s %s", component, discovery_hash
)
entity_registry.async_remove(entity_id)
continue
for platform in SUPPORTED_PLATFORMS:
if not tasmota_has_entities_with_platform(payload, platform):
continue
await _load_platform(platform)
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
_LOGGER.debug(
"Entity already added, sending update: %s %s",
component,
discovery_hash,
)
async_dispatcher_send(
hass,
TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
tasmota_entity_config,
)
else:
_LOGGER.debug("Adding new entity: %s %s", component, discovery_hash)
hass.data[ALREADY_DISCOVERED][discovery_hash] = None
tasmota_entity = tasmota_get_entity(
tasmota_entity_config, component_key, tasmota_mqtt
)
async_dispatcher_send(
hass,
TASMOTA_DISCOVERY_ENTITY_NEW.format(component),
tasmota_entity,
discovery_hash,
)
for platform in SUPPORTED_PLATFORMS:
tasmota_entities = tasmota_get_entities_for_platform(payload, platform)
for (tasmota_entity_config, discovery_hash) in tasmota_entities:
await _discover_entity(tasmota_entity_config, discovery_hash, platform)
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)
await tasmota_discovery.start_discovery(async_device_discovered, None)

View File

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

View File

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

View File

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

View File

@ -11,8 +11,8 @@ from hatasmota.const import (
PREFIX_TELE,
)
from hatasmota.utils import (
get_state_offline,
get_state_online,
config_get_state_offline,
config_get_state_online,
get_topic_tele_state,
get_topic_tele_will,
)
@ -23,6 +23,38 @@ from homeassistant.const import STATE_UNAVAILABLE
from tests.async_mock import ANY
from tests.common import async_fire_mqtt_message
DEFAULT_CONFIG = {
"ip": "192.168.15.10",
"dn": "Tasmota",
"fn": ["Test", "Beer", "Milk", "Four", None],
"hn": "tasmota_49A3BC-0956",
"lk": 1, # RGB + white channels linked to a single light
"mac": "00000049A3BC",
"md": "Sonoff Basic",
"ofln": "Offline",
"onln": "Online",
"state": ["OFF", "ON", "TOGGLE", "HOLD"],
"sw": "8.4.0.2",
"t": "tasmota_49A3BC",
"ft": "%topic%/%prefix%/",
"tp": ["cmnd", "stat", "tele"],
"rl": [0, 0, 0, 0, 0, 0, 0, 0],
"swc": [-1, -1, -1, -1, -1, -1, -1, -1],
"btn": [0, 0, 0, 0],
"so": {
"11": 0, # Swap button single and double press functionality
"13": 0, # Allow immediate action on single button press
"17": 1, # Show Color string as hex or comma-separated
"20": 0, # Update of Dimmer/Color/CT without turning power on
"30": 0, # Enforce Home Assistant auto-discovery as light
"68": 0, # Multi-channel PWM instead of a single light
"73": 0, # Enable Buttons decoupling and send multi-press and hold MQTT messages
"80": 0, # Blinds and shutters support
},
"lt_st": 0,
"ver": 1,
}
async def help_test_availability_when_connection_lost(
hass, mqtt_client_mock, mqtt_mock, domain, config
@ -37,7 +69,7 @@ async def help_test_availability_when_connection_lost(
async_fire_mqtt_message(
hass,
get_topic_tele_will(config),
get_state_online(config),
config_get_state_online(config),
)
state = hass.states.get(f"{domain}.test")
@ -83,7 +115,7 @@ async def help_test_availability(
async_fire_mqtt_message(
hass,
get_topic_tele_will(config),
get_state_online(config),
config_get_state_online(config),
)
state = hass.states.get(f"{domain}.test")
@ -92,7 +124,7 @@ async def help_test_availability(
async_fire_mqtt_message(
hass,
get_topic_tele_will(config),
get_state_offline(config),
config_get_state_offline(config),
)
state = hass.states.get(f"{domain}.test")
@ -124,11 +156,11 @@ async def help_test_availability_discovery_update(
availability_topic1 = get_topic_tele_will(config1)
availability_topic2 = get_topic_tele_will(config2)
assert availability_topic1 != availability_topic2
offline1 = get_state_offline(config1)
offline2 = get_state_offline(config2)
offline1 = config_get_state_offline(config1)
offline2 = config_get_state_offline(config2)
assert offline1 != offline2
online1 = get_state_online(config1)
online2 = get_state_online(config2)
online1 = config_get_state_online(config1)
online2 = config_get_state_online(config2)
assert online1 != online2
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config1[CONF_MAC]}/config", data1)
@ -232,13 +264,12 @@ async def help_test_discovery_update_unchanged(
assert discovery_update.called
async def help_test_discovery_device_remove(hass, mqtt_mock, domain, config):
async def help_test_discovery_device_remove(hass, mqtt_mock, domain, unique_id, config):
"""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()
config = copy.deepcopy(config)
unique_id = f"{config[CONF_MAC]}_{domain}_0"
data = json.dumps(config)
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data)
@ -304,16 +335,17 @@ 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()
async_fire_mqtt_message(hass, topic, get_state_online(config))
async_fire_mqtt_message(hass, topic, config_get_state_online(config))
state = hass.states.get(f"{domain}.test")
assert state.state != STATE_UNAVAILABLE
async_fire_mqtt_message(hass, topic, get_state_offline(config))
async_fire_mqtt_message(hass, topic, config_get_state_offline(config))
state = hass.states.get(f"{domain}.test")
assert state.state == STATE_UNAVAILABLE
entity_reg.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk")
await hass.async_block_till_done()
assert hass.states.get(f"{domain}.milk")
assert config[CONF_PREFIX][PREFIX_TELE] != "tele2"
config[CONF_PREFIX][PREFIX_TELE] = "tele2"
@ -323,6 +355,6 @@ async def help_test_entity_id_update_discovery_update(hass, mqtt_mock, domain, c
assert len(hass.states.async_entity_ids(domain)) == 1
topic = get_topic_tele_will(config)
async_fire_mqtt_message(hass, topic, get_state_online(config))
async_fire_mqtt_message(hass, topic, config_get_state_online(config))
state = hass.states.get(f"{domain}.milk")
assert state.state != STATE_UNAVAILABLE

View File

@ -2,34 +2,16 @@
import copy
import json
from hatasmota.const import CONF_ONLINE
import pytest
from homeassistant.components.tasmota.const import DEFAULT_PREFIX
from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED
from .conftest import setup_tasmota_helper
from .test_common import DEFAULT_CONFIG
from tests.async_mock import patch
from tests.common import async_fire_mqtt_message
from tests.components.tasmota.conftest import setup_tasmota_helper
DEFAULT_CONFIG = {
"dn": "My Device",
"fn": ["Beer", "Milk", "Three", "Four", "Five"],
"hn": "tasmota_49A3BC",
"mac": "00000049A3BC",
"md": "Sonoff 123",
"ofl": "offline",
CONF_ONLINE: "online",
"state": ["OFF", "ON", "TOGGLE", "HOLD"],
"sw": "2.3.3.4",
"t": "tasmota_49A3BC",
"t_f": "%topic%/%prefix%/",
"t_p": ["cmnd", "stat", "tele"],
"li": [0, 0, 0, 0, 0, 0, 0, 0],
"rl": [0, 0, 0, 0, 0, 0, 0, 0],
"se": [],
"ver": 1,
}
async def test_subscribing_config_topic(hass, mqtt_mock, setup_tasmota):
@ -118,14 +100,14 @@ async def test_correct_config_discovery(
# Verify device and registry entries are created
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
assert device_entry is not None
entity_entry = entity_reg.async_get("switch.beer")
entity_entry = entity_reg.async_get("switch.test")
assert entity_entry is not None
state = hass.states.get("switch.beer")
state = hass.states.get("switch.test")
assert state is not None
assert state.name == "Beer"
assert state.name == "Test"
assert (mac, "switch", 0) in hass.data[ALREADY_DISCOVERED]
assert (mac, "switch", "relay", 0) in hass.data[ALREADY_DISCOVERED]
async def test_device_discover(
@ -337,14 +319,14 @@ async def test_entity_duplicate_discovery(hass, mqtt_mock, caplog, setup_tasmota
)
await hass.async_block_till_done()
state = hass.states.get("switch.beer")
state = hass.states.get("switch.test")
state_duplicate = hass.states.get("binary_sensor.beer1")
assert state is not None
assert state.name == "Beer"
assert state.name == "Test"
assert state_duplicate is None
assert (
f"Entity already added, sending update: switch ('{mac}', 'switch', 0)"
f"Entity already added, sending update: switch ('{mac}', 'switch', 'relay', 0)"
in caplog.text
)
@ -364,9 +346,9 @@ async def test_entity_duplicate_removal(hass, mqtt_mock, caplog, setup_tasmota):
config["rl"][0] = 0
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config))
await hass.async_block_till_done()
assert f"Removing entity: switch ('{mac}', 'switch', 0)" in caplog.text
assert f"Removing entity: switch ('{mac}', 'switch', 'relay', 0)" in caplog.text
caplog.clear()
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config))
await hass.async_block_till_done()
assert f"Removing entity: switch ('{mac}', 'switch', 0)" not in caplog.text
assert "Removing entity: switch" not in caplog.text

View File

@ -1,14 +1,13 @@
"""The tests for the MQTT switch platform."""
"""The tests for the Tasmota switch platform."""
import copy
import json
from hatasmota.const import CONF_ONLINE
from homeassistant.components import switch
from homeassistant.components.tasmota.const import DEFAULT_PREFIX
from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON
from .test_common import (
DEFAULT_CONFIG,
help_test_availability,
help_test_availability_discovery_update,
help_test_availability_when_connection_lost,
@ -23,29 +22,11 @@ from tests.async_mock import patch
from tests.common import async_fire_mqtt_message
from tests.components.switch import common
DEFAULT_CONFIG = {
"dn": "My Device",
"fn": ["Test", "Beer", "Milk", "Four", "Five"],
"hn": "tasmota_49A3BC",
"mac": "00000049A3BC",
"md": "Sonoff 123",
"ofl": "offline",
CONF_ONLINE: "online",
"state": ["OFF", "ON", "TOGGLE", "HOLD"],
"sw": "2.3.3.4",
"t": "tasmota_49A3BC",
"t_f": "%topic%/%prefix%/",
"t_p": ["cmnd", "stat", "tele"],
"li": [0, 0, 0, 0, 0, 0, 0, 0],
"rl": [1, 0, 0, 0, 0, 0, 0, 0],
"se": [],
"ver": 1,
}
async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
"""Test state update via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["rl"][0] = 1
mac = config["mac"]
async_fire_mqtt_message(
@ -59,7 +40,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
assert state.state == "unavailable"
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "online")
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
state = hass.states.get("switch.test")
assert state.state == STATE_OFF
assert not state.attributes.get(ATTR_ASSUMED_STATE)
@ -78,6 +59,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
"""Test the sending MQTT commands."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["rl"][0] = 1
mac = config["mac"]
async_fire_mqtt_message(
@ -87,9 +69,12 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "online")
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
state = hass.states.get("switch.test")
assert state.state == STATE_OFF
await hass.async_block_till_done()
await hass.async_block_till_done()
mqtt_mock.async_publish.reset_mock()
# Turn the switch on and verify MQTT message is sent
await common.async_turn_on(hass, "switch.test")
@ -116,26 +101,33 @@ 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)
config["rl"][0] = 1
await help_test_availability_when_connection_lost(
hass, mqtt_client_mock, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG
hass, mqtt_client_mock, mqtt_mock, switch.DOMAIN, config
)
async def test_availability(hass, mqtt_mock, setup_tasmota):
"""Test availability."""
await help_test_availability(hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG)
config = copy.deepcopy(DEFAULT_CONFIG)
config["rl"][0] = 1
await help_test_availability(hass, mqtt_mock, switch.DOMAIN, config)
async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota):
"""Test availability discovery update."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["rl"][0] = 1
await help_test_availability_discovery_update(
hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG
hass, mqtt_mock, switch.DOMAIN, config
)
async def test_discovery_removal_switch(hass, mqtt_mock, caplog, setup_tasmota):
"""Test removal of discovered switch."""
config1 = copy.deepcopy(DEFAULT_CONFIG)
config1["rl"][0] = 1
config2 = copy.deepcopy(DEFAULT_CONFIG)
config2["rl"][0] = 0
@ -148,30 +140,39 @@ async def test_discovery_update_unchanged_switch(
hass, mqtt_mock, caplog, setup_tasmota
):
"""Test update of discovered switch."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["rl"][0] = 1
with patch(
"homeassistant.components.tasmota.switch.TasmotaSwitch.discovery_update"
) as discovery_update:
await help_test_discovery_update_unchanged(
hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG, discovery_update
hass, mqtt_mock, caplog, switch.DOMAIN, config, discovery_update
)
async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota):
"""Test device registry remove."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["rl"][0] = 1
unique_id = f"{DEFAULT_CONFIG['mac']}_switch_relay_0"
await help_test_discovery_device_remove(
hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG
hass, mqtt_mock, switch.DOMAIN, unique_id, 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)
config["rl"][0] = 1
await help_test_entity_id_update_subscriptions(
hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG
hass, mqtt_mock, switch.DOMAIN, config
)
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)
config["rl"][0] = 1
await help_test_entity_id_update_discovery_update(
hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG
hass, mqtt_mock, switch.DOMAIN, config
)