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.
- pyg90alarm-2.3.1/.github/dependabot.yml +26 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.github/workflows/main.yml +6 -6
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/PKG-INFO +1 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/pyproject.toml +1 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/alarm.py +5 -3
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/notifications.py +2 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/base_list.py +15 -10
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/base_cmd.py +1 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/config.py +28 -29
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/notifications.py +2 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/paginated_cmd.py +1 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/paginated_result.py +1 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/SOURCES.txt +3 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/device_mock.py +2 -1
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_alarm.py +13 -7
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_config.py +9 -8
- pyg90alarm-2.3.1/tests/unit/entities/test_base_list.py +192 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.github/CODEOWNERS +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.gitignore +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.pylintrc +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/.readthedocs.yaml +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/LICENSE +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/MANIFEST.in +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/README.rst +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/.DS_Store +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/.gitignore +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/api-docs.rst +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/cloud-protocol.rst +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/conf.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/index.rst +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/local-protocol.rst +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/docs/requirements.txt +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/setup.cfg +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/setup.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/sonar-project.properties +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/__init__.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/const.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/messages.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/cloud/protocol.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/const.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/definitions/base.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/definitions/devices.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/definitions/sensors.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/base_entity.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/device_list.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/sensor.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/entities/sensor_list.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/__init__.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/discovery.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/history.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/host_info.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/host_status.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/targeted_discovery.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/local/user_data_crc.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/notifications/__init__.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/notifications/base.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/notifications/protocol.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm/py.typed +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/__init__.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/conftest.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_base_commands.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_cloud_notifications.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_devices.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_discovery.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_history.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_local_notifications.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_paginated_commands.py +0 -0
- {pyg90alarm-2.2.0 → pyg90alarm-2.3.1}/tests/test_sensor.py +0 -0
- {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@
|
|
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@
|
|
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:
|
|
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@
|
|
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@
|
|
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
|
-
|
|
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
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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,[
|
|
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
|
-
|
|
42
|
-
await g90.alert_config.set_flag(
|
|
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
|
|
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
|
|
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
|
|
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
|