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
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ repos:
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude: ^.*\.patch$
exclude_types: [ csv, json ]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
Expand Down
2 changes: 2 additions & 0 deletions custom_components/schellenberg_usb/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,8 @@ def _handle_event(self, event: str) -> None:
self._attr_is_opening = logical_opening
self._attr_is_closing = not logical_opening
self._move_start_time = time.monotonic()
if self._attr_current_cover_position is None:
self._attr_current_cover_position = 0
self._move_start_position = self._attr_current_cover_position
self._position_source_kind = "primary status"
self._position_confirmed_since_restart = True
Expand Down
13 changes: 8 additions & 5 deletions tests/test_api_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ async def _complete_transmit(_: str) -> bool:
api._handle_message("t0")
return True

api._wait_for_transmitter_idle = AsyncMock(side_effect=_complete_transmit)
wait_for_idle = AsyncMock(side_effect=_complete_transmit)
setattr(api, "_wait_for_transmitter_idle", wait_for_idle)

with caplog.at_level("WARNING"):
assert await api.teach_motor("0d", device_id="F2B8D5", source="developer_tools")
Expand All @@ -154,7 +155,7 @@ async def _complete_transmit(_: str) -> bool:
b"ss0D9600000\r\n",
b"ss0D9400000\r\n",
]
assert api._wait_for_transmitter_idle.await_args_list == [
assert wait_for_idle.await_args_list == [
call("finishing motor teach phase teach_60"),
call("finishing motor teach phase finish_40"),
]
Expand Down Expand Up @@ -183,12 +184,13 @@ async def test_raw_transmit_preserves_exact_protocol_slots(
api._transport = mock_transport
api._is_connected = True
api._device_mode = "listening"
api._wait_for_transmitter_idle = AsyncMock(return_value=True)
wait_for_idle = AsyncMock(return_value=True)
setattr(api, "_wait_for_transmitter_idle", wait_for_idle)

assert await api.send_raw_transmit(payload)

mock_transport.write.assert_called_once_with(expected)
api._wait_for_transmitter_idle.assert_awaited_once_with("finishing raw RF transmit")
wait_for_idle.assert_awaited_once_with("finishing raw RF transmit")


@pytest.mark.asyncio
Expand Down Expand Up @@ -222,7 +224,8 @@ async def test_raw_transmit_timeout_latches_busy_state(hass: HomeAssistant) -> N
api._transport = mock_transport
api._is_connected = True
api._device_mode = "listening"
api._wait_for_transmitter_idle = AsyncMock(return_value=False)
wait_for_idle = AsyncMock(return_value=False)
setattr(api, "_wait_for_transmitter_idle", wait_for_idle)

assert not await api.send_raw_transmit("ss109010000")

Expand Down
4 changes: 3 additions & 1 deletion tests/test_api_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,9 @@ def test_manual_position_sync_dispatches_and_retains_last_confirmation(
new_position=45,
status="estimated",
)
assert api.get_last_position_update("F2B8D5")["new_position"] == 45
latest_position_update = api.get_last_position_update("F2B8D5")
assert latest_position_update is not None
assert latest_position_update["new_position"] == 45
assert api.get_last_manual_position_sync("F2B8D5") == manual_update


Expand Down
24 changes: 15 additions & 9 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
from __future__ import annotations

from types import MappingProxyType
from uuid import UUID
from unittest.mock import AsyncMock, MagicMock, call, patch
from uuid import UUID

import pytest
from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigEntries,
ConfigSubentry,
ConfigSubentryFlow,
SOURCE_RECONFIGURE,
SOURCE_USER,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
Expand All @@ -23,10 +23,10 @@
SchellenbergPairingSubentryFlow,
)
from custom_components.schellenberg_usb.const import (
CONF_BLIND_ID,
CMD_DOWN,
CMD_STOP,
CMD_UP,
CONF_BLIND_ID,
CONF_CLOSE_TIME,
CONF_CLOSE_TIME_SECONDS,
CONF_COMMAND_DEVICE_ID,
Expand Down Expand Up @@ -573,14 +573,17 @@ async def test_developer_tools_show_restored_startup_confidence() -> None:
copy_result = await flow.async_step_copy_diagnostics()

placeholders = result["description_placeholders"]
assert placeholders is not None
assert placeholders["current_position"] == "75%"
assert placeholders["position_source"] == "restored HA state"
assert placeholders["position_confidence"] == (
"restored / estimated / not confirmed since restart"
)
assert placeholders["position_confirmed_since_restart"] == "No"

diagnostics = copy_result["data_schema"]({})["diagnostics"]
data_schema = copy_result["data_schema"]
assert data_schema is not None
diagnostics = data_schema({})["diagnostics"]
assert "Current estimated position: 75" in diagnostics
assert "Source: restored HA state" in diagnostics
assert (
Expand Down Expand Up @@ -681,13 +684,15 @@ async def test_developer_manual_position_form_uses_current_position_and_submits(
result = await flow.async_step_set_position_manual({"position": 63})

assert form["step_id"] == "set_position_manual"
assert form["data_schema"] is not None
assert form["data_schema"]({})["position"] == 37
assert form["description_placeholders"] == {
"selected_blind": "Sitting room door",
"current_position": "37%",
}
api.manual_sync_position.assert_called_once_with("F2B8D5", 63)
assert result["step_id"] == "developer_tools"
assert result["description_placeholders"] is not None
assert "confirmed at 63%" in result["description_placeholders"]["result"]


Expand Down Expand Up @@ -746,6 +751,7 @@ async def test_original_remote_discovery_saves_primary_secondary_and_provenance(
assert form["step_id"] == "discover_status"
assert confirmation["step_id"] == "confirm_status_discovery"
placeholders = confirmation["description_placeholders"]
assert placeholders is not None
assert placeholders["command_identity"] == "06C5C0/11"
assert placeholders["primary_identity"] == "3720B8/08"
assert placeholders["primary_commands"] == "01, 00, 02"
Expand Down Expand Up @@ -798,10 +804,9 @@ async def test_original_remote_discovery_keeps_status_unknown_when_unrecognized(
confirmation = await flow.async_step_discover_status({})
result = await flow.async_step_confirm_status_discovery({})

assert (
"No remote/status tracking identity"
in confirmation["description_placeholders"]["position_tracking"]
)
placeholders = confirmation["description_placeholders"]
assert placeholders is not None
assert "No remote/status tracking identity" in placeholders["position_tracking"]
assert result["type"] is FlowResultType.CREATE_ENTRY
assert CONF_STATUS_DEVICE_ID not in result["data"]
assert CONF_STATUS_ENUM not in result["data"]
Expand Down Expand Up @@ -916,6 +921,7 @@ async def test_developer_raw_command_validates_and_uses_api() -> None:
result = await flow.async_step_send_raw_command({"payload": "ss109010000"})

assert form["step_id"] == "send_raw_command"
assert form["data_schema"] is not None
assert form["data_schema"]({})["payload"] == "ss109010000"
api.send_raw_transmit.assert_awaited_once_with(
"ss109010000", source="developer_tools"
Expand Down
70 changes: 47 additions & 23 deletions tests/test_cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er

from custom_components.schellenberg_usb.api import SchellenbergUsbApi
from custom_components.schellenberg_usb.const import (
Expand Down Expand Up @@ -184,29 +184,26 @@ async def test_setup_restores_manual_cover_from_persisted_subentry(
source="user",
unique_id="/dev/ttyUSB0",
discovery_keys=MappingProxyType({}),
subentries_data=[
{
"subentry_id": "manual_blind",
"subentry_type": SUBENTRY_TYPE_BLIND,
"title": "Sitting room door",
"unique_id": "F2B8D5",
"data": {
CONF_BLIND_ID: TEST_BLIND_ID,
CONF_DEVICE_ID: "F2B8D5",
CONF_DEVICE_ENUM: "23",
CONF_COMMAND_DEVICE_ID: "F2B8D5",
CONF_COMMAND_ENUM: command_enum,
CONF_STATUS_DEVICE_ID: "3720B8",
CONF_STATUS_ENUM: status_enum,
CONF_SECONDARY_STATUS_IDENTITIES: [
{"device_id": "F2B8D5", "enum": "23"}
],
CONF_OPEN_TIME: 25.06,
CONF_CLOSE_TIME: 23.05,
},
}
],
subentries_data=None,
)
subentry = MagicMock()
subentry.subentry_id = "manual_blind"
subentry.subentry_type = SUBENTRY_TYPE_BLIND
subentry.title = "Sitting room door"
subentry.unique_id = "F2B8D5"
subentry.data = {
CONF_BLIND_ID: TEST_BLIND_ID,
CONF_DEVICE_ID: "F2B8D5",
CONF_DEVICE_ENUM: "23",
CONF_COMMAND_DEVICE_ID: "F2B8D5",
CONF_COMMAND_ENUM: command_enum,
CONF_STATUS_DEVICE_ID: "3720B8",
CONF_STATUS_ENUM: status_enum,
CONF_SECONDARY_STATUS_IDENTITIES: [{"device_id": "F2B8D5", "enum": "23"}],
CONF_OPEN_TIME: 25.06,
CONF_CLOSE_TIME: 23.05,
}
entry.subentries = MappingProxyType({"manual_blind": subentry}) # type: ignore[misc]
entry.runtime_data = mock_api
hass.config_entries._entries[entry.entry_id] = entry
add_entities = MagicMock()
Expand Down Expand Up @@ -730,6 +727,33 @@ async def test_cover_handle_started_moving_down(
assert cover._move_start_position == 100


@pytest.mark.asyncio
async def test_cover_handle_started_moving_up_without_known_position_defaults_to_zero(
hass: HomeAssistant,
mock_api: SchellenbergUsbApi,
) -> None:
"""Test movement events default to a closed-start estimate when no prior position exists."""
cover = SchellenbergCover(
api=mock_api,
device_id="ABC123",
device_enum="01",
device_name="Test Cover",
)
cover.hass = hass
cover._attr_current_cover_position = None

with (
patch.object(cover, "_start_position_tracking") as start_tracking,
patch.object(cover, "async_write_ha_state"),
):
cover._handle_event(EVENT_STARTED_MOVING_UP)

assert cover._attr_is_opening is True
assert cover._attr_is_closing is False
assert cover._move_start_position == 0
start_tracking.assert_called_once_with()


@pytest.mark.asyncio
async def test_cover_handle_stopped(
hass: HomeAssistant,
Expand Down
Loading