ucapi-framework 1.8.4__tar.gz → 1.9.0b1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/PKG-INFO +1 -1
  2. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/pyproject.toml +1 -1
  3. ucapi_framework-1.9.0b1/tests/test_button_entity.py +154 -0
  4. ucapi_framework-1.9.0b1/tests/test_climate_entity.py +230 -0
  5. ucapi_framework-1.9.0b1/tests/test_cover_entity.py +228 -0
  6. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_driver.py +115 -0
  7. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_entity.py +105 -1
  8. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_helpers.py +13 -5
  9. ucapi_framework-1.9.0b1/tests/test_ir_emitter_entity.py +120 -0
  10. ucapi_framework-1.9.0b1/tests/test_light_entity.py +234 -0
  11. ucapi_framework-1.9.0b1/tests/test_media_player_entity.py +259 -0
  12. ucapi_framework-1.9.0b1/tests/test_remote_entity.py +125 -0
  13. ucapi_framework-1.9.0b1/tests/test_select_entity.py +195 -0
  14. ucapi_framework-1.9.0b1/tests/test_sensor_entity.py +206 -0
  15. ucapi_framework-1.9.0b1/tests/test_switch_entity.py +128 -0
  16. ucapi_framework-1.9.0b1/tests/test_voice_assistant_entity.py +128 -0
  17. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/__init__.py +24 -0
  18. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/driver.py +93 -26
  19. ucapi_framework-1.9.0b1/ucapi_framework/entities/__init__.py +35 -0
  20. ucapi_framework-1.9.0b1/ucapi_framework/entities/button.py +89 -0
  21. ucapi_framework-1.9.0b1/ucapi_framework/entities/climate.py +236 -0
  22. ucapi_framework-1.9.0b1/ucapi_framework/entities/cover.py +161 -0
  23. ucapi_framework-1.9.0b1/ucapi_framework/entities/ir_emitter.py +89 -0
  24. ucapi_framework-1.9.0b1/ucapi_framework/entities/light.py +201 -0
  25. ucapi_framework-1.9.0b1/ucapi_framework/entities/media_player.py +488 -0
  26. ucapi_framework-1.9.0b1/ucapi_framework/entities/remote.py +95 -0
  27. ucapi_framework-1.9.0b1/ucapi_framework/entities/select.py +165 -0
  28. ucapi_framework-1.9.0b1/ucapi_framework/entities/sensor.py +155 -0
  29. ucapi_framework-1.9.0b1/ucapi_framework/entities/switch.py +99 -0
  30. ucapi_framework-1.9.0b1/ucapi_framework/entities/voice_assistant.py +93 -0
  31. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/entity.py +94 -1
  32. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/helpers.py +1 -1
  33. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework.egg-info/PKG-INFO +1 -1
  34. ucapi_framework-1.9.0b1/ucapi_framework.egg-info/SOURCES.txt +48 -0
  35. ucapi_framework-1.8.4/ucapi_framework.egg-info/SOURCES.txt +0 -25
  36. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/README.md +0 -0
  37. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/setup.cfg +0 -0
  38. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_config.py +0 -0
  39. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_device.py +0 -0
  40. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_discovery.py +0 -0
  41. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_factory_pattern.py +0 -0
  42. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_migration.py +0 -0
  43. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/tests/test_setup.py +0 -0
  44. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/config.py +0 -0
  45. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/device.py +0 -0
  46. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/discovery.py +0 -0
  47. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/migration.py +0 -0
  48. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework/setup.py +0 -0
  49. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework.egg-info/dependency_links.txt +0 -0
  50. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework.egg-info/requires.txt +0 -0
  51. {ucapi_framework-1.8.4 → ucapi_framework-1.9.0b1}/ucapi_framework.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucapi-framework
3
- Version: 1.8.4
3
+ Version: 1.9.0b1
4
4
  Summary: ucapi framework that provides core functionality for building integrations.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ucapi-framework"
3
- version = "1.8.4"
3
+ version = "1.9.0b1"
4
4
  description = "ucapi framework that provides core functionality for building integrations."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,154 @@
