pyg90alarm 2.6.0__tar.gz → 2.7.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.6.0 → pyg90alarm-2.7.1}/.github/workflows/main.yml +28 -9
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/PKG-INFO +28 -26
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/README.rst +27 -25
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/local-protocol.rst +59 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/alarm.py +118 -15
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/messages.py +5 -9
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/notifications.py +14 -10
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/protocol.py +2 -2
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/const.py +32 -5
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/base_cmd.py +170 -100
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/notifications.py +5 -5
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/paginated_cmd.py +5 -10
- pyg90alarm-2.7.1/src/pyg90alarm/local/system_cmd.py +276 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/PKG-INFO +28 -26
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/SOURCES.txt +2 -1
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/conftest.py +1 -1
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/device_mock.py +21 -18
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_alarm.py +13 -13
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_cloud_notifications.py +43 -15
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_local_notifications.py +22 -22
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_sensor.py +1 -1
- pyg90alarm-2.7.1/tests/test_system_commands.py +128 -0
- pyg90alarm-2.6.0/.github/workflows/release.yaml +0 -45
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.github/CODEOWNERS +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.github/dependabot.yml +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.gitignore +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.pylintrc +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.readthedocs.yaml +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/LICENSE +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/MANIFEST.in +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/.DS_Store +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/.gitignore +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/api-docs.rst +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/cloud-protocol.rst +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/conf.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/index.rst +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/requirements.txt +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/pyproject.toml +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/setup.cfg +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/setup.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/sonar-project.properties +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/__init__.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/const.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/dataclass/__init__.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/dataclass/load_save.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/dataclass/validation.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/definitions/base.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/definitions/devices.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/definitions/sensors.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/base_entity.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/base_list.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/device_list.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/sensor.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/sensor_list.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/event_mapping.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/__init__.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/alarm_phones.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/alert_config.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/config.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/discovery.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/history.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/host_config.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/host_info.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/host_status.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/net_config.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/paginated_result.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/targeted_discovery.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/user_data_crc.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/notifications/__init__.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/notifications/base.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/notifications/protocol.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/py.typed +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/__init__.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_alarm_phones.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_base_commands.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_config.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_devices.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_discovery.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_history.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_host_config.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_net_config.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_paginated_commands.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/dataclass/test_dataclass_load_save.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/dataclass/test_dataclass_load_save_descriptor.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/dataclass/test_dataclass_load_save_serialize.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/dataclass/test_validation.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/entities/test_base_list.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/test_exceptions.py +0 -0
- {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tox.ini +0 -0
|
@@ -3,8 +3,6 @@ name: main
|
|
|
3
3
|
|
|
4
4
|
on:
|
|
5
5
|
pull_request:
|
|
6
|
-
release:
|
|
7
|
-
types: [published]
|
|
8
6
|
push:
|
|
9
7
|
branches:
|
|
10
8
|
- main
|
|
@@ -68,10 +66,35 @@ jobs:
|
|
|
68
66
|
-Dsonar.projectVersion=${{ steps.package-version.outputs.VALUE }}
|
|
69
67
|
# yamllint enable rule:line-length
|
|
70
68
|
|
|
69
|
+
release:
|
|
70
|
+
name: Release with semantic-release
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
needs: [tests]
|
|
73
|
+
permissions:
|
|
74
|
+
contents: write
|
|
75
|
+
issues: write
|
|
76
|
+
pull-requests: write
|
|
77
|
+
outputs:
|
|
78
|
+
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
|
|
79
|
+
steps:
|
|
80
|
+
- name: Checkout code (SSH)
|
|
81
|
+
uses: actions/checkout@v6
|
|
82
|
+
with:
|
|
83
|
+
fetch-depth: 0
|
|
84
|
+
|
|
85
|
+
- name: Run semantic release
|
|
86
|
+
id: semantic
|
|
87
|
+
uses: cycjimmy/semantic-release-action@v6
|
|
88
|
+
with:
|
|
89
|
+
extra_plugins: |
|
|
90
|
+
conventional-changelog-conventionalcommits@9.1.0
|
|
91
|
+
env:
|
|
92
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
93
|
+
|
|
71
94
|
pypi-publish:
|
|
72
95
|
name: Publish to PyPi
|
|
73
96
|
runs-on: ubuntu-latest
|
|
74
|
-
needs: [tests]
|
|
97
|
+
needs: [tests, release]
|
|
75
98
|
permissions:
|
|
76
99
|
id-token: write # Required for trusted publishing
|
|
77
100
|
steps:
|
|
@@ -90,7 +113,7 @@ jobs:
|
|
|
90
113
|
- name: Publish the package to Test PyPi
|
|
91
114
|
# Skip publishing to test PyPI if we're performing release, there might
|
|
92
115
|
# be already the version of the package from the merge to master branch
|
|
93
|
-
if:
|
|
116
|
+
if: needs.release.outputs.new_release_published != 'true'
|
|
94
117
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
95
118
|
with:
|
|
96
119
|
repository-url: https://test.pypi.org/legacy/
|
|
@@ -98,11 +121,7 @@ jobs:
|
|
|
98
121
|
- name: Publish the release to PyPi
|
|
99
122
|
# Publish to production PyPi only happens when a release published out
|
|
100
123
|
# of the main branch
|
|
101
|
-
if:
|
|
102
|
-
github.event_name == 'release'
|
|
103
|
-
&& github.event.action == 'published'
|
|
104
|
-
&& (github.event.release.target_commitish == 'main'
|
|
105
|
-
|| github.event.release.target_commitish == 'master')
|
|
124
|
+
if: needs.release.outputs.new_release_published == 'true'
|
|
106
125
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
107
126
|
with:
|
|
108
127
|
attestations: true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyg90alarm
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.1
|
|
4
4
|
Summary: G90 Alarm system protocol
|
|
5
5
|
Home-page: https://github.com/hostcc/pyg90alarm
|
|
6
6
|
Author: Ilia Sotnikov
|
|
@@ -189,7 +189,7 @@ Cloud notifications
|
|
|
189
189
|
The cloud protocol is native to the panel and is used to interact with mobile application. The package can mimic the cloud server and interpret the messages the panel sends to the cloud, allowing to receive the notifications and alerts.
|
|
190
190
|
While the protocol also allows to send commands to the panel, it is not implemented and local protocol is used for that - i.e. when cloud notifications are in use the local protocol still utilized for sending commands to the panel.
|
|
191
191
|
|
|
192
|
-
The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port
|
|
192
|
+
The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port, which could be customized. To process the cloud notifications all the traffic from panel towards the configured IP address service needs to be received by the node where the package is running.
|
|
193
193
|
|
|
194
194
|
Please see
|
|
195
195
|
`the section <docs/cloud-protocol.rst>`_ for further details on the protocol.
|
|
@@ -203,25 +203,6 @@ The package could act as:
|
|
|
203
203
|
- Chained cloud server, where in addition to interpreting the notifications it
|
|
204
204
|
will also forward all packets received from the panel to the cloud server, and pass its responses back to the panel. This allows to have notifications processed by the package and the mobile application working as well.
|
|
205
205
|
|
|
206
|
-
.. note:: Sending packets upstream to the known IP address and port of the cloud server might result in those looped back (since traffic from panel to cloud service has to be redirected to the host where package runs), if your network equipment can't account for source address in redirection rules (i.e. limiting the port redirection to the panel's IP address). In that case you'll need another redirection, from the host where the package runs to the cloud service using an IP from your network. That way those two redirection rules will coexist correctly. To illustrate:
|
|
207
|
-
|
|
208
|
-
Port forwarding rule 1:
|
|
209
|
-
|
|
210
|
-
- Source: panel IP address
|
|
211
|
-
- Destination: 47.88.7.61
|
|
212
|
-
- Port: 5678
|
|
213
|
-
- Redirect to host: host where package runs
|
|
214
|
-
- Redirect to port: 5678 (or other port if you want to use it)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
Port forwarding rule 2 (optional):
|
|
218
|
-
|
|
219
|
-
- Source: host where package runs
|
|
220
|
-
- Destination: an IP address from your network
|
|
221
|
-
- Port: 5678 (or other port if you want to use it)
|
|
222
|
-
- Redirect to : 47.88.7.61
|
|
223
|
-
- Redirect to port: 5678
|
|
224
|
-
|
|
225
206
|
The code fragments below demonstrate how to utilize both modes - please note those are incomplete, since no callbacks are set to process the notifications.
|
|
226
207
|
|
|
227
208
|
**Standalone mode**
|
|
@@ -232,10 +213,20 @@ The code fragments below demonstrate how to utilize both modes - please note tho
|
|
|
232
213
|
|
|
233
214
|
# Create an instance of the alarm panel
|
|
234
215
|
alarm = G90Alarm(host='<panel IP address>')
|
|
216
|
+
|
|
217
|
+
# Configure cloud server address the panel should use - the host running the
|
|
218
|
+
# package.
|
|
219
|
+
await alarm.set_cloud_server_address(
|
|
220
|
+
cloud_ip='<host IP address running the package>', cloud_port=5678
|
|
221
|
+
)
|
|
222
|
+
|
|
235
223
|
# Enable cloud notifications
|
|
236
224
|
await alarm.use_cloud_notifications(
|
|
237
|
-
#
|
|
238
|
-
|
|
225
|
+
# The host/port the package will listen on for the cloud notifications,
|
|
226
|
+
# should match ones above.
|
|
227
|
+
cloud_ip='<host IP address running the package>',
|
|
228
|
+
cloud_port=5678,
|
|
229
|
+
cloud_local_port=5678,
|
|
239
230
|
upstream_host=None
|
|
240
231
|
)
|
|
241
232
|
# Start listening for notifications
|
|
@@ -250,11 +241,22 @@ The code fragments below demonstrate how to utilize both modes - please note tho
|
|
|
250
241
|
|
|
251
242
|
# Create an instance of the alarm panel
|
|
252
243
|
alarm = G90Alarm(host='<panel IP address>')
|
|
244
|
+
|
|
245
|
+
# Configure cloud server address the panel should use - the host running the
|
|
246
|
+
# package.
|
|
247
|
+
await alarm.set_cloud_server_address(
|
|
248
|
+
cloud_ip='<host IP address running the package>', cloud_port=5678
|
|
249
|
+
)
|
|
250
|
+
|
|
253
251
|
# Enable cloud notifications
|
|
254
252
|
await alarm.use_cloud_notifications(
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
|
|
253
|
+
# The host/port the package will listen on for the cloud notifications,
|
|
254
|
+
# should match ones above.
|
|
255
|
+
cloud_ip='<host IP address running the package>',
|
|
256
|
+
cloud_port=5678,
|
|
257
|
+
cloud_local_port=5678,
|
|
258
|
+
# Upstream cloud server address the package should forward the
|
|
259
|
+
# notifications to.
|
|
258
260
|
upstream_host='47.88.7.61',
|
|
259
261
|
upstream_port=5678
|
|
260
262
|
)
|
|
@@ -142,7 +142,7 @@ Cloud notifications
|
|
|
142
142
|
The cloud protocol is native to the panel and is used to interact with mobile application. The package can mimic the cloud server and interpret the messages the panel sends to the cloud, allowing to receive the notifications and alerts.
|
|
143
143
|
While the protocol also allows to send commands to the panel, it is not implemented and local protocol is used for that - i.e. when cloud notifications are in use the local protocol still utilized for sending commands to the panel.
|
|
144
144
|
|
|
145
|
-
The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port
|
|
145
|
+
The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port, which could be customized. To process the cloud notifications all the traffic from panel towards the configured IP address service needs to be received by the node where the package is running.
|
|
146
146
|
|
|
147
147
|
Please see
|
|
148
148
|
`the section <docs/cloud-protocol.rst>`_ for further details on the protocol.
|
|
@@ -156,25 +156,6 @@ The package could act as:
|
|
|
156
156
|
- Chained cloud server, where in addition to interpreting the notifications it
|
|
157
157
|
will also forward all packets received from the panel to the cloud server, and pass its responses back to the panel. This allows to have notifications processed by the package and the mobile application working as well.
|
|
158
158
|
|
|
159
|
-
.. note:: Sending packets upstream to the known IP address and port of the cloud server might result in those looped back (since traffic from panel to cloud service has to be redirected to the host where package runs), if your network equipment can't account for source address in redirection rules (i.e. limiting the port redirection to the panel's IP address). In that case you'll need another redirection, from the host where the package runs to the cloud service using an IP from your network. That way those two redirection rules will coexist correctly. To illustrate:
|
|
160
|
-
|
|
161
|
-
Port forwarding rule 1:
|
|
162
|
-
|
|
163
|
-
- Source: panel IP address
|
|
164
|
-
- Destination: 47.88.7.61
|
|
165
|
-
- Port: 5678
|
|
166
|
-
- Redirect to host: host where package runs
|
|
167
|
-
- Redirect to port: 5678 (or other port if you want to use it)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Port forwarding rule 2 (optional):
|
|
171
|
-
|
|
172
|
-
- Source: host where package runs
|
|
173
|
-
- Destination: an IP address from your network
|
|
174
|
-
- Port: 5678 (or other port if you want to use it)
|
|
175
|
-
- Redirect to : 47.88.7.61
|
|
176
|
-
- Redirect to port: 5678
|
|
177
|
-
|
|
178
159
|
The code fragments below demonstrate how to utilize both modes - please note those are incomplete, since no callbacks are set to process the notifications.
|
|
179
160
|
|
|
180
161
|
**Standalone mode**
|
|
@@ -185,10 +166,20 @@ The code fragments below demonstrate how to utilize both modes - please note tho
|
|
|
185
166
|
|
|
186
167
|
# Create an instance of the alarm panel
|
|
187
168
|
alarm = G90Alarm(host='<panel IP address>')
|
|
169
|
+
|
|
170
|
+
# Configure cloud server address the panel should use - the host running the
|
|
171
|
+
# package.
|
|
172
|
+
await alarm.set_cloud_server_address(
|
|
173
|
+
cloud_ip='<host IP address running the package>', cloud_port=5678
|
|
174
|
+
)
|
|
175
|
+
|
|
188
176
|
# Enable cloud notifications
|
|
189
177
|
await alarm.use_cloud_notifications(
|
|
190
|
-
#
|
|
191
|
-
|
|
178
|
+
# The host/port the package will listen on for the cloud notifications,
|
|
179
|
+
# should match ones above.
|
|
180
|
+
cloud_ip='<host IP address running the package>',
|
|
181
|
+
cloud_port=5678,
|
|
182
|
+
cloud_local_port=5678,
|
|
192
183
|
upstream_host=None
|
|
193
184
|
)
|
|
194
185
|
# Start listening for notifications
|
|
@@ -203,11 +194,22 @@ The code fragments below demonstrate how to utilize both modes - please note tho
|
|
|
203
194
|
|
|
204
195
|
# Create an instance of the alarm panel
|
|
205
196
|
alarm = G90Alarm(host='<panel IP address>')
|
|
197
|
+
|
|
198
|
+
# Configure cloud server address the panel should use - the host running the
|
|
199
|
+
# package.
|
|
200
|
+
await alarm.set_cloud_server_address(
|
|
201
|
+
cloud_ip='<host IP address running the package>', cloud_port=5678
|
|
202
|
+
)
|
|
203
|
+
|
|
206
204
|
# Enable cloud notifications
|
|
207
205
|
await alarm.use_cloud_notifications(
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
# The host/port the package will listen on for the cloud notifications,
|
|
207
|
+
# should match ones above.
|
|
208
|
+
cloud_ip='<host IP address running the package>',
|
|
209
|
+
cloud_port=5678,
|
|
210
|
+
cloud_local_port=5678,
|
|
211
|
+
# Upstream cloud server address the package should forward the
|
|
212
|
+
# notifications to.
|
|
211
213
|
upstream_host='47.88.7.61',
|
|
212
214
|
upstream_port=5678
|
|
213
215
|
)
|
|
@@ -158,3 +158,62 @@ uses ``utf-8`` encoding.
|
|
|
158
158
|
|
|
159
159
|
Data varies across different notification and alert types, see
|
|
160
160
|
`src/pyg90alarm/local/notifications.py <../../src/pyg90alarm/local/notifications.py>`_.
|
|
161
|
+
|
|
162
|
+
System commands
|
|
163
|
+
---------------
|
|
164
|
+
|
|
165
|
+
In addition to regular commands, the local protocol also supports system commands that perform device maintenance operations. Unlike regular commands, system commands do not expect a response from the device and are invoked using a special wire format based on AT commands.
|
|
166
|
+
|
|
167
|
+
.. note:: System commands are not exposed through regular command interface but have their own dedicated methods in the ``G90Alarm`` class.
|
|
168
|
+
|
|
169
|
+
Wire Format
|
|
170
|
+
^^^^^^^^^^^
|
|
171
|
+
|
|
172
|
+
System commands use a different wire format compared to regular commands:
|
|
173
|
+
|
|
174
|
+
:samp:`ISTART[0,100,"AT^IWT={command code}{command data},IWT"]IEND\\0`
|
|
175
|
+
|
|
176
|
+
The command is wrapped in an AT command format (``AT^IWT=...``) within a regular command structure with fixed codes ``0`` and ``100``.
|
|
177
|
+
|
|
178
|
+
Available System Commands
|
|
179
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
180
|
+
|
|
181
|
+
The following system commands are available:
|
|
182
|
+
|
|
183
|
+
- MCU Reboot (code ``1123``)
|
|
184
|
+
Reboots the Main Control Unit (MCU) of the alarm panel.
|
|
185
|
+
|
|
186
|
+
**Wire format example:**
|
|
187
|
+
|
|
188
|
+
:samp:`ISTART[0,100,"AT^IWT=1123,IWT"]IEND\\0`
|
|
189
|
+
|
|
190
|
+
- GSM Reboot (code ``1129``)
|
|
191
|
+
Reboots the GSM module of the alarm panel.
|
|
192
|
+
|
|
193
|
+
**Wire format example:**
|
|
194
|
+
|
|
195
|
+
:samp:`ISTART[0,100,"AT^IWT=1129,IWT"]IEND\\0`
|
|
196
|
+
|
|
197
|
+
- WiFi Reboot (code ``1006``)
|
|
198
|
+
Reboots the WiFi module of the alarm panel.
|
|
199
|
+
|
|
200
|
+
**Wire format example:**
|
|
201
|
+
|
|
202
|
+
:samp:`ISTART[0,100,"AT^IWT=1006,IWT"]IEND\\0`
|
|
203
|
+
|
|
204
|
+
- Set Server Address (configuration command ``1`` with sub-command ``78``)
|
|
205
|
+
Configures the cloud server address that the alarm panel connects to.
|
|
206
|
+
The command requires three parameters separated by ``&``: cloud primary host,
|
|
207
|
+
cloud secondary host, and cloud port number.
|
|
208
|
+
|
|
209
|
+
There is no indication the panel will use DNS resolution, so both addresses should be IP ones. Also, it is unclear what 2nd address is used for - experiments revealed the panel always connects to the 1st one.
|
|
210
|
+
|
|
211
|
+
**Wire format example:**
|
|
212
|
+
|
|
213
|
+
:samp:`ISTART[0,100,"AT^IWT=1,78=192.168.1.100&192.168.1.100&5678,IWT"]IEND\\0`
|
|
214
|
+
|
|
215
|
+
Where:
|
|
216
|
+
|
|
217
|
+
- ``192.168.1.100`` is the primary IP address of the cloud server
|
|
218
|
+
- ``192.168.1.100`` is the secondary IP address of the cloud server (see above note)
|
|
219
|
+
- ``5678`` is the port number the cloud server listens on
|
|
@@ -60,14 +60,14 @@ from typing import (
|
|
|
60
60
|
Callable, Coroutine, Union
|
|
61
61
|
)
|
|
62
62
|
from .const import (
|
|
63
|
-
G90Commands,
|
|
63
|
+
G90Commands, G90SystemCommands,
|
|
64
|
+
REMOTE_PORT,
|
|
64
65
|
REMOTE_TARGETED_DISCOVERY_PORT,
|
|
65
66
|
LOCAL_TARGETED_DISCOVERY_PORT,
|
|
66
|
-
|
|
67
|
+
LOCAL_NOTIFICATIONS_IP,
|
|
67
68
|
LOCAL_NOTIFICATIONS_PORT,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
REMOTE_CLOUD_HOST,
|
|
69
|
+
LOCAL_CLOUD_NOTIFICATIONS_IP,
|
|
70
|
+
LOCAL_CLOUD_NOTIFICATIONS_PORT,
|
|
71
71
|
REMOTE_CLOUD_PORT,
|
|
72
72
|
DEVICE_REGISTRATION_TIMEOUT,
|
|
73
73
|
ROOM_ID,
|
|
@@ -75,7 +75,8 @@ from .const import (
|
|
|
75
75
|
G90RemoteButtonStates,
|
|
76
76
|
G90RFIDKeypadStates,
|
|
77
77
|
)
|
|
78
|
-
from .local.base_cmd import
|
|
78
|
+
from .local.base_cmd import G90BaseCommand, BaseCommandsDataT
|
|
79
|
+
from .local.system_cmd import G90SystemCommand, G90SetServerAddressCommand
|
|
79
80
|
from .local.paginated_result import G90PaginatedResult, G90PaginatedResponse
|
|
80
81
|
from .entities.base_list import ListChangeCallback
|
|
81
82
|
from .entities.sensor import G90Sensor
|
|
@@ -263,8 +264,8 @@ class G90Alarm(G90NotificationProtocol):
|
|
|
263
264
|
return self._port
|
|
264
265
|
|
|
265
266
|
async def command(
|
|
266
|
-
self, code: G90Commands, data: Optional[
|
|
267
|
-
) ->
|
|
267
|
+
self, code: G90Commands, data: Optional[BaseCommandsDataT] = None
|
|
268
|
+
) -> BaseCommandsDataT:
|
|
268
269
|
"""
|
|
269
270
|
Invokes a command against the alarm panel.
|
|
270
271
|
|
|
@@ -272,7 +273,7 @@ class G90Alarm(G90NotificationProtocol):
|
|
|
272
273
|
:param data: Command data
|
|
273
274
|
:return: The result of command invocation
|
|
274
275
|
"""
|
|
275
|
-
cmd
|
|
276
|
+
cmd = await G90BaseCommand(
|
|
276
277
|
self._host, self._port, code, data).process()
|
|
277
278
|
return cmd.result
|
|
278
279
|
|
|
@@ -292,6 +293,81 @@ class G90Alarm(G90NotificationProtocol):
|
|
|
292
293
|
self._host, self._port, code, start, end
|
|
293
294
|
).process()
|
|
294
295
|
|
|
296
|
+
async def mcu_reboot(self) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Reboots the MCU of the alarm panel.
|
|
299
|
+
|
|
300
|
+
Note that underlying command doesn't return any result, so there is no
|
|
301
|
+
feedback from the panel upon execution.
|
|
302
|
+
"""
|
|
303
|
+
await G90SystemCommand(
|
|
304
|
+
host=self._host, port=self._port,
|
|
305
|
+
code=G90SystemCommands.MCU_REBOOT
|
|
306
|
+
).process()
|
|
307
|
+
|
|
308
|
+
async def gsm_reboot(self) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Reboots the GSM module of the alarm panel.
|
|
311
|
+
|
|
312
|
+
Note that underlying command doesn't return any result, so there is no
|
|
313
|
+
feedback from the panel upon execution.
|
|
314
|
+
"""
|
|
315
|
+
await G90SystemCommand(
|
|
316
|
+
host=self._host, port=self._port,
|
|
317
|
+
code=G90SystemCommands.GSM_REBOOT
|
|
318
|
+
).process()
|
|
319
|
+
|
|
320
|
+
async def wifi_reboot(self) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Reboots the WiFi module of the alarm panel.
|
|
323
|
+
|
|
324
|
+
Note that underlying command doesn't return any result, so there is no
|
|
325
|
+
feedback from the panel upon execution.
|
|
326
|
+
"""
|
|
327
|
+
await G90SystemCommand(
|
|
328
|
+
host=self._host, port=self._port,
|
|
329
|
+
code=G90SystemCommands.WIFI_REBOOT
|
|
330
|
+
).process()
|
|
331
|
+
|
|
332
|
+
async def reboot(self) -> None:
|
|
333
|
+
"""
|
|
334
|
+
Reboots the entire alarm panel.
|
|
335
|
+
|
|
336
|
+
The system commands performing reboot of a panel's module don't return
|
|
337
|
+
anything so there is no feedback from the panel upon execution, hence
|
|
338
|
+
the commands are spaced with delays to allow the panel to process
|
|
339
|
+
them.
|
|
340
|
+
|
|
341
|
+
Please be aware that the delays are determined experimentally and might
|
|
342
|
+
be too long or too short.
|
|
343
|
+
"""
|
|
344
|
+
await self.gsm_reboot()
|
|
345
|
+
await asyncio.sleep(1)
|
|
346
|
+
await self.mcu_reboot()
|
|
347
|
+
await asyncio.sleep(1)
|
|
348
|
+
await self.wifi_reboot()
|
|
349
|
+
# The MCU likely needs more than 1 second to reboot, but checking it
|
|
350
|
+
# completed the reboot should be done separately
|
|
351
|
+
await asyncio.sleep(1)
|
|
352
|
+
|
|
353
|
+
async def set_cloud_server_address(
|
|
354
|
+
self, cloud_ip: str, cloud_port: int
|
|
355
|
+
) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Sets the cloud server address the alarm panel connects to.
|
|
358
|
+
|
|
359
|
+
:param cloud_ip: IP address of the server to receive cloud protocol
|
|
360
|
+
notifications, should be reachable from the panel. Typically it is the
|
|
361
|
+
IP address of the host running the package
|
|
362
|
+
:param cloud_port: Port number of the server to receive cloud protocol
|
|
363
|
+
notifications, should be reachable from the panel
|
|
364
|
+
"""
|
|
365
|
+
await G90SetServerAddressCommand(
|
|
366
|
+
host=self._host, port=self._port,
|
|
367
|
+
cloud_ip=cloud_ip,
|
|
368
|
+
cloud_port=cloud_port
|
|
369
|
+
).process()
|
|
370
|
+
|
|
295
371
|
@classmethod
|
|
296
372
|
async def discover(cls) -> List[G90DiscoveredDevice]:
|
|
297
373
|
"""
|
|
@@ -1303,7 +1379,7 @@ class G90Alarm(G90NotificationProtocol):
|
|
|
1303
1379
|
await asyncio.sleep(interval)
|
|
1304
1380
|
|
|
1305
1381
|
async def use_local_notifications(
|
|
1306
|
-
self,
|
|
1382
|
+
self, notifications_local_ip: str = LOCAL_NOTIFICATIONS_IP,
|
|
1307
1383
|
notifications_local_port: int = LOCAL_NOTIFICATIONS_PORT
|
|
1308
1384
|
) -> None:
|
|
1309
1385
|
"""
|
|
@@ -1315,20 +1391,45 @@ class G90Alarm(G90NotificationProtocol):
|
|
|
1315
1391
|
protocol_factory=lambda: self,
|
|
1316
1392
|
host=self._host,
|
|
1317
1393
|
port=self._port,
|
|
1318
|
-
|
|
1394
|
+
local_ip=notifications_local_ip,
|
|
1319
1395
|
local_port=notifications_local_port
|
|
1320
1396
|
)
|
|
1321
1397
|
|
|
1322
1398
|
# pylint: disable=too-many-positional-arguments
|
|
1323
1399
|
async def use_cloud_notifications(
|
|
1324
|
-
self,
|
|
1325
|
-
|
|
1326
|
-
|
|
1400
|
+
self,
|
|
1401
|
+
cloud_ip: str,
|
|
1402
|
+
cloud_port: int,
|
|
1403
|
+
cloud_local_ip: str = LOCAL_CLOUD_NOTIFICATIONS_IP,
|
|
1404
|
+
cloud_local_port: int = LOCAL_CLOUD_NOTIFICATIONS_PORT,
|
|
1405
|
+
upstream_host: Optional[str] = None,
|
|
1327
1406
|
upstream_port: Optional[int] = REMOTE_CLOUD_PORT,
|
|
1328
1407
|
keep_single_connection: bool = True
|
|
1329
1408
|
) -> None:
|
|
1330
1409
|
"""
|
|
1331
1410
|
Switches to use cloud notifications for device alerts.
|
|
1411
|
+
|
|
1412
|
+
Please note the method does not configure the panel for the host to
|
|
1413
|
+
receive the notifications - please invoke
|
|
1414
|
+
:meth:`G90Alarm.set_cloud_server_address` method to do that. The reason
|
|
1415
|
+
of that is configuring cloud server address on the panel is one-time
|
|
1416
|
+
operation, while the method will be called multiple times.
|
|
1417
|
+
|
|
1418
|
+
:param cloud_ip: The IP address of cloud server to connect to, should
|
|
1419
|
+
be reachable from the panel
|
|
1420
|
+
:param cloud_port: The cloud server port to connect to, should be
|
|
1421
|
+
reachable from the panel
|
|
1422
|
+
:param cloud_local_ip: Local IP address to bind cloud notifications
|
|
1423
|
+
listener to
|
|
1424
|
+
:param cloud_local_port: Local port to bind cloud notifications
|
|
1425
|
+
listener to, should match `cloud_port` above unless network setup
|
|
1426
|
+
dictates otherwise
|
|
1427
|
+
:param upstream_host: Optional upstream host to connect to cloud
|
|
1428
|
+
server through
|
|
1429
|
+
:param upstream_port: Optional upstream port to connect to cloud
|
|
1430
|
+
server through
|
|
1431
|
+
:param keep_single_connection: If enabled, keeps a single connection
|
|
1432
|
+
to the upstream cloud server for both sending and receiving data
|
|
1332
1433
|
"""
|
|
1333
1434
|
await self.close_notifications()
|
|
1334
1435
|
|
|
@@ -1336,8 +1437,10 @@ class G90Alarm(G90NotificationProtocol):
|
|
|
1336
1437
|
protocol_factory=lambda: self,
|
|
1337
1438
|
upstream_host=upstream_host,
|
|
1338
1439
|
upstream_port=upstream_port,
|
|
1339
|
-
|
|
1440
|
+
local_ip=cloud_local_ip,
|
|
1340
1441
|
local_port=cloud_local_port,
|
|
1442
|
+
cloud_ip=cloud_ip,
|
|
1443
|
+
cloud_port=cloud_port,
|
|
1341
1444
|
keep_single_connection=keep_single_connection
|
|
1342
1445
|
)
|
|
1343
1446
|
|
|
@@ -37,7 +37,7 @@ from .protocol import (
|
|
|
37
37
|
)
|
|
38
38
|
from .const import G90CloudDirection, G90CloudCommand
|
|
39
39
|
from ..const import (
|
|
40
|
-
G90AlertStateChangeTypes,
|
|
40
|
+
G90AlertStateChangeTypes,
|
|
41
41
|
G90AlertTypes, G90AlertSources, G90AlertStates,
|
|
42
42
|
)
|
|
43
43
|
from ..definitions.base import G90PeripheralTypes
|
|
@@ -212,15 +212,11 @@ class G90CloudHelloDiscoveryRespMessage(G90CloudMessage):
|
|
|
212
212
|
_source = G90CloudDirection.CLOUD_DISCOVERY
|
|
213
213
|
_destination = G90CloudDirection.DEVICE
|
|
214
214
|
|
|
215
|
-
#
|
|
216
|
-
|
|
217
|
-
# simulated cloud service will use same IP address for unification (i.e.
|
|
218
|
-
# traffic redicrection will always be used to divert panel's cloud traffic
|
|
219
|
-
# to the simulated cloud service)
|
|
220
|
-
ip_addr: bytes = REMOTE_CLOUD_HOST.encode()
|
|
215
|
+
# The default values are set in `__post_init__()` method below
|
|
216
|
+
ip_addr: bytes = b''
|
|
221
217
|
flag2: int = 0
|
|
222
218
|
flag3: int = 0
|
|
223
|
-
port: int =
|
|
219
|
+
port: int = 0
|
|
224
220
|
_timestamp: int = 0 # unix timestamp
|
|
225
221
|
|
|
226
222
|
def __post_init__(self, context: G90CloudMessageContext) -> None:
|
|
@@ -230,7 +226,7 @@ class G90CloudHelloDiscoveryRespMessage(G90CloudMessage):
|
|
|
230
226
|
_LOGGER.debug(
|
|
231
227
|
"%s: Timestamp added: %s", type(self).__name__, str(self)
|
|
232
228
|
)
|
|
233
|
-
self.ip_addr = context.
|
|
229
|
+
self.ip_addr = context.cloud_ip.encode()
|
|
234
230
|
self.port = context.cloud_port
|
|
235
231
|
|
|
236
232
|
@property
|
|
@@ -45,7 +45,6 @@ from .messages import (
|
|
|
45
45
|
)
|
|
46
46
|
from ..notifications.base import G90NotificationsBase
|
|
47
47
|
from ..notifications.protocol import G90NotificationProtocol
|
|
48
|
-
from ..const import (REMOTE_CLOUD_HOST, REMOTE_CLOUD_PORT)
|
|
49
48
|
|
|
50
49
|
|
|
51
50
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -62,7 +61,9 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
|
|
|
62
61
|
|
|
63
62
|
:param protocol_factory: Factory function to create notification
|
|
64
63
|
protocol handlers
|
|
65
|
-
:param
|
|
64
|
+
:param cloud_ip: Cloud IP address to announce to the panel
|
|
65
|
+
:param cloud_port: Cloud port to announce to the panel
|
|
66
|
+
:param local_ip: Local IP to bind the server to
|
|
66
67
|
:param local_port: Local port to bind the server to
|
|
67
68
|
:param upstream_host: Optional upstream host to forward messages to
|
|
68
69
|
:param upstream_port: Optional upstream port to forward messages to
|
|
@@ -74,7 +75,8 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
|
|
|
74
75
|
def __init__(
|
|
75
76
|
self,
|
|
76
77
|
protocol_factory: Callable[[], G90NotificationProtocol],
|
|
77
|
-
|
|
78
|
+
local_ip: str, local_port: int,
|
|
79
|
+
cloud_ip: str, cloud_port: int,
|
|
78
80
|
upstream_host: Optional[str] = None,
|
|
79
81
|
upstream_port: Optional[int] = None,
|
|
80
82
|
keep_single_connection: bool = True,
|
|
@@ -82,8 +84,10 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
|
|
|
82
84
|
super().__init__(protocol_factory)
|
|
83
85
|
self._transport: Optional[Transport] = None
|
|
84
86
|
self._server: Optional[asyncio.Server] = None
|
|
85
|
-
self.
|
|
87
|
+
self._local_ip = local_ip
|
|
86
88
|
self._local_port = local_port
|
|
89
|
+
self._cloud_ip = cloud_ip
|
|
90
|
+
self._cloud_port = cloud_port
|
|
87
91
|
self._upstream_host = upstream_host
|
|
88
92
|
self._upstream_port = upstream_port
|
|
89
93
|
self._keep_single_connection = keep_single_connection
|
|
@@ -162,10 +166,10 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
|
|
|
162
166
|
# Instantiate a context for the messages
|
|
163
167
|
ctx = G90CloudMessageContext(
|
|
164
168
|
device_id=self.device_id,
|
|
165
|
-
|
|
169
|
+
local_ip=self._local_ip,
|
|
166
170
|
local_port=self._local_port,
|
|
167
|
-
|
|
168
|
-
cloud_port=
|
|
171
|
+
cloud_ip=self._cloud_ip,
|
|
172
|
+
cloud_port=self._cloud_port,
|
|
169
173
|
upstream_host=self._upstream_host,
|
|
170
174
|
upstream_port=self._upstream_port,
|
|
171
175
|
remote_host=host,
|
|
@@ -371,11 +375,11 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
|
|
|
371
375
|
loop = asyncio.get_running_loop()
|
|
372
376
|
|
|
373
377
|
_LOGGER.debug('Creating cloud endpoint for %s:%s',
|
|
374
|
-
self.
|
|
378
|
+
self._local_ip,
|
|
375
379
|
self._local_port)
|
|
376
380
|
self._server = await loop.create_server(
|
|
377
381
|
lambda: self,
|
|
378
|
-
self.
|
|
382
|
+
self._local_ip, self._local_port
|
|
379
383
|
)
|
|
380
384
|
|
|
381
385
|
async def close(self) -> None:
|
|
@@ -404,7 +408,7 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
|
|
|
404
408
|
if self._server:
|
|
405
409
|
_LOGGER.debug(
|
|
406
410
|
'No longer listening for cloud connections on %s:%s',
|
|
407
|
-
self.
|
|
411
|
+
self._local_ip, self._local_port
|
|
408
412
|
)
|
|
409
413
|
self._server.close()
|
|
410
414
|
self._server = None
|
|
@@ -73,11 +73,11 @@ class G90CloudMessageContext: # pylint:disable=too-many-instance-attributes
|
|
|
73
73
|
This class holds information about the local and remote hosts and ports,
|
|
74
74
|
as well as the cloud server and upstream connection details.
|
|
75
75
|
"""
|
|
76
|
-
|
|
76
|
+
local_ip: str
|
|
77
77
|
local_port: int
|
|
78
78
|
remote_host: str
|
|
79
79
|
remote_port: int
|
|
80
|
-
|
|
80
|
+
cloud_ip: str
|
|
81
81
|
cloud_port: int
|
|
82
82
|
upstream_host: Optional[str]
|
|
83
83
|
upstream_port: Optional[int]
|