diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index 6d224a98707..06a88333230 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -29,6 +29,9 @@ TASMOTA_DISCOVERY_INSTANCE = "tasmota_discovery_instance" def clear_discovery_hash(hass, discovery_hash): """Clear entry in ALREADY_DISCOVERED list.""" + if ALREADY_DISCOVERED not in hass.data: + # Discovery is shutting down + return del hass.data[ALREADY_DISCOVERED][discovery_hash] diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 8cc3006c63a..991e38e6a95 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.0.23"], + "requirements": ["hatasmota==0.0.24"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index cb5026ba143..5e9eb37ea52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ hass-nabucasa==0.37.1 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.0.23 +hatasmota==0.0.24 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc288c1e481..445d51f9f38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hangups==0.4.11 hass-nabucasa==0.37.1 # homeassistant.components.tasmota -hatasmota==0.0.23 +hatasmota==0.0.24 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 69d49a6ca8f..52ab88b0ecb 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -92,6 +92,30 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert state.state == STATE_OFF +async def test_friendly_names(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + config["swc"][0] = 1 + config["swc"][1] = 1 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") + assert state.state == "unavailable" + assert state.attributes.get("friendly_name") == "Tasmota binary_sensor 1" + + state = hass.states.get("binary_sensor.beer") + assert state.state == "unavailable" + assert state.attributes.get("friendly_name") == "Beer" + + async def test_off_delay(hass, mqtt_mock, setup_tasmota): """Test off_delay option.""" config = copy.deepcopy(DEFAULT_CONFIG) diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 9d6d1cac793..35f3b4be9d8 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -830,3 +830,53 @@ async def test_attach_unknown_remove_device_from_registry( # Remove the device device_reg.async_remove_device(device_entry.id) await hass.async_block_till_done() + + +async def test_attach_remove_config_entry(hass, mqtt_mock, setup_tasmota, device_reg): + """Test trigger cleanup when removing a Tasmota config entry.""" + # Discover a device with device trigger + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 0 + mac = config["mac"] + + mqtt_mock.async_subscribe.reset_mock() + + async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + + calls = [] + + def callback(trigger, context): + calls.append(trigger["trigger"]["description"]) + + await async_attach_trigger( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_switch_1_TOGGLE", + "type": "button_short_press", + "subtype": "switch_1", + }, + callback, + None, + ) + + # Fake short press. + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/SWITCH1T", '{"TRIG":"TOGGLE"}') + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0] == "event 'tasmota_event'" + + # Remove the Tasmota config entry + config_entries = hass.config_entries.async_entries("tasmota") + await hass.config_entries.async_remove(config_entries[0].entry_id) + await hass.async_block_till_done() + + # Verify the triggers are no longer active + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/SWITCH1T", '{"TRIG":"TOGGLE"}') + await hass.async_block_till_done() + assert len(calls) == 1 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index f9d839516e8..48b1dec7232 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -482,7 +482,7 @@ async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota): # Turn the light on and verify MQTT message is sent await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 ON", 0, False + "tasmota_49A3BC/cmnd/Power1", "ON", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -493,7 +493,7 @@ async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota): # Turn the light off and verify MQTT message is sent await common.async_turn_off(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 OFF", 0, False + "tasmota_49A3BC/cmnd/Power1", "OFF", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -581,6 +581,57 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() +async def test_sending_mqtt_commands_power_unlinked(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands to a light with unlinked dimlevel and power.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 1 # 1 channel light (dimmer) + config["so"]["20"] = 1 # Update of Dimmer/Color/CT without turning power on + 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, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.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 light on and verify MQTT message is sent + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be off + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + # Turn the light off and verify MQTT message is sent + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT messages are sent; POWER should be sent + await common.async_turn_on(hass, "light.test", brightness=192) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 0;NoDelay;Dimmer 75;NoDelay;Power1 ON", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_transition(hass, mqtt_mock, setup_tasmota): """Test transition commands.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -733,7 +784,7 @@ async def test_split_light2(hass, mqtt_mock, setup_tasmota): async def _test_unlinked_light(hass, mqtt_mock, config, num_switches): - """Test multi-channel light split to single-channel dimmers.""" + """Test rgbww light split to rgb+ww.""" mac = config["mac"] num_lights = 2 @@ -775,7 +826,7 @@ async def _test_unlinked_light(hass, mqtt_mock, config, num_switches): async def test_unlinked_light(hass, mqtt_mock, setup_tasmota): - """Test multi-channel light split to rgb+ww.""" + """Test rgbww light split to rgb+ww.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 2 config["rl"][1] = 2 @@ -786,7 +837,7 @@ async def test_unlinked_light(hass, mqtt_mock, setup_tasmota): async def test_unlinked_light2(hass, mqtt_mock, setup_tasmota): - """Test multi-channel light split to single-channel dimmers.""" + """Test rgbww light split to rgb+ww.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 1 config["rl"][1] = 1 @@ -899,6 +950,22 @@ async def test_discovery_removal_relay_as_light(hass, mqtt_mock, caplog, setup_t ) +async def test_discovery_removal_relay_as_light2( + hass, mqtt_mock, caplog, setup_tasmota +): + """Test removal of discovered relay as light.""" + config1 = copy.deepcopy(DEFAULT_CONFIG) + config1["rl"][0] = 1 + config1["so"]["30"] = 1 # Enforce Home Assistant auto-discovery as light + config2 = copy.deepcopy(DEFAULT_CONFIG) + config2["rl"][0] = 0 + config2["so"]["30"] = 0 # Disable Home Assistant auto-discovery as light + + await help_test_discovery_removal( + hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + ) + + async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog, setup_tasmota): """Test update of discovered light.""" config = copy.deepcopy(DEFAULT_CONFIG)