pyg90alarm 2.5.0__tar.gz → 2.5.2__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 (96) hide show
  1. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/PKG-INFO +1 -1
  2. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/__init__.py +3 -0
  3. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/entities/base_list.py +6 -1
  4. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/exceptions.py +1 -1
  5. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/host_config.py +1 -0
  6. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/net_config.py +1 -1
  7. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
  8. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm.egg-info/SOURCES.txt +1 -0
  9. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_host_config.py +32 -0
  10. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_net_config.py +40 -1
  11. pyg90alarm-2.5.2/tests/unit/entities/test_base_list.py +306 -0
  12. pyg90alarm-2.5.2/tests/unit/test_exceptions.py +16 -0
  13. pyg90alarm-2.5.0/tests/unit/entities/test_base_list.py +0 -192
  14. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/.github/CODEOWNERS +0 -0
  15. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/.github/dependabot.yml +0 -0
  16. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/.github/workflows/main.yml +0 -0
  17. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/.gitignore +0 -0
  18. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/.pylintrc +0 -0
  19. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/.readthedocs.yaml +0 -0
  20. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/LICENSE +0 -0
  21. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/MANIFEST.in +0 -0
  22. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/README.rst +0 -0
  23. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/docs/.DS_Store +0 -0
  24. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/docs/.gitignore +0 -0
  25. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/docs/api-docs.rst +0 -0
  26. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/docs/cloud-protocol.rst +0 -0
  27. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/docs/conf.py +0 -0
  28. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/docs/index.rst +0 -0
  29. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/docs/local-protocol.rst +0 -0
  30. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/docs/requirements.txt +0 -0
  31. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/pyproject.toml +0 -0
  32. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/setup.cfg +0 -0
  33. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/setup.py +0 -0
  34. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/sonar-project.properties +0 -0
  35. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/alarm.py +0 -0
  36. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/callback.py +0 -0
  37. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/cloud/__init__.py +0 -0
  38. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/cloud/const.py +0 -0
  39. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/cloud/messages.py +0 -0
  40. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/cloud/notifications.py +0 -0
  41. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/cloud/protocol.py +0 -0
  42. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/const.py +0 -0
  43. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/dataclass/__init__.py +0 -0
  44. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/dataclass/load_save.py +0 -0
  45. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/dataclass/validation.py +0 -0
  46. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/definitions/__init__.py +0 -0
  47. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/definitions/base.py +0 -0
  48. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/definitions/devices.py +0 -0
  49. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/definitions/sensors.py +0 -0
  50. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/entities/__init__.py +0 -0
  51. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/entities/base_entity.py +0 -0
  52. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/entities/device.py +0 -0
  53. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/entities/device_list.py +0 -0
  54. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/entities/sensor.py +0 -0
  55. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/entities/sensor_list.py +0 -0
  56. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/event_mapping.py +0 -0
  57. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/__init__.py +0 -0
  58. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/alarm_phones.py +0 -0
  59. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/alert_config.py +0 -0
  60. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/base_cmd.py +0 -0
  61. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/config.py +0 -0
  62. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/discovery.py +0 -0
  63. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/history.py +0 -0
  64. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/host_info.py +0 -0
  65. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/host_status.py +0 -0
  66. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/notifications.py +0 -0
  67. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/paginated_cmd.py +0 -0
  68. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/paginated_result.py +0 -0
  69. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/targeted_discovery.py +0 -0
  70. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/local/user_data_crc.py +0 -0
  71. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/notifications/__init__.py +0 -0
  72. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/notifications/base.py +0 -0
  73. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/notifications/protocol.py +0 -0
  74. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm/py.typed +0 -0
  75. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  76. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm.egg-info/requires.txt +0 -0
  77. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  78. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/__init__.py +0 -0
  79. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/conftest.py +0 -0
  80. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/device_mock.py +0 -0
  81. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_alarm.py +0 -0
  82. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_alarm_phones.py +0 -0
  83. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_base_commands.py +0 -0
  84. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_cloud_notifications.py +0 -0
  85. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_config.py +0 -0
  86. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_devices.py +0 -0
  87. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_discovery.py +0 -0
  88. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_history.py +0 -0
  89. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_local_notifications.py +0 -0
  90. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_paginated_commands.py +0 -0
  91. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/test_sensor.py +0 -0
  92. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/unit/dataclass/test_dataclass_load_save.py +0 -0
  93. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/unit/dataclass/test_dataclass_load_save_descriptor.py +0 -0
  94. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/unit/dataclass/test_dataclass_load_save_serialize.py +0 -0
  95. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tests/unit/dataclass/test_validation.py +0 -0
  96. {pyg90alarm-2.5.0 → pyg90alarm-2.5.2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyg90alarm
3
- Version: 2.5.0
3
+ Version: 2.5.2
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -53,6 +53,7 @@ from .local.alarm_phones import G90AlarmPhones
53
53
  from .local.net_config import G90NetConfig, G90APNAuth
54
54
  from .local.history import G90History
55
55
 
56
+ from .dataclass.load_save import DataclassLoadSave
56
57
  from .dataclass.validation import (
57
58
  get_field_validation_constraints,
58
59
  )
@@ -101,4 +102,6 @@ __all__ = [
101
102
  'G90History',
102
103
  # Dataclass validation
103
104
  'get_field_validation_constraints',
105
+ # Dataclass load/save
106
+ 'DataclassLoadSave',
104
107
  ]
@@ -68,6 +68,7 @@ class G90BaseList(Generic[T], ABC):
68
68
 
69
69
  :return: Async generator of entities
70
70
  """
71
+ # Placeholder to satisfy the abstractmethod
71
72
  yield cast(T, None) # pragma: no cover
72
73
 
73
74
  @property
@@ -118,7 +119,11 @@ class G90BaseList(Generic[T], ABC):
118
119
  )
119
120
 
120
121
  existing_entity.update(entity)
121
- non_existing_entities.remove(existing_entity)
122
+ # The entity might have already been removed if there
123
+ # are duplicate entities from the `_fetch` method,
124
+ # protect against that
125
+ if existing_entity in non_existing_entities:
126
+ non_existing_entities.remove(existing_entity)
122
127
 
123
128
  # Invoke the list change callback for the existing
124
129
  # entity to notify about the update
@@ -32,7 +32,7 @@ class G90Error(Exception):
32
32
  """
33
33
 
34
34
 
35
- class G90TimeoutError(asyncio.TimeoutError): # pylint:disable=R0903
35
+ class G90TimeoutError(asyncio.TimeoutError, G90Error):
36
36
  """
37
37
  Raised when particular package class to report an operation (typically
38
38
  device command) has timed out.
@@ -35,6 +35,7 @@ class G90SpeechLanguage(IntEnum):
35
35
  """
36
36
  Supported speech languages.
37
37
  """
38
+ NONE = 0 # Some panels send the value, exact meaning is unknown
38
39
  ENGLISH_FEMALE = 1
39
40
  ENGLISH_MALE = 2
40
41
  CHINESE_FEMALE = 3
@@ -73,7 +73,7 @@ class G90NetConfig(DataclassLoadSave):
73
73
  # Access Point Name (APN) for GPRS connection, as provided by the cellular
74
74
  # operator
75
75
  apn_name: str = validated_string_field(
76
- min_length=1, max_length=100, trust_initial_value=True
76
+ min_length=0, max_length=100, trust_initial_value=True
77
77
  )
78
78
  # User name for APN authentication, as provided by the cellular operator
79
79
  apn_user: str = validated_string_field(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyg90alarm
3
- Version: 2.5.0
3
+ Version: 2.5.2
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -86,6 +86,7 @@ tests/test_local_notifications.py
86
86
  tests/test_net_config.py
87
87
  tests/test_paginated_commands.py
88
88
  tests/test_sensor.py
89
+ tests/unit/test_exceptions.py
89
90
  tests/unit/dataclass/test_dataclass_load_save.py
90
91
  tests/unit/dataclass/test_dataclass_load_save_descriptor.py
91
92
  tests/unit/dataclass/test_dataclass_load_save_serialize.py
@@ -164,3 +164,35 @@ async def test_host_config_constraints(
164
164
  # Test setting valid value
165
165
  setattr(cfg, field_name, valid_value)
166
166
  assert getattr(cfg, field_name) == valid_value
167
+
168
+
169
+ @pytest.mark.g90device(sent_data=[
170
+ b'ISTART[106,'
171
+ b'[900,0,0,1,2,2,60,0,0,60,2]'
172
+ b']IEND\0',
173
+ b'ISTARTIEND\0'
174
+ ])
175
+ async def test_host_config_speech_language_zero(
176
+ mock_device: DeviceMock
177
+ ) -> None:
178
+ """
179
+ Tests for handling host configuration with speech language set to zero.
180
+ """
181
+ g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
182
+
183
+ # Retrieve configuration
184
+ cfg = await g90.host_config()
185
+ assert isinstance(cfg, G90HostConfig)
186
+
187
+ # Verify retrieved values
188
+ assert cfg.speech_language == G90SpeechLanguage.NONE
189
+
190
+ # Verify zero value is allowed to be sent back to the panel
191
+ await cfg.save()
192
+
193
+ assert await mock_device.recv_data == [
194
+ b'ISTART[106,106,""]IEND\0',
195
+ b'ISTART[107,107,[107,'
196
+ b'[900,0,0,1,2,2,60,0,0,60,2]'
197
+ b']]IEND\0'
198
+ ]
@@ -80,7 +80,7 @@ async def test_net_config(
80
80
  @pytest.mark.parametrize(
81
81
  'field_name,invalid_value_low,invalid_value_high,valid_value', [
82
82
  pytest.param(
83
- 'apn_name', '', 'a' * 101, 'valid.apn.net',
83
+ 'apn_name', None, 'a' * 101, 'valid.apn.net',
84
84
  id='apn_name'
85
85
  ),
86
86
  pytest.param(
@@ -151,3 +151,42 @@ async def test_net_config_constraints(
151
151
  # Test setting valid value
152
152
  setattr(cfg, field_name, valid_value)
153
153
  assert getattr(cfg, field_name) == valid_value
154
+
155
+
156
+ @pytest.mark.g90device(sent_data=[
157
+ b'ISTART[212,'
158
+ b'[0,"123456789",1,1,"","user","pwd",3,"54321"]'
159
+ b']IEND\0',
160
+ b'ISTARTIEND\0'
161
+ ])
162
+ async def test_net_config_apn_name_empty(
163
+ mock_device: DeviceMock, caplog: pytest.LogCaptureFixture
164
+ ) -> None:
165
+ """
166
+ Tests for handling network configuration with empty APN name.
167
+ """
168
+ g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
169
+
170
+ # Retrieve configuration
171
+ cfg = await g90.net_config()
172
+ assert isinstance(cfg, G90NetConfig)
173
+
174
+ # Verify retrieved values
175
+ assert cfg.apn_name == ''
176
+
177
+ # Ensure that no validation error was logged for empty APN name, the value
178
+ # of the field as received from the panel is trusted, so no exception will
179
+ # be raised only error logged
180
+ assert (
181
+ 'apn_name: Validation failed during initialization for trusted value'
182
+ ) not in ''.join(caplog.messages)
183
+
184
+ # Verify empty APN name is allowed to be sent back to the panel
185
+ await cfg.save()
186
+
187
+ assert await mock_device.recv_data == [
188
+ b'ISTART[212,212,""]IEND\0',
189
+ b'ISTART[213,213,[213,'
190
+ b'[0,"123456789",1,1,"","user","pwd",3]'
191
+ b']]IEND\0'
192
+ ]
@@ -0,0 +1,306 @@
1
+ """
2
+ Tests for G90BaseList class
3
+ """
4
+ from __future__ import annotations
5
+ from typing import AsyncGenerator
6
+ from unittest.mock import MagicMock
7
+
8
+ from pyg90alarm.entities.base_list import G90BaseList
9
+
10
+
11
+ async def test_find_free_idx_empty_list() -> None:
12
+ """
13
+ Tests find_free_idx with empty entities list.
14
+ """
15
+ class TestClass(G90BaseList[MagicMock]):
16
+ """
17
+ Mock subclass for testing G90BaseList.
18
+ """
19
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
20
+ """
21
+ Mock no entities.
22
+ """
23
+ x: MagicMock
24
+ for x in []:
25
+ yield x
26
+
27
+ base_list = TestClass(parent=MagicMock())
28
+ result = await base_list.find_free_idx()
29
+
30
+ assert result == 0
31
+
32
+
33
+ async def test_find_free_idx_one_entity_at_zero() -> None:
34
+ """
35
+ Tests find_free_idx with one entity at index 0.
36
+ """
37
+ class TestClass(G90BaseList[MagicMock]):
38
+ """
39
+ Mock subclass for testing G90BaseList.
40
+ """
41
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
42
+ """
43
+ Mock one entity at index 0.
44
+ """
45
+ for x in [MagicMock(index=0)]:
46
+ yield x
47
+
48
+ base_list = TestClass(parent=MagicMock())
49
+ result = await base_list.find_free_idx()
50
+
51
+ assert result == 1
52
+
53
+
54
+ async def test_find_free_idx_returns_lowest_available_index() -> None:
55
+ """
56
+ Tests find_free_idx with two entities at indexes 10 and 11.
57
+ """
58
+ class TestClass(G90BaseList[MagicMock]):
59
+ """
60
+ Mock subclass for testing G90BaseList.
61
+ """
62
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
63
+ """
64
+ Mock entities at indexes 10 and 11.
65
+ """
66
+ for x in [MagicMock(index=10), MagicMock(index=11)]:
67
+ yield x
68
+
69
+ base_list = TestClass(parent=MagicMock())
70
+ result = await base_list.find_free_idx()
71
+
72
+ assert result == 0
73
+
74
+
75
+ async def test_find_free_idx_returns_lowest_available_index_over_gap() -> None:
76
+ """
77
+ Tests find_free_idx with two entities at indexes 10 and 11.
78
+ """
79
+ class TestClass(G90BaseList[MagicMock]):
80
+ """
81
+ Mock subclass for testing G90BaseList.
82
+ """
83
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
84
+ """
85
+ Mock entities at indexes 0, 10 and 11.
86
+ """
87
+ for x in [
88
+ MagicMock(index=0), MagicMock(index=10), MagicMock(index=11)
89
+ ]:
90
+ yield x
91
+
92
+ base_list = TestClass(parent=MagicMock())
93
+ result = await base_list.find_free_idx()
94
+
95
+ assert result == 1
96
+
97
+
98
+ async def test_find_entity_by_idx_and_name() -> None:
99
+ """
100
+ Tests find with matching index, subindex, and name.
101
+ """
102
+ entity = MagicMock()
103
+ entity.index = 0
104
+ entity.subindex = 0
105
+ entity.name = "Test Entity"
106
+ entity.is_unavailable = False
107
+
108
+ class TestClass(G90BaseList[MagicMock]):
109
+ """
110
+ Mock subclass for testing G90BaseList.
111
+ """
112
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
113
+ """
114
+ Mock one entity.
115
+ """
116
+ yield entity
117
+
118
+ base_list = TestClass(parent=MagicMock())
119
+ result = await base_list.find(
120
+ idx=0, name="Test Entity", exclude_unavailable=False
121
+ )
122
+
123
+ assert result == entity
124
+
125
+
126
+ async def test_find_entity_name_mismatch() -> None:
127
+ """
128
+ Tests find with matching index but mismatched name.
129
+ """
130
+ class TestClass(G90BaseList[MagicMock]):
131
+ """
132
+ Mock subclass for testing G90BaseList.
133
+ """
134
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
135
+ """
136
+ Mock one entity.
137
+ """
138
+ entity = MagicMock()
139
+ entity.index = 0
140
+ entity.subindex = 0
141
+ entity.name = "Test Entity"
142
+ entity.is_unavailable = False
143
+ yield entity
144
+
145
+ base_list = TestClass(parent=MagicMock())
146
+ result = await base_list.find(
147
+ idx=0, name="Wrong Name", exclude_unavailable=False
148
+ )
149
+
150
+ assert result is None
151
+
152
+
153
+ async def test_find_entity_not_found() -> None:
154
+ """
155
+ Tests find with non-existing index.
156
+ """
157
+ class TestClass(G90BaseList[MagicMock]):
158
+ """
159
+ Mock subclass for testing G90BaseList.
160
+ """
161
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
162
+ """
163
+ Mock no entities.
164
+ """
165
+ x: MagicMock
166
+ for x in []:
167
+ yield x
168
+
169
+ base_list = TestClass(parent=MagicMock())
170
+ result = await base_list.find(
171
+ idx=0, name="Test Entity", exclude_unavailable=False
172
+ )
173
+
174
+ assert result is None
175
+
176
+
177
+ async def test_find_entity_unavailable_excluded() -> None:
178
+ """
179
+ Tests find with unavailable entity and exclude_unavailable=True.
180
+ """
181
+ class TestClass(G90BaseList[MagicMock]):
182
+ """
183
+ Mock subclass for testing G90BaseList.
184
+ """
185
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
186
+ """
187
+ Mock one entity.
188
+ """
189
+ entity = MagicMock()
190
+ entity.index = 0
191
+ entity.subindex = 0
192
+ entity.name = "Test Entity"
193
+ entity.is_unavailable = True
194
+ yield entity
195
+
196
+ base_list = TestClass(parent=MagicMock())
197
+ result = await base_list.find(
198
+ idx=0, name="Test Entity", exclude_unavailable=True
199
+ )
200
+
201
+ assert result is None
202
+
203
+
204
+ async def test_find_entity_unavailable_not_excluded() -> None:
205
+ """
206
+ Tests find with unavailable entity and exclude_unavailable=False.
207
+ """
208
+ entity = MagicMock()
209
+ entity.index = 0
210
+ entity.subindex = 0
211
+ entity.name = "Test Entity"
212
+ entity.is_unavailable = True
213
+
214
+ class TestClass(G90BaseList[MagicMock]):
215
+ """
216
+ Mock subclass for testing G90BaseList.
217
+ """
218
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
219
+ """
220
+ Mock one entity.
221
+ """
222
+ yield entity
223
+
224
+ base_list = TestClass(parent=MagicMock())
225
+ result = await base_list.find(
226
+ idx=0, name="Test Entity", exclude_unavailable=False
227
+ )
228
+
229
+ assert result == entity
230
+
231
+
232
+ async def test_find_entity_with_subindex() -> None:
233
+ """
234
+ Tests find with specific subindex.
235
+ """
236
+ entity = MagicMock()
237
+ entity.index = 0
238
+ entity.subindex = 1
239
+ entity.name = "Test Entity"
240
+ entity.is_unavailable = False
241
+
242
+ class TestClass(G90BaseList[MagicMock]):
243
+ """
244
+ Mock subclass for testing G90BaseList.
245
+ """
246
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
247
+ """
248
+ Mock one entity.
249
+ """
250
+ yield entity
251
+
252
+ base_list = TestClass(parent=MagicMock())
253
+ result = await base_list.find(
254
+ idx=0, name="Test Entity", exclude_unavailable=False, subindex=1
255
+ )
256
+
257
+ assert result == entity
258
+
259
+
260
+ async def test_duplicate_entities() -> None:
261
+ """
262
+ Tests that duplicate entities are handled correctly during update.
263
+ """
264
+ existing_entity = MagicMock()
265
+ existing_entity.index = 1
266
+ existing_entity.subindex = 0
267
+ existing_entity.name = "Existing entity"
268
+ existing_entity.is_unavailable = False
269
+
270
+ new_entity = MagicMock()
271
+ new_entity.index = 1
272
+ new_entity.subindex = 0
273
+ new_entity.name = "Test entity"
274
+ new_entity.is_unavailable = False
275
+
276
+ mock_entities = iter([
277
+ # Initial fetch returns one existing entity
278
+ [existing_entity],
279
+ # Second fetch returns duplicate new entities
280
+ [new_entity, new_entity],
281
+ ])
282
+
283
+ class TestClass(G90BaseList[MagicMock]):
284
+ """
285
+ Mock subclass for testing G90BaseList.
286
+ """
287
+ async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
288
+ """
289
+ Mock test list entities.
290
+ """
291
+ # Yield entities under test
292
+ for x in next(mock_entities):
293
+ yield x
294
+
295
+ base_list = TestClass(parent=MagicMock())
296
+ # Initial update to simulate the existing entity
297
+ await base_list.update()
298
+ # Second update to test handling of duplicates, should not raise
299
+ result = await base_list.update()
300
+
301
+ assert len(result) == 2
302
+ # Initial entities not present in second update result should be marked
303
+ # unavailable
304
+ assert result[0] == existing_entity
305
+ assert result[0].is_unavailable is True
306
+ assert result[1] == new_entity
@@ -0,0 +1,16 @@
1
+ """
2
+ Tests for package-specific exceptions.
3
+ """
4
+ from __future__ import annotations
5
+ import pytest
6
+
7
+ from pyg90alarm.exceptions import G90TimeoutError, G90Error
8
+
9
+
10
+ def test_g90_timeout_error_inheritance() -> None:
11
+ """
12
+ Test that catching G90Error also catches G90TimeoutError.
13
+ """
14
+
15
+ with pytest.raises(G90Error):
16
+ raise G90TimeoutError("Timeout occurred")
@@ -1,192 +0,0 @@
1
- """
2
- Tests for G90BaseList class
3
- """
4
- from __future__ import annotations
5
- from typing import AsyncGenerator
6
- from unittest.mock import MagicMock
7
-
8
- from pyg90alarm.entities.base_list import G90BaseList
9
-
10
-
11
- class TestBaseList(G90BaseList[MagicMock]):
12
- """
13
- Mock subclass for testing G90BaseList.
14
- """
15
-
16
- # Prevent pytest from collecting this class as a test case
17
- __test__ = False
18
-
19
- async def _fetch(self) -> AsyncGenerator[MagicMock, None]:
20
- """Mock _fetch method."""
21
- if False: # pragma: no cover
22
- yield MagicMock()
23
-
24
-
25
- async def test_find_free_idx_empty_list() -> None:
26
- """
27
- Tests find_free_idx with empty entities list.
28
- """
29
-
30
- parent = MagicMock()
31
- base_list = TestBaseList(parent)
32
- base_list._entities = []
33
-
34
- result = await base_list.find_free_idx()
35
-
36
- assert result == 0
37
-
38
-
39
- async def test_find_free_idx_one_entity_at_zero() -> None:
40
- """
41
- Tests find_free_idx with one entity at index 0.
42
- """
43
-
44
- base_list = TestBaseList(parent=MagicMock())
45
- base_list._entities = [MagicMock(index=0)]
46
-
47
- result = await base_list.find_free_idx()
48
-
49
- assert result == 1
50
-
51
-
52
- async def test_find_free_idx_returns_lowest_available_index() -> None:
53
- """
54
- Tests find_free_idx with two entities at indexes 10 and 11.
55
- """
56
-
57
- base_list = TestBaseList(parent=MagicMock())
58
- base_list._entities = [MagicMock(index=10), MagicMock(index=11)]
59
-
60
- result = await base_list.find_free_idx()
61
-
62
- assert result == 0
63
-
64
-
65
- async def test_find_free_idx_returns_lowest_available_index_over_gap() -> None:
66
- """
67
- Tests find_free_idx with two entities at indexes 10 and 11.
68
- """
69
-
70
- base_list = TestBaseList(parent=MagicMock())
71
- base_list._entities = [
72
- MagicMock(index=0), MagicMock(index=10), MagicMock(index=11)
73
- ]
74
-
75
- result = await base_list.find_free_idx()
76
-
77
- assert result == 1
78
-
79
-
80
- async def test_find_entity_by_idx_and_name() -> None:
81
- """
82
- Tests find with matching index, subindex, and name.
83
- """
84
-
85
- base_list = TestBaseList(parent=MagicMock())
86
- entity = MagicMock()
87
- entity.index = 0
88
- entity.subindex = 0
89
- entity.name = "Test Entity"
90
- entity.is_unavailable = False
91
- base_list._entities = [entity]
92
-
93
- result = await base_list.find(
94
- idx=0, name="Test Entity", exclude_unavailable=False
95
- )
96
-
97
- assert result == entity
98
-
99
-
100
- async def test_find_entity_name_mismatch() -> None:
101
- """
102
- Tests find with matching index but mismatched name.
103
- """
104
-
105
- base_list = TestBaseList(parent=MagicMock())
106
- entity = MagicMock()
107
- entity.index = 0
108
- entity.subindex = 0
109
- entity.name = "Test Entity"
110
- entity.is_unavailable = False
111
- base_list._entities = [entity]
112
-
113
- result = await base_list.find(
114
- idx=0, name="Wrong Name", exclude_unavailable=False
115
- )
116
-
117
- assert result is None
118
-
119
-
120
- async def test_find_entity_not_found() -> None:
121
- """
122
- Tests find with non-existing index.
123
- """
124
-
125
- base_list = TestBaseList(parent=MagicMock())
126
- base_list._entities = []
127
-
128
- result = await base_list.find(
129
- idx=0, name="Test Entity", exclude_unavailable=False
130
- )
131
-
132
- assert result is None
133
-
134
-
135
- async def test_find_entity_unavailable_excluded() -> None:
136
- """
137
- Tests find with unavailable entity and exclude_unavailable=True.
138
- """
139
-
140
- base_list = TestBaseList(parent=MagicMock())
141
- entity = MagicMock()
142
- entity.index = 0
143
- entity.subindex = 0
144
- entity.name = "Test Entity"
145
- entity.is_unavailable = True
146
- base_list._entities = [entity]
147
-
148
- result = await base_list.find(
149
- idx=0, name="Test Entity", exclude_unavailable=True
150
- )
151
-
152
- assert result is None
153
-
154
-
155
- async def test_find_entity_unavailable_not_excluded() -> None:
156
- """
157
- Tests find with unavailable entity and exclude_unavailable=False.
158
- """
159
-
160
- base_list = TestBaseList(parent=MagicMock())
161
- entity = MagicMock()
162
- entity.index = 0
163
- entity.subindex = 0
164
- entity.name = "Test Entity"
165
- entity.is_unavailable = True
166
- base_list._entities = [entity]
167
-
168
- result = await base_list.find(
169
- idx=0, name="Test Entity", exclude_unavailable=False
170
- )
171
-
172
- assert result == entity
173
-
174
-
175
- async def test_find_entity_with_subindex() -> None:
176
- """
177
- Tests find with specific subindex.
178
- """
179
-
180
- base_list = TestBaseList(parent=MagicMock())
181
- entity = MagicMock()
182
- entity.index = 0
183
- entity.subindex = 1
184
- entity.name = "Test Entity"
185
- entity.is_unavailable = False
186
- base_list._entities = [entity]
187
-
188
- result = await base_list.find(
189
- idx=0, name="Test Entity", exclude_unavailable=False, subindex=1
190
- )
191
-
192
- assert result == entity
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes