Update docs and screenshots
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,682 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.dmx.engine import DmxEngine
|
||||
from app.dmx.frame import DmxFrame
|
||||
from app.homeassistant.service import HomeAssistantService
|
||||
from app.telemetry.service import TelemetryService
|
||||
|
||||
|
||||
class FakeHomeAssistantAdapter:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, str, str, dict[str, object]]] = []
|
||||
self.list_payload: list[dict[str, object]] = [
|
||||
{
|
||||
"entity_id": "light.bar_rgb",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"friendly_name": "Bar RGB",
|
||||
"supported_color_modes": ["rgb", "brightness"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"entity_id": "light.bar_rgbw",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"friendly_name": "Bar RGBW",
|
||||
"supported_color_modes": ["rgbw", "brightness"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"entity_id": "light.bar_cct",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"friendly_name": "Bar CCT",
|
||||
"supported_color_modes": ["color_temp", "brightness"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"entity_id": "light.test",
|
||||
"state": "off",
|
||||
"attributes": {
|
||||
"friendly_name": "Test light",
|
||||
"supported_color_modes": ["brightness"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"entity_id": "switch.test",
|
||||
"state": "off",
|
||||
"attributes": {
|
||||
"friendly_name": "Test switch",
|
||||
},
|
||||
},
|
||||
{
|
||||
"entity_id": "input_boolean.test",
|
||||
"state": "off",
|
||||
"attributes": {
|
||||
"friendly_name": "Test bool",
|
||||
},
|
||||
},
|
||||
{
|
||||
"entity_id": "scene.test",
|
||||
"state": "scening",
|
||||
"attributes": {
|
||||
"friendly_name": "Test scene",
|
||||
},
|
||||
},
|
||||
{
|
||||
"entity_id": "automation.test",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"friendly_name": "Test automation",
|
||||
},
|
||||
},
|
||||
]
|
||||
self.connection_error: Exception | None = None
|
||||
self.dispatch_error: Exception | None = None
|
||||
|
||||
async def get_api_root(self, base_url: str, token: str) -> dict[str, object]:
|
||||
if self.connection_error is not None:
|
||||
raise self.connection_error
|
||||
return {"message": "API running."}
|
||||
|
||||
async def get_config(self, base_url: str, token: str) -> dict[str, object]:
|
||||
if self.connection_error is not None:
|
||||
raise self.connection_error
|
||||
return {"version": "2026.7.0"}
|
||||
|
||||
async def list_entities(self, base_url: str, token: str) -> list[dict[str, object]]:
|
||||
if self.connection_error is not None:
|
||||
raise self.connection_error
|
||||
return self.list_payload
|
||||
|
||||
async def call_service(
|
||||
self,
|
||||
base_url: str,
|
||||
token: str,
|
||||
domain: str,
|
||||
service: str,
|
||||
data: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
if self.dispatch_error is not None:
|
||||
raise self.dispatch_error
|
||||
self.calls.append((base_url, token, f"{domain}.{service}", data))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def build_service(adapter: FakeHomeAssistantAdapter) -> HomeAssistantService:
|
||||
engine = DmxEngine(TelemetryService())
|
||||
service = HomeAssistantService(engine, adapter=adapter)
|
||||
service._config = {
|
||||
"enabled": True,
|
||||
"base_url": "http://ha.local:8123",
|
||||
"default_universe": 10,
|
||||
}
|
||||
service._token = "test-token"
|
||||
return service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_home_assistant_connection_reports_version_and_auth() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
|
||||
result = await service.test_connection()
|
||||
|
||||
assert result["reachable"] is True
|
||||
assert result["auth_ok"] is True
|
||||
assert result["ha_version"] == "2026.7.0"
|
||||
config = await service.get_config()
|
||||
assert config["has_token"] is True
|
||||
assert config["token_mask"] == "********"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_home_assistant_rgb_mapping_reads_virtual_universe_10() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
frame.set_channel(1, 255, "scene")
|
||||
frame.set_channel(2, 200, "scene")
|
||||
frame.set_channel(3, 100, "scene")
|
||||
frame.set_channel(4, 50, "scene")
|
||||
service.engine.current_frames = {1: DmxFrame(universe=1), 10: frame}
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Bar RGB",
|
||||
"universe": 10,
|
||||
"start_address": 1,
|
||||
"fixture_type": "rgb",
|
||||
"entity_id": "light.bar_rgb",
|
||||
"rate_limit_hz": 5,
|
||||
"deadband": 1,
|
||||
"fade_ms": 150,
|
||||
"enabled": True,
|
||||
"master_dimmer": True,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.dispatch_once()
|
||||
|
||||
assert len(adapter.calls) == 1
|
||||
_base_url, _token, service_name, data = adapter.calls[0]
|
||||
assert service_name == "light.turn_on"
|
||||
assert data["entity_id"] == "light.bar_rgb"
|
||||
assert data["brightness"] == 255
|
||||
assert data["rgb_color"] == [200, 100, 50]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_home_assistant_rgbw_mapping_at_channel_512_uses_last_channel() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
frame.set_channel(508, 255, "scene")
|
||||
frame.set_channel(509, 10, "scene")
|
||||
frame.set_channel(510, 20, "scene")
|
||||
frame.set_channel(511, 30, "scene")
|
||||
frame.set_channel(512, 40, "scene")
|
||||
service.engine.current_frames = {10: frame}
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Bar RGBW",
|
||||
"universe": 10,
|
||||
"start_address": 508,
|
||||
"fixture_type": "rgbw",
|
||||
"entity_id": "light.bar_rgbw",
|
||||
"rate_limit_hz": 5,
|
||||
"deadband": 1,
|
||||
"fade_ms": 100,
|
||||
"enabled": True,
|
||||
"master_dimmer": True,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.dispatch_once()
|
||||
|
||||
assert adapter.calls[0][2] == "light.turn_on"
|
||||
assert adapter.calls[0][3]["rgbw_color"] == [10, 20, 30, 40]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_home_assistant_rate_limit_prevents_duplicate_spam() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
frame.set_channel(1, 180, "scene")
|
||||
service.engine.current_frames = {10: frame}
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Dimmer",
|
||||
"universe": 10,
|
||||
"start_address": 1,
|
||||
"fixture_type": "dimmer",
|
||||
"entity_id": "light.bar_rgb",
|
||||
"rate_limit_hz": 5,
|
||||
"deadband": 2,
|
||||
"fade_ms": 0,
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.dispatch_once()
|
||||
await service.dispatch_once()
|
||||
|
||||
assert len(adapter.calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scene_mapping_uses_rising_edge_only() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
service.engine.current_frames = {10: frame}
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Party scene",
|
||||
"universe": 10,
|
||||
"start_address": 6,
|
||||
"fixture_type": "scene",
|
||||
"entity_id": "scene.party_mode",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.dispatch_once()
|
||||
frame.set_channel(6, 140, "scene")
|
||||
await service.dispatch_once()
|
||||
await service.dispatch_once()
|
||||
|
||||
assert len(adapter.calls) == 1
|
||||
assert adapter.calls[0][2] == "scene.turn_on"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_mapping_uses_hysteresis() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
service.engine.current_frames = {10: frame}
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Switch",
|
||||
"universe": 10,
|
||||
"start_address": 50,
|
||||
"fixture_type": "switch",
|
||||
"entity_id": "switch.party_relay",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
frame.set_channel(50, 150, "scene")
|
||||
await service.dispatch_once()
|
||||
frame.set_channel(50, 130, "scene")
|
||||
await service.dispatch_once()
|
||||
frame.set_channel(50, 110, "scene")
|
||||
await service.dispatch_once()
|
||||
|
||||
assert [call[2] for call in adapter.calls] == ["switch.turn_on", "switch.turn_off"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_fixture_routes_light_entity_by_entity_domain() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
service.engine.current_frames = {10: frame}
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Light as switch",
|
||||
"universe": 10,
|
||||
"start_address": 20,
|
||||
"fixture_type": "switch",
|
||||
"entity_id": "light.test",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
frame.set_channel(20, 150, "scene")
|
||||
await service.dispatch_once()
|
||||
frame.set_channel(20, 110, "scene")
|
||||
await service.dispatch_once()
|
||||
|
||||
mapping = (await service.list_mappings())["items"][0]
|
||||
assert [call[2] for call in adapter.calls] == ["light.turn_on", "light.turn_off"]
|
||||
assert mapping["last_sent_summary"] == "light.turn_off light.test"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_fixture_routes_switch_entity_by_entity_domain() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
service.engine.current_frames = {10: frame}
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 11,
|
||||
"name": "Switch domain",
|
||||
"universe": 10,
|
||||
"start_address": 21,
|
||||
"fixture_type": "switch",
|
||||
"entity_id": "switch.test",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
frame.set_channel(21, 150, "scene")
|
||||
await service.dispatch_once()
|
||||
frame.set_channel(21, 110, "scene")
|
||||
await service.dispatch_once()
|
||||
|
||||
assert [call[2] for call in adapter.calls] == ["switch.turn_on", "switch.turn_off"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dimmer_fixture_routes_light_entity_with_brightness() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
frame.set_channel(22, 180, "scene")
|
||||
service.engine.current_frames = {10: frame}
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Dimmer light",
|
||||
"universe": 10,
|
||||
"start_address": 22,
|
||||
"fixture_type": "dimmer",
|
||||
"entity_id": "light.test",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.dispatch_once()
|
||||
|
||||
assert adapter.calls[0][2] == "light.turn_on"
|
||||
assert adapter.calls[0][3]["brightness"] == 180
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scene_fixture_routes_scene_entity() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
service.engine.current_frames = {10: frame}
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Scene test",
|
||||
"universe": 10,
|
||||
"start_address": 23,
|
||||
"fixture_type": "scene",
|
||||
"entity_id": "scene.test",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
frame.set_channel(23, 200, "scene")
|
||||
await service.dispatch_once()
|
||||
|
||||
assert adapter.calls[0][2] == "scene.turn_on"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_automation_fixture_routes_automation_entity() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
service.engine.current_frames = {10: frame}
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Automation test",
|
||||
"universe": 10,
|
||||
"start_address": 24,
|
||||
"fixture_type": "automation",
|
||||
"entity_id": "automation.test",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
frame.set_channel(24, 200, "scene")
|
||||
await service.dispatch_once()
|
||||
|
||||
assert adapter.calls[0][2] == "automation.trigger"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_test_mapping_uses_same_entity_domain_routing() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 15,
|
||||
"name": "Test mapping light switch",
|
||||
"universe": 10,
|
||||
"start_address": 25,
|
||||
"fixture_type": "switch",
|
||||
"entity_id": "light.test",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.test_mapping(15)
|
||||
|
||||
assert adapter.calls[0][2] == "light.turn_on"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_test_mapping_resyncs_back_to_live_dmx() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
frame.set_channel(1, 120, "scene")
|
||||
service.engine.current_frames = {10: frame}
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Dimmer",
|
||||
"universe": 10,
|
||||
"start_address": 1,
|
||||
"fixture_type": "dimmer",
|
||||
"entity_id": "light.bar_rgb",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.test_mapping(6)
|
||||
await service.dispatch_once()
|
||||
|
||||
assert adapter.calls[0][3]["brightness"] == 255
|
||||
assert adapter.calls[-1][3]["brightness"] == 120
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_error_is_tracked_without_blocking() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
adapter.dispatch_error = httpx.ReadTimeout("timeout")
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
frame.set_channel(1, 180, "scene")
|
||||
service.engine.current_frames = {10: frame}
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Dimmer",
|
||||
"universe": 10,
|
||||
"start_address": 1,
|
||||
"fixture_type": "dimmer",
|
||||
"entity_id": "light.bar_rgb",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.dispatch_once()
|
||||
|
||||
mapping = (await service.list_mappings())["items"][0]
|
||||
assert mapping["last_error"] == "Timeout ved kald til Home Assistant"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_401_is_reported_from_connection_test() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
request = httpx.Request("GET", "http://ha.local:8123/api/")
|
||||
response = httpx.Response(401, request=request)
|
||||
adapter.connection_error = httpx.HTTPStatusError("401", request=request, response=response)
|
||||
service = build_service(adapter)
|
||||
|
||||
result = await service.test_connection()
|
||||
|
||||
assert result["reachable"] is True
|
||||
assert result["auth_ok"] is False
|
||||
assert result["last_error"] == "HTTP 401 fra Home Assistant"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_404_entity_not_found_is_tracked() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
request = httpx.Request("POST", "http://ha.local:8123/api/services/light/turn_on")
|
||||
response = httpx.Response(404, request=request)
|
||||
adapter.dispatch_error = httpx.HTTPStatusError("404", request=request, response=response)
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
frame.set_channel(1, 180, "scene")
|
||||
service.engine.current_frames = {10: frame}
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Dimmer",
|
||||
"universe": 10,
|
||||
"start_address": 1,
|
||||
"fixture_type": "dimmer",
|
||||
"entity_id": "light.bar_rgb",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
await service.dispatch_once()
|
||||
|
||||
mapping = (await service.list_mappings())["items"][0]
|
||||
assert mapping["last_error"] == "HTTP 404 fra Home Assistant"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_home_assistant_unavailable_is_reported() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
adapter.connection_error = httpx.ConnectError("offline")
|
||||
service = build_service(adapter)
|
||||
|
||||
result = await service.test_connection()
|
||||
|
||||
assert result["reachable"] is False
|
||||
assert result["auth_ok"] is False
|
||||
assert result["last_error"] == "Home Assistant kunne ikke kontaktes"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persistence_survives_service_reload() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
await service.save_config(
|
||||
{
|
||||
"enabled": True,
|
||||
"base_url": "http://ha.local:8123",
|
||||
"token": "persistent-token",
|
||||
"default_universe": 10,
|
||||
}
|
||||
)
|
||||
await service.create_mapping(
|
||||
{
|
||||
"name": "Persisted RGB",
|
||||
"universe": 10,
|
||||
"start_address": 1,
|
||||
"fixture_type": "rgb",
|
||||
"entity_id": "light.bar_rgb",
|
||||
"enabled": True,
|
||||
"master_dimmer": True,
|
||||
}
|
||||
)
|
||||
|
||||
reloaded = HomeAssistantService(service.engine, adapter=adapter)
|
||||
await reloaded._load()
|
||||
|
||||
config = await reloaded.get_config()
|
||||
mappings = await reloaded.list_mappings()
|
||||
assert config["base_url"] == "http://ha.local:8123"
|
||||
assert config["has_token"] is True
|
||||
assert mappings["items"][0]["entity_id"] == "light.bar_rgb"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_slow_dispatch_drops_intermediate_frames_and_keeps_latest() -> None:
|
||||
adapter = FakeHomeAssistantAdapter()
|
||||
service = build_service(adapter)
|
||||
frame = DmxFrame(universe=10)
|
||||
frame.set_channel(1, 50, "scene")
|
||||
service.engine.current_frames = {10: frame}
|
||||
await service.list_entities()
|
||||
service._mappings = [
|
||||
service._normalize_mapping(
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Dimmer",
|
||||
"universe": 10,
|
||||
"start_address": 1,
|
||||
"fixture_type": "dimmer",
|
||||
"entity_id": "light.bar_rgb",
|
||||
"enabled": True,
|
||||
"master_dimmer": False,
|
||||
"deadband": 1,
|
||||
"rate_limit_hz": 30,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
first_call = asyncio.Event()
|
||||
release_call = asyncio.Event()
|
||||
|
||||
async def slow_call(
|
||||
base_url: str,
|
||||
token: str,
|
||||
domain: str,
|
||||
service_name: str,
|
||||
data: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
adapter.calls.append((base_url, token, f"{domain}.{service_name}", data))
|
||||
first_call.set()
|
||||
await release_call.wait()
|
||||
return {"ok": True}
|
||||
|
||||
adapter.call_service = slow_call # type: ignore[method-assign]
|
||||
|
||||
dispatch_task = asyncio.create_task(service._dispatch_enabled_mappings())
|
||||
await first_call.wait()
|
||||
frame.set_channel(1, 120, "scene")
|
||||
await service._dispatch_enabled_mappings()
|
||||
frame.set_channel(1, 220, "scene")
|
||||
await service._dispatch_enabled_mappings()
|
||||
release_call.set()
|
||||
await dispatch_task
|
||||
await service._drain_mapping_tasks()
|
||||
|
||||
assert adapter.calls[0][3]["brightness"] == 50
|
||||
assert adapter.calls[-1][3]["brightness"] == 220
|
||||
assert len(adapter.calls) == 2
|
||||
Reference in New Issue
Block a user