Opus review of the previous commit caught two real bugs. The airflow fan's power writes preferred /power/vs/0, copied from the TP1X fan class -- but that order is only harmless there because TP1X never reports /power/0 at all. This family's dumps carry both hrefs, and the power_switch entity is unconditionally bound to /power/0 when present, so the fan was writing to a different resource than power_switch reads/writes, leaving the two entities disagreeing until the next poll. Flipped to prefer /power/0, matching the range hood's fan and common.POWER_GENERIC. Also renamed the new FanDesc's key from 'fan' to 'airflow_fan': BoundEntity unique_ids are derived from key alone, not href, so it collided with air_purifier.FAN's own 'fan' key on the (currently unobserved, but unenforced) possibility of a board reporting both. Added a platform-level test file covering the power-href preference and percentage<->speed-code mapping, mirroring test_range_hood_fan.py's harness.
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""HA fan-entity mapping tests for the ARTIK051_TVTL air purifier (issue #56)."""
|
|
|
|
from custom_components.localthings.fan import LocalThingsAirflowFan
|
|
from custom_components.localthings.registry.by_type import air_purifier
|
|
from custom_components.localthings.registry.capabilities.air_purifier import HREF_AIRFLOW
|
|
from custom_components.localthings.registry.discovery import discover
|
|
from custom_components.localthings.registry.entities import FanDesc
|
|
from tests.conftest import _load_device
|
|
|
|
|
|
class _FakeCoordinator:
|
|
device_serial = 'TEST-AIRFLOW-SERIAL'
|
|
device_info = {}
|
|
data = {}
|
|
|
|
def __init__(self, resources):
|
|
self.last_resources = resources
|
|
self.commands = []
|
|
|
|
def resource(self, href):
|
|
return self.last_resources.get(href, {})
|
|
|
|
async def async_send_command(self, bound, payload):
|
|
self.commands.append((bound, payload))
|
|
|
|
|
|
def _entity(resources, coordinator=None):
|
|
bound = discover(
|
|
resources,
|
|
air_purifier.REGISTRY.capabilities,
|
|
air_purifier.REGISTRY.pattern_capabilities,
|
|
)
|
|
fan_bound = next(
|
|
item for item in bound
|
|
if isinstance(item.desc, FanDesc) and item.href == HREF_AIRFLOW
|
|
)
|
|
return LocalThingsAirflowFan(
|
|
coordinator or _FakeCoordinator(resources), fan_bound,
|
|
)
|
|
|
|
|
|
def test_power_off_maps_to_zero_percent_and_five_retained_speeds():
|
|
entity = _entity(_load_device('air_purifier'))
|
|
assert entity.is_on is False
|
|
assert entity.speed_count == 5
|
|
assert entity.percentage == 0
|
|
|
|
|
|
def test_active_codes_map_to_ordered_percentages():
|
|
"""Confirmed via issue #56's second, properly-spaced diagnostics round:
|
|
/airflow/0's speed is a clean 0-4 code across Auto/Sleep/Low/Medium/High."""
|
|
resources = _load_device('air_purifier')
|
|
resources['/power/0']['value'] = True
|
|
|
|
for code, expected_percentage in ((0, 20), (1, 40), (2, 60), (3, 80), (4, 100)):
|
|
resources['/airflow/0']['speed'] = code
|
|
assert _entity(resources).percentage == expected_percentage
|
|
|
|
|
|
async def test_power_write_prefers_standard_resource_when_both_exist():
|
|
"""Both /power/0 and /power/vs/0 are present on this family's dumps --
|
|
/power/0 must win, matching common.POWER_GENERIC's own preference (the
|
|
power_switch entity is unconditionally bound to /power/0 whenever it's
|
|
present, so writing here to /power/vs/0 first would leave the two
|
|
entities disagreeing until the next poll)."""
|
|
resources = _load_device('air_purifier')
|
|
coordinator = _FakeCoordinator(resources)
|
|
entity = _entity(resources, coordinator)
|
|
|
|
await entity.async_turn_on()
|
|
|
|
assert coordinator.commands[-1][1] == ('power', True, '/power/0')
|
|
|
|
|
|
async def test_power_write_falls_back_to_vendor_resource():
|
|
resources = _load_device('air_purifier')
|
|
resources.pop('/power/0')
|
|
coordinator = _FakeCoordinator(resources)
|
|
entity = _entity(resources, coordinator)
|
|
|
|
await entity.async_turn_off()
|
|
|
|
assert coordinator.commands[-1][1] == ('power', False, '/power/vs/0')
|
|
|
|
|
|
async def test_set_percentage_writes_raw_speed_code():
|
|
resources = _load_device('air_purifier')
|
|
resources['/power/0']['value'] = True
|
|
coordinator = _FakeCoordinator(resources)
|
|
entity = _entity(resources, coordinator)
|
|
|
|
await entity.async_set_percentage(60)
|
|
|
|
assert coordinator.commands[-1][1] == ('speed', 2)
|
|
|
|
|
|
async def test_set_percentage_zero_turns_off():
|
|
resources = _load_device('air_purifier')
|
|
resources['/power/0']['value'] = True
|
|
coordinator = _FakeCoordinator(resources)
|
|
entity = _entity(resources, coordinator)
|
|
|
|
await entity.async_set_percentage(0)
|
|
|
|
assert coordinator.commands[-1][1] == ('power', False, '/power/0')
|