pyg90alarm 2.2.0__tar.gz → 2.3.1__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 (79) hide show
  1. pyg90alarm-2.3.1/.github/dependabot.yml +26 -0
  2. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.github/workflows/main.yml +6 -6
  3. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/PKG-INFO +1 -1
  4. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/pyproject.toml +1 -0
  5. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/alarm.py +5 -3
  6. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/notifications.py +2 -1
  7. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/base_list.py +15 -10
  8. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/base_cmd.py +1 -1
  9. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/config.py +28 -29
  10. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/notifications.py +2 -1
  11. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/paginated_cmd.py +1 -1
  12. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/paginated_result.py +1 -1
  13. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
  14. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/SOURCES.txt +3 -1
  15. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/device_mock.py +2 -1
  16. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_alarm.py +13 -7
  17. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_config.py +9 -8
  18. pyg90alarm-2.3.1/tests/unit/entities/test_base_list.py +192 -0
  19. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.github/CODEOWNERS +0 -0
  20. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.gitignore +0 -0
  21. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.pylintrc +0 -0
  22. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.readthedocs.yaml +0 -0
  23. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/LICENSE +0 -0
  24. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/MANIFEST.in +0 -0
  25. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/README.rst +0 -0
  26. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/.DS_Store +0 -0
  27. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/.gitignore +0 -0
  28. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/api-docs.rst +0 -0
  29. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/cloud-protocol.rst +0 -0
  30. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/conf.py +0 -0
  31. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/index.rst +0 -0
  32. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/local-protocol.rst +0 -0
  33. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/requirements.txt +0 -0
  34. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/setup.cfg +0 -0
  35. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/setup.py +0 -0
  36. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/sonar-project.properties +0 -0
  37. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/__init__.py +0 -0
  38. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/callback.py +0 -0
  39. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/__init__.py +0 -0
  40. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/const.py +0 -0
  41. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/messages.py +0 -0
  42. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/protocol.py +0 -0
  43. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/const.py +0 -0
  44. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/definitions/__init__.py +0 -0
  45. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/definitions/base.py +0 -0
  46. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/definitions/devices.py +0 -0
  47. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/definitions/sensors.py +0 -0
  48. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/__init__.py +0 -0
  49. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/base_entity.py +0 -0
  50. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/device.py +0 -0
  51. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/device_list.py +0 -0
  52. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/sensor.py +0 -0
  53. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/sensor_list.py +0 -0
  54. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/exceptions.py +0 -0
  55. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/__init__.py +0 -0
  56. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/discovery.py +0 -0
  57. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/history.py +0 -0
  58. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/host_info.py +0 -0
  59. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/host_status.py +0 -0
  60. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/targeted_discovery.py +0 -0
  61. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/user_data_crc.py +0 -0
  62. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/notifications/__init__.py +0 -0
  63. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/notifications/base.py +0 -0
  64. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/notifications/protocol.py +0 -0
  65. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/py.typed +0 -0
  66. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  67. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
  68. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  69. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/__init__.py +0 -0
  70. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/conftest.py +0 -0
  71. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_base_commands.py +0 -0
  72. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_cloud_notifications.py +0 -0
  73. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_devices.py +0 -0
  74. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_discovery.py +0 -0
  75. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_history.py +0 -0
  76. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_local_notifications.py +0 -0
  77. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_paginated_commands.py +0 -0
  78. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_sensor.py +0 -0
  79. {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tox.ini +0 -0
@@ -0,0 +1,26 @@
1
+ ---
2
+ version: 2
3
+ updates:
4
+ - package-ecosystem: "pip"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "daily"
8
+ commit-message:
9
+ prefix: "deps"
10
+ include: "scope"
11
+ labels:
12
+ - "dependencies"
13
+ - "automated"
14
+ open-pull-requests-limit: 5
15
+
16
+ - package-ecosystem: "github-actions"
17
+ directory: "/"
18
+ schedule:
19
+ interval: "daily"
20
+ commit-message:
21
+ prefix: "deps"
22
+ include: "scope"
23
+ labels:
24
+ - "dependencies"
25
+ - "automated"
26
+ open-pull-requests-limit: 5
@@ -34,13 +34,13 @@ jobs:
34
34
  runs-on: ${{ matrix.os }}
35
35
  steps:
36
36
  - name: Checkout the code
37
- uses: actions/checkout@v3
37
+ uses: actions/checkout@v6
38
38
  with:
39
39
  # Disable shallow clone for Sonar scanner, as it needs access to the
40
40
  # history
41
41
  fetch-depth: 0
42
42
  - name: Set Python up
43
- uses: actions/setup-python@v4
43
+ uses: actions/setup-python@v6
44
44
  with:
45
45
  python-version: ${{ matrix.python }}
46
46
  - name: Install testing tools
@@ -56,7 +56,7 @@ jobs:
56
56
  package_version=`python3 setup.py --version`
57
57
  echo "VALUE=$package_version" >> $GITHUB_OUTPUT
58
58
  - name: SonarCloud scanning
59
- uses: sonarsource/sonarcloud-github-action@master
59
+ uses: SonarSource/sonarqube-scan-action@v6
60
60
  env:
61
61
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62
62
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -76,11 +76,11 @@ jobs:
76
76
  id-token: write # Required for trusted publishing
77
77
  steps:
78
78
  - name: Checkout the code
79
- uses: actions/checkout@v3
79
+ uses: actions/checkout@v6
80
80
  with:
81
81
  fetch-depth: 0 # `setuptools_scm` needs tags
82
82
  - name: Set Python up
83
- uses: actions/setup-python@v4
83
+ uses: actions/setup-python@v6
84
84
  with:
85
85
  python-version: 3.9
86
86
  - name: Install the PEP517 package builder
@@ -93,7 +93,7 @@ jobs:
93
93
  if: github.event_name != 'release'
94
94
  uses: pypa/gh-action-pypi-publish@release/v1
95
95
  with:
96
- repository_url: https://test.pypi.org/legacy/
96
+ repository-url: https://test.pypi.org/legacy/
97
97
  attestations: true
98
98
  - name: Publish the release to PyPi
99
99
  # Publish to production PyPi only happens when a release published out
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyg90alarm
3
- Version: 2.2.0
3
+ Version: 2.3.1
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -11,6 +11,7 @@ local_scheme = "no-local-version"
11
11
  log_cli = 1
12
12
  log_cli_level = "error"
13
13
  asyncio_mode = "auto"
14
+ asyncio_default_fixture_loop_scope = "function"
14
15
  pythonpath = "src/"
15
16
 
16
17
  [tool.coverage.run]
@@ -566,10 +566,11 @@ class G90Alarm(G90NotificationProtocol):
566
566
  sensor._set_occupancy(False)
567
567
  sensor.state_callback.invoke(sensor.occupancy)
568
568
 
569
+ alert_config_flags = await self.alert_config.flags
569
570
  # Determine if door close notifications are available for the given
570
571
  # sensor
571
- door_close_alert_enabled = await self.alert_config.get_flag(
572
- G90AlertConfigFlags.DOOR_CLOSE
572
+ door_close_alert_enabled = (
573
+ G90AlertConfigFlags.DOOR_CLOSE in alert_config_flags
573
574
  )
574
575
  # The condition intentionally doesn't account for cord sensors of
575
576
  # subtype door, since those won't send door open/close alerts, only
@@ -587,7 +588,7 @@ class G90Alarm(G90NotificationProtocol):
587
588
  ' closing event will be emulated upon'
588
589
  ' %s seconds',
589
590
  name, sensor.type,
590
- await self.alert_config.flags,
591
+ alert_config_flags,
591
592
  self._reset_occupancy_interval)
592
593
  G90Callback.invoke_delayed(
593
594
  self._reset_occupancy_interval,
@@ -1230,6 +1231,7 @@ class G90Alarm(G90NotificationProtocol):
1230
1231
  local_port=notifications_local_port
1231
1232
  )
1232
1233
 
1234
+ # pylint: disable=too-many-positional-arguments
1233
1235
  async def use_cloud_notifications(
1234
1236
  self, cloud_local_host: str = CLOUD_NOTIFICATIONS_HOST,
1235
1237
  cloud_local_port: int = CLOUD_NOTIFICATIONS_PORT,
@@ -69,7 +69,8 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
69
69
  :param keep_single_connection: Whether to keep only a single device
70
70
  connection
71
71
  """
72
- # pylint:disable=too-many-arguments
72
+ # pylint:disable=too-many-positional-arguments,too-many-arguments
73
+ # pylint:disable=too-many-statements
73
74
  def __init__(
74
75
  self,
75
76
  protocol_factory: Callable[[], G90NotificationProtocol],
@@ -68,7 +68,7 @@ class G90BaseList(Generic[T], ABC):
68
68
 
69
69
  :return: Async generator of entities
70
70
  """
71
- yield cast(T, None)
71
+ yield cast(T, None) # pragma: no cover
72
72
 
73
73
  @property
74
74
  async def entities(self) -> List[T]:
@@ -231,25 +231,30 @@ class G90BaseList(Generic[T], ABC):
231
231
  # Collect indexes in use by the existing entities
232
232
  occupied_indexes = set(x.index for x in entities)
233
233
  # Generate a set of possible indexes from 0 to the maximum index in
234
- # use
235
- possible_indexes = set(range(0, max(occupied_indexes)))
234
+ # use, or provide an empty set if there are no existing entities
235
+ if occupied_indexes:
236
+ possible_indexes = set(range(0, max(occupied_indexes)))
237
+ else:
238
+ # No occupied indexes, so possible_indexes is empty
239
+ possible_indexes = set()
236
240
 
237
241
  try:
238
242
  # Find the first free index by taking difference between
239
243
  # possible indexes and occupied ones, and then taking the minimum
240
244
  # value off the difference
241
245
  free_idx = min(
242
- set(possible_indexes).difference(occupied_indexes)
246
+ possible_indexes.difference(occupied_indexes)
243
247
  )
244
- _LOGGER.debug(
245
- 'Found free index: %s out of occupied indexes: %s',
246
- free_idx, occupied_indexes
247
- )
248
- return free_idx
249
248
  except ValueError:
250
249
  # If no gaps in existing indexes, then return the index next to
251
250
  # the last existing entity
252
- return len(entities)
251
+ free_idx = len(entities)
252
+
253
+ _LOGGER.debug(
254
+ 'Found free index=%s out of occupied indexes: %s',
255
+ free_idx, occupied_indexes
256
+ )
257
+ return free_idx
253
258
 
254
259
  @property
255
260
  def list_change_callback(self) -> Optional[ListChangeCallback[T]]:
@@ -59,11 +59,11 @@ class G90BaseCommand(DatagramProtocol):
59
59
  # Lock need to be shared across all of the class instances
60
60
  _sk_lock = asyncio.Lock()
61
61
 
62
+ # pylint: disable=too-many-positional-arguments,too-many-arguments
62
63
  def __init__(self, host: str, port: int, code: G90Commands,
63
64
  data: Optional[G90BaseCommandData] = None,
64
65
  local_port: Optional[int] = None,
65
66
  timeout: float = 3.0, retries: int = 3) -> None:
66
- # pylint: disable=too-many-arguments
67
67
  self._remote_host = host
68
68
  self._remote_port = port
69
69
  self._local_port = local_port
@@ -22,7 +22,7 @@
22
22
  Represents various configuration aspects of the alarm panel.
23
23
  """
24
24
  from __future__ import annotations
25
- from typing import TYPE_CHECKING, Optional
25
+ from typing import TYPE_CHECKING
26
26
  import logging
27
27
  from dataclasses import dataclass
28
28
  from enum import IntFlag
@@ -79,22 +79,9 @@ class G90AlertConfig:
79
79
  Represents alert configuration as received from the alarm panel.
80
80
  """
81
81
  def __init__(self, parent: G90Alarm) -> None:
82
- self._alert_config: Optional[G90AlertConfigData] = None
83
82
  self.parent = parent
84
83
 
85
84
  async def _get(self) -> G90AlertConfigData:
86
- """
87
- Retrieves the alert configuration flags from the device. Please note
88
- the configuration is cached upon first call, so you need to
89
- re-instantiate the class to reflect any updates there.
90
-
91
- :return: The alerts configured
92
- """
93
- if not self._alert_config:
94
- self._alert_config = await self._get_uncached()
95
- return self._alert_config
96
-
97
- async def _get_uncached(self) -> G90AlertConfigData:
98
85
  """
99
86
  Retrieves the alert configuration flags directly from the device.
100
87
 
@@ -110,22 +97,23 @@ class G90AlertConfig:
110
97
  return data
111
98
 
112
99
  async def set(self, flags: G90AlertConfigFlags) -> None:
100
+ """
101
+ .. deprecated:: 2.3.0
102
+
103
+ This method is deprecated and will always raise a RuntimeError.
104
+ Please use :meth:`set_flag` to set individual flags.
105
+ """
106
+ raise RuntimeError(
107
+ 'The set() method is deprecated. Please use set_flag() to set'
108
+ ' individual flags instead.'
109
+ )
110
+
111
+ async def _set(self, flags: G90AlertConfigFlags) -> None:
113
112
  """
114
113
  Sets the alert configuration flags on the device.
115
114
  """
116
- # Use uncached method retrieving the alert configuration, to ensure the
117
- # actual value retrieved from the device
118
115
  _LOGGER.debug('Setting alert configuration to %s', repr(flags))
119
- alert_config = await self._get_uncached()
120
- if alert_config != self._alert_config:
121
- _LOGGER.warning(
122
- 'Alert configuration changed externally,'
123
- ' overwriting (read "%s", will be set to "%s")',
124
- repr(alert_config), repr(flags)
125
- )
126
116
  await self.parent.command(G90Commands.SETNOTICEFLAG, [flags.value])
127
- # Update the alert configuration stored
128
- (await self._get()).flags = flags
129
117
 
130
118
  async def get_flag(self, flag: G90AlertConfigFlags) -> bool:
131
119
  """
@@ -135,20 +123,31 @@ class G90AlertConfig:
135
123
 
136
124
  async def set_flag(self, flag: G90AlertConfigFlags, value: bool) -> None:
137
125
  """
126
+ Sets the given flag to the desired value.
127
+
128
+ Uses read-modify-write approach.
129
+
138
130
  :param flag: The flag to set
139
131
  :param value: The value to set
140
132
  """
133
+ # Retrieve current flags
134
+ current_flags = await self.flags
141
135
  # Skip updating the flag if it has the desired value
142
- if await self.get_flag(flag) == value:
136
+ if (flag in current_flags) == value:
143
137
  _LOGGER.debug(
144
138
  'Flag %s already set to %s, skipping update',
145
139
  repr(flag), value
146
140
  )
147
141
  return
148
142
 
149
- # Invert corresponding user flag and set it
150
- flags = await self.flags ^ flag
151
- await self.set(flags)
143
+ # Set or reset corresponding user flag depending on desired value
144
+ if value:
145
+ current_flags |= flag
146
+ else:
147
+ current_flags &= ~flag
148
+
149
+ # Set the updated flags
150
+ await self._set(current_flags)
152
151
 
153
152
  @property
154
153
  async def flags(self) -> G90AlertConfigFlags:
@@ -55,7 +55,8 @@ class G90LocalNotifications(G90NotificationsBase, DatagramProtocol):
55
55
  :param local_host: The host on which the local host is listening for
56
56
  notifications.
57
57
  """
58
- def __init__( # pylint:disable=too-many-arguments
58
+ # pylint:disable=too-many-positional-arguments,too-many-arguments
59
+ def __init__(
59
60
  self, protocol_factory: Callable[[], G90NotificationProtocol],
60
61
  port: int, host: str, local_port: int, local_host: str,
61
62
  ):
@@ -48,11 +48,11 @@ class G90PaginatedCommand(G90BaseCommand):
48
48
  """
49
49
  Implements paginated command for alarm panel protocol.
50
50
  """
51
+ # pylint: disable=too-many-positional-arguments,too-many-arguments
51
52
  def __init__(
52
53
  self, host: str, port: int, code: G90Commands, start: int, end: int,
53
54
  **kwargs: Any
54
55
  ) -> None:
55
- # pylint: disable=too-many-arguments
56
56
  self._start = start
57
57
  self._end = end
58
58
  self._expected_nelems = end - start + 1
@@ -49,11 +49,11 @@ class G90PaginatedResult:
49
49
  Processes paginated response from G90 corresponding panel commands.
50
50
  """
51
51
  # pylint: disable=too-few-public-methods
52
+ # pylint: disable=too-many-positional-arguments,too-many-arguments
52
53
  def __init__(
53
54
  self, host: str, port: int, code: G90Commands, start: int = 1,
54
55
  end: Optional[int] = None, **kwargs: Any
55
56
  ):
56
- # pylint: disable=too-many-arguments
57
57
  self._host = host
58
58
  self._port = port
59
59
  self._code = code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyg90alarm
3
- Version: 2.2.0
3
+ Version: 2.3.1
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -10,6 +10,7 @@ setup.py
10
10
  sonar-project.properties
11
11
  tox.ini
12
12
  .github/CODEOWNERS
13
+ .github/dependabot.yml
13
14
  .github/workflows/main.yml
14
15
  docs/.DS_Store
15
16
  docs/.gitignore
@@ -73,4 +74,5 @@ tests/test_discovery.py
73
74
  tests/test_history.py
74
75
  tests/test_local_notifications.py
75
76
  tests/test_paginated_commands.py
76
- tests/test_sensor.py
77
+ tests/test_sensor.py
78
+ tests/unit/entities/test_base_list.py
@@ -406,7 +406,8 @@ class DeviceMock: # pylint:disable=too-many-instance-attributes
406
406
  :param cloud_upstream_port: The port the simulated cloud upstream
407
407
  endpoint listens on for client requests
408
408
  """
409
- def __init__( # pylint:disable=too-many-arguments
409
+ # pylint:disable=too-many-positional-arguments,too-many-arguments
410
+ def __init__(
410
411
  self, data: List[bytes], notification_data: List[bytes],
411
412
  device_port: int, notification_port: int,
412
413
  cloud_notification_data: Optional[List[bytes]] = None,
@@ -432,6 +432,7 @@ async def test_door_open_close_callback(mock_device: DeviceMock) -> None:
432
432
  # Alert configuration, used by sensor activity callback invoked when
433
433
  # handling alarm
434
434
  b'ISTART[117,[256]]IEND\0',
435
+ b'ISTART[117,[256]]IEND\0',
435
436
  ],
436
437
  notification_data=[
437
438
  b'[208,[3,100,1,1,"Hall","DUMMYGUID",1630876128,0,[""]]]\0',
@@ -553,6 +554,7 @@ async def test_sensor_tamper_callback(
553
554
  b'ISTART[102,'
554
555
  b'[[1,1,1],["Remote",11,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
555
556
  b'ISTART[117,[256]]IEND\0',
557
+ b'ISTART[117,[256]]IEND\0',
556
558
  ],
557
559
  notification_data=[
558
560
  # Host SOS
@@ -602,9 +604,18 @@ async def test_sos_callback(mock_device: DeviceMock) -> None:
602
604
  # Button press callback should be called with the remote button state, but
603
605
  # only for SOS initiated by the remote
604
606
  button_cb.assert_called_once_with(11, 'Remote', G90RemoteButtonStates.SOS)
605
-
606
607
  await g90.close_notifications()
607
608
 
609
+ # Test incurs other callbacks and corresponding tasks (two invocations of
610
+ # on_sensor_activity callback for reflecting state changes for the remote
611
+ # as a sensor), but those aren't in scope - explicitly await any
612
+ # pending tasks to avoid asyncio warnings
613
+ await asyncio.gather(
614
+ *(t for t in asyncio.all_tasks() if t is not asyncio.current_task()),
615
+ # Ignore exceptions from other tasks
616
+ return_exceptions=True
617
+ )
618
+
608
619
 
609
620
  @pytest.mark.g90device(
610
621
  sent_data=[
@@ -690,11 +701,7 @@ async def test_disarm(mock_device: DeviceMock) -> None:
690
701
 
691
702
  @pytest.mark.g90device(
692
703
  sent_data=[
693
- # First command to get alert configuration is from
694
- # `G90Alarm.get_alert_config()` property
695
- b"ISTART[117,[1]]IEND\0",
696
- # Second command for same is invoked by `G90Alarm.set_alert_config`
697
- # that checks if alert config has been modified externally
704
+ # Get alert configuration
698
705
  b"ISTART[117,[1]]IEND\0",
699
706
  b"ISTARTIEND\0",
700
707
  # Simulated list of sensors, which is used to reset door open when
@@ -738,7 +745,6 @@ async def test_sms_alert_when_armed(mock_device: DeviceMock) -> None:
738
745
  sent_data=[
739
746
  # See above for the clarification on command sequence
740
747
  b"ISTART[117,[513]]IEND\0",
741
- b"ISTART[117,[513]]IEND\0",
742
748
  b"ISTARTIEND\0",
743
749
  b'ISTART[102,'
744
750
  b'[[3,1,3],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
@@ -29,7 +29,7 @@ async def test_alert_config(mock_device: DeviceMock) -> None:
29
29
 
30
30
  @pytest.mark.g90device(sent_data=[
31
31
  b"ISTART[117,[1]]IEND\0",
32
- b"ISTART[117,[3]]IEND\0",
32
+ b"ISTART[117,[1]]IEND\0",
33
33
  b"ISTARTIEND\0",
34
34
  ])
35
35
  async def test_set_alert_config(mock_device: DeviceMock) -> None:
@@ -38,15 +38,16 @@ async def test_set_alert_config(mock_device: DeviceMock) -> None:
38
38
  """
39
39
  g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
40
40
 
41
- await g90.alert_config.set_flag(G90AlertConfigFlags.AC_POWER_FAILURE, True)
42
- await g90.alert_config.set_flag(G90AlertConfigFlags.HOST_LOW_VOLTAGE, True)
41
+ # The flag is already set, so no update should be performed
42
+ await g90.alert_config.set_flag(
43
+ G90AlertConfigFlags.AC_POWER_FAILURE, True
44
+ )
45
+ await g90.alert_config.set_flag(
46
+ G90AlertConfigFlags.HOST_LOW_VOLTAGE, True
47
+ )
48
+
43
49
  assert await mock_device.recv_data == [
44
50
  b'ISTART[117,117,""]IEND\0',
45
51
  b'ISTART[117,117,""]IEND\0',
46
52
  b"ISTART[116,116,[116,[9]]]IEND\0",
47
53
  ]
48
- # Validate we retrieve same alert configuration just has been set
49
- assert await g90.alert_config.get_flag(
50
- G90AlertConfigFlags.AC_POWER_FAILURE) is True
51
- assert await g90.alert_config.get_flag(
52
- G90AlertConfigFlags.HOST_LOW_VOLTAGE) is True
@@ -0,0 +1,192 @@
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