1
+ """Tests for ButtonEntity with built-in state management."""
2
+
3
+ import pytest
4
+ from unittest.mock import MagicMock
5
+ from ucapi import button
6
+ from ucapi_framework import ButtonEntity
7
+
8
+
9
+ class TestButtonEntity:
10
+ """Test ButtonEntity state management."""
11
+
12
+ @pytest.fixture
13
+ def mock_api(self):
14
+ """Create a mock API for testing."""
15
+ api = MagicMock()
16
+ api.configured_entities.get.return_value = MagicMock(
17
+ attributes={button.Attributes.STATE: button.States.AVAILABLE}
18
+ )
19
+ return api
20
+
21
+ @pytest.fixture
22
+ def entity(self, mock_api):
23
+ """Create a ButtonEntity for testing."""
24
+ entity = ButtonEntity(
25
+ "button.test",
26
+ "Test Button",
27
+ )
28
+ entity._api = mock_api # noqa: SLF001
29
+ return entity
30
+
31
+ def test_initial_state(self, entity):
32
+ """Test initial state from constructor attributes."""
33
+ assert entity.state == button.States.AVAILABLE
34
+
35
+ def test_initial_state_always_available(self, mock_api):
36
+ """Test that button always initializes STATE to AVAILABLE (ucapi hardcodes this)."""
37
+ entity = ButtonEntity(
38
+ "button.unset",
39
+ "Unset Button",
40
+ )
41
+ entity._api = mock_api # noqa: SLF001
42
+ # ucapi.button.Button hardcodes {Attributes.STATE: States.AVAILABLE} in __init__
43
+ assert entity.state == button.States.AVAILABLE
44
+
45
+ def test_set_state_with_update(self, entity, mock_api):
46
+ """Test set_state() calls entity.update() by default."""
47
+ entity.set_state(button.States.UNAVAILABLE, update=True)
48
+
49
+ # Verify internal state was updated
50
+ assert entity.state == button.States.UNAVAILABLE
51
+
52
+ # Verify update was called
53
+ assert mock_api.configured_entities.update_attributes.called
54
+ call_args = mock_api.configured_entities.update_attributes.call_args
55
+ entity_id, attributes = call_args[0]
56
+ assert entity_id == "button.test"
57
+ assert button.Attributes.STATE in attributes
58
+ assert attributes[button.Attributes.STATE] == button.States.UNAVAILABLE
59
+
60
+ def test_set_state_without_update(self, entity, mock_api):
61
+ """Test set_state(update=False) does not call entity.update()."""
62
+ entity.set_state(button.States.UNAVAILABLE, update=False)
63
+
64
+ # Verify internal state was updated
65
+ assert entity.state == button.States.UNAVAILABLE
66
+
67
+ # Verify update was NOT called
68
+ assert not mock_api.configured_entities.update_attributes.called
69
+
70
+ def test_set_state_available(self, entity, mock_api):
71
+ """Test transitioning back to AVAILABLE."""
72
+ entity.set_state(button.States.UNAVAILABLE, update=False)
73
+ assert entity.state == button.States.UNAVAILABLE
74
+
75
+ entity.set_state(button.States.AVAILABLE, update=False)
76
+ assert entity.state == button.States.AVAILABLE
77
+
78
+ def test_set_state_none_does_not_update_remote(self, entity, mock_api):
79
+ """Test set_state(None) stores None but update filter strips it."""
80
+ # set_state(None) writes None into attributes dict
81
+ entity.set_state(None, update=True)
82
+
83
+ # Internal state stores None
84
+ assert entity.state is None
85
+
86
+ # The update should have been called but the None filter in
87
+ # update_attributes strips the None value — so update_attributes
88
+ # is called with an empty dict (or not called if filter removes all)
89
+ # Either way, the Remote should not receive a None value
90
+ if mock_api.configured_entities.update_attributes.called:
91
+ call_args = mock_api.configured_entities.update_attributes.call_args
92
+ _, attributes = call_args[0]
93
+ assert button.Attributes.STATE not in attributes
94
+
95
+ def test_property_getter_is_read_only(self, entity):
96
+ """Test that the state property cannot be set directly."""
97
+ with pytest.raises(AttributeError):
98
+ entity.state = button.States.AVAILABLE # type: ignore[misc]
99
+
100
+ def test_only_one_attribute(self, entity, mock_api):
101
+ """Test that Button only has the STATE attribute."""
102
+ entity.set_state(button.States.UNAVAILABLE, update=True)
103
+
104
+ call_args = mock_api.configured_entities.update_attributes.call_args
105
+ _, attributes = call_args[0]
106
+ # Should only contain STATE
107
+ assert list(attributes.keys()) == [button.Attributes.STATE]
108
+
109
+
110
+ class TestButtonEntityInheritance:
111
+ """Test that ButtonEntity can be subclassed and overridden."""
112
+
113
+ def test_custom_set_state(self):
114
+ """Test that set_state can be overridden."""
115
+
116
+ class CustomButton(ButtonEntity):
117
+ def __init__(self):
118
+ super().__init__(
119
+ "button.custom",
120
+ "Custom Button",
121
+ )
122
+ self.custom_set_state_called = False
123
+
124
+ def set_state(self, value, *, update=True):
125
+ """Override set_state to add custom logic."""
126
+ self.custom_set_state_called = True
127
+ super().set_state(value, update=update)
128
+
129
+ entity = CustomButton()
130
+ entity._api = MagicMock() # noqa: SLF001
131
+ entity._api.configured_entities.get.return_value = entity # noqa: SLF001
132
+
133
+ entity.set_state(button.States.AVAILABLE, update=False)
134
+ assert entity.custom_set_state_called is True
135
+ assert entity.state == button.States.AVAILABLE
136
+
137
+ def test_custom_property_getter(self):
138
+ """Test that the state property getter can be overridden."""
139
+
140
+ class CustomButton(ButtonEntity):
141
+ def __init__(self):
142
+ super().__init__(
143
+ "button.custom",
144
+ "Custom Button",
145
+ )
146
+
147
+ @property
148
+ def state(self):
149
+ """Override state getter to always return AVAILABLE."""
150
+ return button.States.AVAILABLE
151
+
152
+ entity = CustomButton()
153
+ # Property override always returns AVAILABLE
154
+ assert entity.state == button.States.AVAILABLE
@@ -0,0 +1,230 @@
1
+ """Tests for ClimateEntity with built-in state management."""
2
+
3
+ import pytest
4
+ from unittest.mock import MagicMock
5
+ from ucapi import climate
6
+ from ucapi_framework import ClimateEntity
7
+
8
+
9
+ class TestClimateEntity:
10
+ """Test ClimateEntity state management."""
11
+
12
+ @pytest.fixture
13
+ def mock_api(self):
14
+ """Create a mock API for testing."""
15
+ api = MagicMock()
16
+ api.configured_entities.get.return_value = MagicMock(
17
+ attributes={climate.Attributes.STATE: climate.States.OFF}
18
+ )
19
+ return api
20
+
21
+ @pytest.fixture
22
+ def entity(self, mock_api):
23
+ """Create a ClimateEntity for testing."""
24
+ entity = ClimateEntity(
25
+ "climate.test",
26
+ "Test Thermostat",
27
+ features=[climate.Features.ON_OFF, climate.Features.HEAT],
28
+ attributes={climate.Attributes.STATE: climate.States.OFF},
29
+ )
30
+ entity._api = mock_api # noqa: SLF001
31
+ return entity
32
+
33
+ def test_initial_state(self, entity):
34
+ """Test initial state from constructor attributes."""
35
+ assert entity.state == climate.States.OFF
36
+ assert entity.current_temperature is None
37
+ assert entity.target_temperature is None
38
+ assert entity.target_temperature_high is None
39
+ assert entity.target_temperature_low is None
40
+ assert entity.fan_mode is None
41
+
42
+ def test_set_state_with_update(self, entity, mock_api):
43
+ """Test set_state() calls entity.update() by default."""
44
+ entity.set_state(climate.States.HEAT, update=True)
45
+
46
+ assert entity.state == climate.States.HEAT
47
+
48
+ assert mock_api.configured_entities.update_attributes.called
49
+ call_args = mock_api.configured_entities.update_attributes.call_args
50
+ entity_id, attributes = call_args[0]
51
+ assert entity_id == "climate.test"
52
+ assert climate.Attributes.STATE in attributes
53
+ assert attributes[climate.Attributes.STATE] == climate.States.HEAT
54
+
55
+ def test_set_state_without_update(self, entity, mock_api):
56
+ """Test set_state(update=False) does not call entity.update()."""
57
+ entity.set_state(climate.States.COOL, update=False)
58
+
59
+ assert entity.state == climate.States.COOL
60
+ assert not mock_api.configured_entities.update_attributes.called
61
+
62
+ def test_set_current_temperature(self, entity, mock_api):
63
+ """Test set_current_temperature() updates state and calls update."""
64
+ entity.set_current_temperature(22.5, update=True)
65
+
66
+ assert entity.current_temperature == 22.5
67
+ assert mock_api.configured_entities.update_attributes.called
68
+
69
+ def test_set_current_temperature_without_update(self, entity, mock_api):
70
+ """Test set_current_temperature(update=False) does not call update."""
71
+ entity.set_current_temperature(22.5, update=False)
72
+
73
+ assert entity.current_temperature == 22.5
74
+ assert not mock_api.configured_entities.update_attributes.called
75
+
76
+ def test_set_target_temperature(self, entity, mock_api):
77
+ """Test set_target_temperature() updates state and calls update."""
78
+ entity.set_target_temperature(21.0, update=True)
79
+
80
+ assert entity.target_temperature == 21.0
81
+ assert mock_api.configured_entities.update_attributes.called
82
+
83
+ def test_set_target_temperature_without_update(self, entity, mock_api):
84
+ """Test set_target_temperature(update=False) does not call update."""
85
+ entity.set_target_temperature(21.0, update=False)
86
+
87
+ assert entity.target_temperature == 21.0
88
+ assert not mock_api.configured_entities.update_attributes.called
89
+
90
+ def test_set_target_temperature_high(self, entity, mock_api):
91
+ """Test set_target_temperature_high() updates state and calls update."""
92
+ entity.set_target_temperature_high(25.0, update=True)
93
+
94
+ assert entity.target_temperature_high == 25.0
95
+ assert mock_api.configured_entities.update_attributes.called
96
+
97
+ def test_set_target_temperature_low(self, entity, mock_api):
98
+ """Test set_target_temperature_low() updates state and calls update."""
99
+ entity.set_target_temperature_low(18.0, update=True)
100
+
101
+ assert entity.target_temperature_low == 18.0
102
+ assert mock_api.configured_entities.update_attributes.called
103
+
104
+ def test_set_fan_mode(self, entity, mock_api):
105
+ """Test set_fan_mode() updates state and calls update."""
106
+ entity.set_fan_mode("AUTO", update=True)
107
+
108
+ assert entity.fan_mode == "AUTO"
109
+ assert mock_api.configured_entities.update_attributes.called
110
+
111
+ def test_set_fan_mode_without_update(self, entity, mock_api):
112
+ """Test set_fan_mode(update=False) does not call update."""
113
+ entity.set_fan_mode("HIGH", update=False)
114
+
115
+ assert entity.fan_mode == "HIGH"
116
+ assert not mock_api.configured_entities.update_attributes.called
117
+
118
+ def test_set_attributes_bulk_update(self, entity, mock_api):
119
+ """Test set_attributes() updates multiple attributes with single update call."""
120
+ entity.set_attributes(
121
+ state=climate.States.HEAT,
122
+ current_temperature=20.5,
123
+ target_temperature=22.0,
124
+ fan_mode="AUTO",
125
+ update=True,
126
+ )
127
+
128
+ assert entity.state == climate.States.HEAT
129
+ assert entity.current_temperature == 20.5
130
+ assert entity.target_temperature == 22.0
131
+ assert entity.fan_mode == "AUTO"
132
+
133
+ # Verify update was called only once
134
+ assert mock_api.configured_entities.update_attributes.call_count == 1
135
+
136
+ call_args = mock_api.configured_entities.update_attributes.call_args
137
+ entity_id, attributes = call_args[0]
138
+ assert entity_id == "climate.test"
139
+ assert len(attributes) == 4
140
+ assert attributes[climate.Attributes.STATE] == climate.States.HEAT
141
+ assert attributes[climate.Attributes.CURRENT_TEMPERATURE] == 20.5
142
+ assert attributes[climate.Attributes.TARGET_TEMPERATURE] == 22.0
143
+ assert attributes[climate.Attributes.FAN_MODE] == "AUTO"
144
+
145
+ def test_set_attributes_without_update(self, entity, mock_api):
146
+ """Test set_attributes(update=False) does not call entity.update()."""
147
+ entity.set_attributes(
148
+ state=climate.States.COOL,
149
+ target_temperature=19.0,
150
+ update=False,
151
+ )
152
+
153
+ assert entity.state == climate.States.COOL
154
+ assert entity.target_temperature == 19.0
155
+ assert not mock_api.configured_entities.update_attributes.called
156
+
157
+ def test_set_attributes_ignores_none_values(self, entity, mock_api):
158
+ """Test set_attributes() ignores None values."""
159
+ entity.set_attributes(
160
+ state=climate.States.HEAT, current_temperature=None, update=True
161
+ )
162
+
163
+ assert entity.state == climate.States.HEAT
164
+ assert entity.current_temperature is None
165
+
166
+ call_args = mock_api.configured_entities.update_attributes.call_args
167
+ entity_id, attributes = call_args[0]
168
+ assert len(attributes) == 1
169
+ assert climate.Attributes.STATE in attributes
170
+
171
+ def test_set_attributes_all_six(self, entity, mock_api):
172
+ """Test setting all six climate attributes at once."""
173
+ entity.set_attributes(
174
+ state=climate.States.HEAT_COOL,
175
+ current_temperature=21.0,
176
+ target_temperature=22.0,
177
+ target_temperature_high=25.0,
178
+ target_temperature_low=18.0,
179
+ fan_mode="AUTO",
180
+ update=True,
181
+ )
182
+
183
+ assert entity.state == climate.States.HEAT_COOL
184
+ assert entity.current_temperature == 21.0
185
+ assert entity.target_temperature == 22.0
186
+ assert entity.target_temperature_high == 25.0
187
+ assert entity.target_temperature_low == 18.0
188
+ assert entity.fan_mode == "AUTO"
189
+
190
+ assert mock_api.configured_entities.update_attributes.call_count == 1
191
+
192
+ def test_property_getters_are_read_only(self, entity):
193
+ """Test that property getters cannot be set directly."""
194
+ with pytest.raises(AttributeError):
195
+ entity.state = climate.States.HEAT # type: ignore[misc]
196
+
197
+ with pytest.raises(AttributeError):
198
+ entity.current_temperature = 20.0 # type: ignore[misc]
199
+
200
+ with pytest.raises(AttributeError):
201
+ entity.target_temperature = 21.0 # type: ignore[misc]
202
+
203
+
204
+ class TestClimateEntityInheritance:
205
+ """Test that ClimateEntity can be subclassed and overridden."""
206
+
207
+ def test_custom_set_state(self):
208
+ """Test that set_state can be overridden."""
209
+
210
+ class CustomClimate(ClimateEntity):
211
+ def __init__(self):
212
+ super().__init__(
213
+ "climate.custom",
214
+ "Custom Thermostat",
215
+ features=[],
216
+ attributes={},
217
+ )
218
+ self.custom_called = False
219
+
220
+ def set_state(self, value, *, update=True):
221
+ self.custom_called = True
222
+ super().set_state(value, update=update)
223
+
224
+ entity = CustomClimate()
225
+ entity._api = MagicMock() # noqa: SLF001
226
+ entity._api.configured_entities.get.return_value = entity # noqa: SLF001
227
+
228
+ entity.set_state(climate.States.COOL, update=False)
229
+ assert entity.custom_called is True
230
+ assert entity.state == climate.States.COOL
@@ -0,0 +1,228 @@
1
+ """Tests for CoverEntity with built-in state management."""
2
+
3
+ import pytest
4
+ from unittest.mock import MagicMock
5
+ from ucapi import cover
6
+ from ucapi_framework import CoverEntity
7
+
8
+
9
+ class TestCoverEntity:
10
+ """Test CoverEntity state management."""
11
+
12
+ @pytest.fixture
13
+ def mock_api(self):
14
+ """Create a mock API for testing."""
15
+ api = MagicMock()
16
+ api.configured_entities.get.return_value = MagicMock(
17
+ attributes={cover.Attributes.STATE: cover.States.CLOSED}
18
+ )
19
+ return api
20
+
21
+ @pytest.fixture
22
+ def entity(self, mock_api):
23
+ """Create a CoverEntity for testing."""
24
+ entity = CoverEntity(
25
+ "cover.test",
26
+ "Test Cover",
27
+ features=[cover.Features.OPEN, cover.Features.CLOSE],
28
+ attributes={cover.Attributes.STATE: cover.States.CLOSED},
29
+ )
30
+ entity._api = mock_api # noqa: SLF001
31
+ return entity
32
+
33
+ def test_initial_state(self, entity):
34
+ """Test initial state from constructor attributes."""
35
+ # State was set to CLOSED in constructor
36
+ assert entity.state == cover.States.CLOSED
37
+ # These were not set, so should be None
38
+ assert entity.position is None
39
+ assert entity.tilt_position is None
40
+
41
+ def test_set_state_with_update(self, entity, mock_api):
42
+ """Test set_state() calls entity.update() by default."""
43
+ entity.set_state(cover.States.OPEN, update=True)
44
+
45
+ # Verify internal state was updated
46
+ assert entity.state == cover.States.OPEN
47
+
48
+ # Verify update was called
49
+ assert mock_api.configured_entities.update_attributes.called
50
+ call_args = mock_api.configured_entities.update_attributes.call_args
51
+ entity_id, attributes = call_args[0]
52
+ assert entity_id == "cover.test"
53
+ assert cover.Attributes.STATE in attributes
54
+ assert attributes[cover.Attributes.STATE] == cover.States.OPEN
55
+
56
+ def test_set_state_without_update(self, entity, mock_api):
57
+ """Test set_state(update=False) does not call entity.update()."""
58
+ entity.set_state(cover.States.OPEN, update=False)
59
+
60
+ # Verify internal state was updated
61
+ assert entity.state == cover.States.OPEN
62
+
63
+ # Verify update was NOT called
64
+ assert not mock_api.configured_entities.update_attributes.called
65
+
66
+ def test_set_state_transitions(self, entity, mock_api):
67
+ """Test all valid state transitions."""
68
+ for state in [
69
+ cover.States.OPENING,
70
+ cover.States.OPEN,
71
+ cover.States.CLOSING,
72
+ cover.States.CLOSED,
73
+ cover.States.UNKNOWN,
74
+ cover.States.UNAVAILABLE,
75
+ ]:
76
+ entity.set_state(state, update=False)
77
+ assert entity.state == state
78
+
79
+ def test_set_position(self, entity, mock_api):
80
+ """Test set_position() updates state and calls update."""
81
+ entity.set_position(75, update=True)
82
+
83
+ assert entity.position == 75
84
+ assert mock_api.configured_entities.update_attributes.called
85
+
86
+ def test_set_position_without_update(self, entity, mock_api):
87
+ """Test set_position(update=False) does not call entity.update()."""
88
+ entity.set_position(75, update=False)
89
+
90
+ assert entity.position == 75
91
+ assert not mock_api.configured_entities.update_attributes.called
92
+
93
+ def test_set_tilt_position(self, entity, mock_api):
94
+ """Test set_tilt_position() updates state and calls update."""
95
+ entity.set_tilt_position(45, update=True)
96
+
97
+ assert entity.tilt_position == 45
98
+ assert mock_api.configured_entities.update_attributes.called
99
+
100
+ def test_set_tilt_position_without_update(self, entity, mock_api):
101
+ """Test set_tilt_position(update=False) does not call entity.update()."""
102
+ entity.set_tilt_position(45, update=False)
103
+
104
+ assert entity.tilt_position == 45
105
+ assert not mock_api.configured_entities.update_attributes.called
106
+
107
+ def test_set_attributes_bulk_update(self, entity, mock_api):
108
+ """Test set_attributes() updates multiple attributes with single update call."""
109
+ entity.set_attributes(
110
+ state=cover.States.OPEN,
111
+ position=100,
112
+ tilt_position=50,
113
+ update=True,
114
+ )
115
+
116
+ # Verify all internal state was updated
117
+ assert entity.state == cover.States.OPEN
118
+ assert entity.position == 100
119
+ assert entity.tilt_position == 50
120
+
121
+ # Verify update was called only once
122
+ assert mock_api.configured_entities.update_attributes.call_count == 1
123
+
124
+ # Verify all attributes were included in the update
125
+ call_args = mock_api.configured_entities.update_attributes.call_args
126
+ entity_id, attributes = call_args[0]
127
+ assert entity_id == "cover.test"
128
+ assert len(attributes) == 3
129
+ assert attributes[cover.Attributes.STATE] == cover.States.OPEN
130
+ assert attributes[cover.Attributes.POSITION] == 100
131
+ assert attributes[cover.Attributes.TILT_POSITION] == 50
132
+
133
+ def test_set_attributes_without_update(self, entity, mock_api):
134
+ """Test set_attributes(update=False) does not call entity.update()."""
135
+ entity.set_attributes(state=cover.States.OPEN, position=100, update=False)
136
+
137
+ # Verify internal state was updated
138
+ assert entity.state == cover.States.OPEN
139
+ assert entity.position == 100
140
+
141
+ # Verify update was NOT called
142
+ assert not mock_api.configured_entities.update_attributes.called
143
+
144
+ def test_set_attributes_ignores_none_values(self, entity, mock_api):
145
+ """Test set_attributes() ignores None values."""
146
+ entity.set_attributes(state=cover.States.OPEN, position=None, update=True)
147
+
148
+ # Only state should be in internal storage
149
+ assert entity.state == cover.States.OPEN
150
+ assert entity.position is None
151
+
152
+ # Verify only state was included in update
153
+ call_args = mock_api.configured_entities.update_attributes.call_args
154
+ entity_id, attributes = call_args[0]
155
+ assert len(attributes) == 1
156
+ assert cover.Attributes.STATE in attributes
157
+
158
+ def test_property_getters_are_read_only(self, entity):
159
+ """Test that property getters cannot be set directly."""
160
+ with pytest.raises(AttributeError):
161
+ entity.state = cover.States.OPEN # type: ignore[misc]
162
+
163
+ def test_all_cover_attributes(self, entity, mock_api):
164
+ """Test setting all cover attributes."""
165
+ entity.set_attributes(
166
+ state=cover.States.OPEN,
167
+ position=100,
168
+ tilt_position=0,
169
+ update=True,
170
+ )
171
+
172
+ assert entity.state == cover.States.OPEN
173
+ assert entity.position == 100
174
+ assert entity.tilt_position == 0
175
+
176
+ # Verify single update call
177
+ assert mock_api.configured_entities.update_attributes.call_count == 1
178
+
179
+
180
+ class TestCoverEntityInheritance:
181
+ """Test that CoverEntity can be subclassed and overridden."""
182
+
183
+ def test_custom_set_state(self):
184
+ """Test that set_state can be overridden."""
185
+
186
+ class CustomCover(CoverEntity):
187
+ def __init__(self):
188
+ super().__init__(
189
+ "cover.custom",
190
+ "Custom Cover",
191
+ features=[],
192
+ attributes={},
193
+ )
194
+ self.custom_set_state_called = False
195
+
196
+ def set_state(self, value, *, update=True):
197
+ """Override set_state to add custom logic."""
198
+ self.custom_set_state_called = True
199
+ super().set_state(value, update=update)
200
+
201
+ entity = CustomCover()
202
+ entity._api = MagicMock() # noqa: SLF001
203
+ entity._api.configured_entities.get.return_value = entity # noqa: SLF001
204
+
205
+ entity.set_state(cover.States.OPEN, update=False)
206
+ assert entity.custom_set_state_called is True
207
+ assert entity.state == cover.States.OPEN
208
+
209
+ def test_custom_property_getter(self):
210
+ """Test that property getters can be overridden."""
211
+
212
+ class CustomCover(CoverEntity):
213
+ def __init__(self):
214
+ super().__init__(
215
+ "cover.custom",
216
+ "Custom Cover",
217
+ features=[],
218
+ attributes={},
219
+ )
220
+
221
+ @property
222
+ def state(self):
223
+ """Override state getter to always return OPEN."""
224
+ return cover.States.OPEN
225
+
226
+ entity = CustomCover()
227
+ # Even if internal state is None, getter returns OPEN
228
+ assert entity.state == cover.States.OPEN