1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Fix unavailable status in Tuya (#162709)

This commit is contained in:
epenet
2026-02-12 11:46:40 +01:00
committed by GitHub
parent 6960cd6853
commit ed9a810908
9 changed files with 67 additions and 23 deletions

View File

@@ -51,9 +51,14 @@ class DeviceWrapper[T]:
) -> bool:
"""Determine if the wrapper should skip an update.
The default is to always skip, unless overridden in subclasses.
The default is to always skip if updated properties is given,
unless overridden in subclasses.
"""
return True
# If updated_status_properties is None, we should not skip,
# as we don't have information on what was updated
# This happens for example on online/offline updates, where
# we still want to update the entity state
return updated_status_properties is not None
def read_device_status(self, device: CustomerDevice) -> T | None:
"""Read device status and convert to a Home Assistant value."""
@@ -88,9 +93,13 @@ class DPCodeWrapper(DeviceWrapper):
By default, skip if updated_status_properties is given and
does not include this dpcode.
"""
# If updated_status_properties is None, we should not skip,
# as we don't have information on what was updated
# This happens for example on online/offline updates, where
# we still want to update the entity state
return (
updated_status_properties is None
or self.dpcode not in updated_status_properties
updated_status_properties is not None
and self.dpcode not in updated_status_properties
)
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
@@ -250,6 +259,13 @@ class DPCodeDeltaIntegerWrapper(DPCodeIntegerWrapper):
Processes delta accumulation before determining if update should be skipped.
"""
# If updated_status_properties is None, we should not skip,
# as we don't have information on what was updated
# This happens for example on online/offline updates, where
# we still want to update the entity state but we have nothing
# to accumulate, so we return False to not skip the update
if updated_status_properties is None:
return False
if (
super().skip_update(device, updated_status_properties, dp_timestamps)
or dp_timestamps is None

View File

@@ -16,6 +16,7 @@ from tuya_sharing import (
)
from homeassistant.components.tuya import DOMAIN, DeviceListener
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.json import json_dumps
from homeassistant.util import dt as dt_util
@@ -38,10 +39,13 @@ class MockDeviceListener(DeviceListener):
device: CustomerDevice,
updated_status_properties: dict[str, Any] | None = None,
dp_timestamps: dict[str, int] | None = None,
*,
online: bool | None = None,
) -> None:
"""Mock update device method."""
property_list: list[str] = []
if updated_status_properties:
property_list: list[str] | None = None
if updated_status_properties is not None:
property_list = []
for key, value in updated_status_properties.items():
if key not in device.status:
raise ValueError(
@@ -49,6 +53,8 @@ class MockDeviceListener(DeviceListener):
)
device.status[key] = value
property_list.append(key)
if online is not None:
device.online = online
self.update_device(device, property_list, dp_timestamps)
await hass.async_block_till_done()
@@ -185,15 +191,30 @@ async def check_selective_state_update(
the entity state is not changed and last_reported is not updated.
"""
initial_reported = "2024-01-01T00:00:00+00:00"
unavailable_reported = "2024-01-01T00:00:10+00:00"
available_reported = "2024-01-01T00:00:20+00:00"
assert hass.states.get(entity_id).state == initial_state
assert hass.states.get(entity_id).last_reported.isoformat() == initial_reported
# Force update the dpcode and trigger device update
freezer.tick(30)
# Trigger device offline
freezer.tick(10)
await mock_listener.async_send_device_update(hass, mock_device, online=False)
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
assert hass.states.get(entity_id).last_reported.isoformat() == unavailable_reported
# Trigger device online
freezer.tick(10)
await mock_listener.async_send_device_update(hass, mock_device, online=True)
assert hass.states.get(entity_id).state == initial_state
assert hass.states.get(entity_id).last_reported.isoformat() == available_reported
# Force update the dpcode and trigger device update without the dpcode
# in updated properties - state should not change
freezer.tick(10)
mock_device.status[dpcode] = None
await mock_listener.async_send_device_update(hass, mock_device, {})
assert hass.states.get(entity_id).state == initial_state
assert hass.states.get(entity_id).last_reported.isoformat() == initial_reported
assert hass.states.get(entity_id).last_reported.isoformat() == available_reported
# Trigger device update with provided updates
freezer.tick(30)

View File

@@ -42,8 +42,9 @@ async def test_platform_setup_and_discovery(
@pytest.mark.parametrize(
("updates", "expected_state", "last_reported"),
[
# Update without dpcode - state should not change, last_reported stays at initial
({"battery_percentage": 80}, "off", "2024-01-01T00:00:00+00:00"),
# Update without dpcode - state should not change, last_reported stays
# at available_reported
({"battery_percentage": 80}, "off", "2024-01-01T00:00:20+00:00"),
# Update with dpcode - state should change, last_reported advances
({"doorcontact_state": True}, "on", "2024-01-01T00:01:00+00:00"),
# Update with multiple properties including dpcode - state should change

View File

@@ -46,8 +46,9 @@ async def test_platform_setup_and_discovery(
@pytest.mark.parametrize(
("updates", "expected_state", "last_reported"),
[
# Update without dpcode - state should not change, last_reported stays at initial
({"switch_alarm_sound": True}, "15.0", "2024-01-01T00:00:00+00:00"),
# Update without dpcode - state should not change, last_reported stays
# at available_reported
({"switch_alarm_sound": True}, "15.0", "2024-01-01T00:00:20+00:00"),
# Update with dpcode - state should change, last_reported advances
({"delay_set": 17}, "17.0", "2024-01-01T00:01:00+00:00"),
# Update with multiple properties including dpcode - state should change

View File

@@ -47,8 +47,9 @@ async def test_platform_setup_and_discovery(
@pytest.mark.parametrize(
("updates", "expected_state", "last_reported"),
[
# Update without dpcode - state should not change, last_reported stays at initial
({"control": "stop"}, "forward", "2024-01-01T00:00:00+00:00"),
# Update without dpcode - state should not change, last_reported stays
# at available_reported
({"control": "stop"}, "forward", "2024-01-01T00:00:20+00:00"),
# Update with dpcode - state should change, last_reported advances
({"control_back_mode": "back"}, "back", "2024-01-01T00:01:00+00:00"),
# Update with multiple properties including dpcode - state should change

View File

@@ -43,8 +43,9 @@ async def test_platform_setup_and_discovery(
@pytest.mark.parametrize(
("updates", "expected_state", "last_reported"),
[
# Update without dpcode - state should not change, last_reported stays at initial
({"doorcontact_state": True}, "62.0", "2024-01-01T00:00:00+00:00"),
# Update without dpcode - state should not change, last_reported stays
# at available_reported
({"doorcontact_state": True}, "62.0", "2024-01-01T00:00:20+00:00"),
# Update with dpcode - state should change, last_reported advances
({"battery_percentage": 50}, "50.0", "2024-01-01T00:01:00+00:00"),
# Update with multiple properties including dpcode - state should change

View File

@@ -46,8 +46,9 @@ async def test_platform_setup_and_discovery(
@pytest.mark.parametrize(
("updates", "expected_state", "last_reported"),
[
# Update without dpcode - state should not change, last_reported stays at initial
({"basic_wdr": False}, "off", "2024-01-01T00:00:00+00:00"),
# Update without dpcode - state should not change, last_reported stays
# at available_reported
({"basic_wdr": False}, "off", "2024-01-01T00:00:20+00:00"),
# Update with dpcode - state should change, last_reported advances
({"siren_switch": True}, "on", "2024-01-01T00:01:00+00:00"),
# Update with multiple properties including dpcode - state should change

View File

@@ -47,8 +47,9 @@ async def test_platform_setup_and_discovery(
@pytest.mark.parametrize(
("updates", "expected_state", "last_reported"),
[
# Update without dpcode - state should not change, last_reported stays at initial
({"countdown_1": 50}, "off", "2024-01-01T00:00:00+00:00"),
# Update without dpcode - state should not change, last_reported stays
# at available_reported
({"countdown_1": 50}, "off", "2024-01-01T00:00:20+00:00"),
# Update with dpcode - state should change, last_reported advances
({"switch": True}, "on", "2024-01-01T00:01:00+00:00"),
# Update with multiple properties including dpcode - state should change

View File

@@ -46,8 +46,9 @@ async def test_platform_setup_and_discovery(
@pytest.mark.parametrize(
("updates", "expected_state", "last_reported"),
[
# Update without dpcode - state should not change, last_reported stays at initial
({"battery_percentage": 50}, "open", "2024-01-01T00:00:00+00:00"),
# Update without dpcode - state should not change, last_reported stays
# at available_reported
({"battery_percentage": 50}, "open", "2024-01-01T00:00:20+00:00"),
# Update with dpcode - state should change, last_reported advances
({"switch_1": False}, "closed", "2024-01-01T00:01:00+00:00"),
# Update with multiple properties including dpcode - state should change