Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 64 additions & 62 deletions homeassistant/components/tasmota/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,16 +18,16 @@

_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):
Expand All @@ -45,6 +45,52 @@ async def async_start(
) -> bool:
"""Start MQTT Discovery."""

async def _load_platform(platform):
Comment thread
MartinHjelmare marked this conversation as resolved.
"""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):
Comment thread
MartinHjelmare marked this conversation as resolved.
"""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."""

Expand All @@ -57,65 +103,21 @@ async def async_device_discovered(payload, mac):
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)

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

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,
)
if not payload:
return

for platform in SUPPORTED_PLATFORMS:
if not tasmota_has_entities_with_platform(payload, platform):
continue
await _load_platform(platform)

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)
2 changes: 1 addition & 1 deletion homeassistant/components/tasmota/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 46 additions & 14 deletions tests/components/tasmota/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
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,
)
Expand All @@ -23,6 +23,38 @@
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
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand All @@ -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
Loading