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.
Files changed (98) hide show
  1. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.github/workflows/main.yml +28 -9
  2. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/PKG-INFO +28 -26
  3. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/README.rst +27 -25
  4. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/local-protocol.rst +59 -0
  5. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/alarm.py +118 -15
  6. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/messages.py +5 -9
  7. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/notifications.py +14 -10
  8. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/protocol.py +2 -2
  9. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/const.py +32 -5
  10. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/base_cmd.py +170 -100
  11. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/notifications.py +5 -5
  12. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/paginated_cmd.py +5 -10
  13. pyg90alarm-2.7.1/src/pyg90alarm/local/system_cmd.py +276 -0
  14. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/PKG-INFO +28 -26
  15. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/SOURCES.txt +2 -1
  16. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/conftest.py +1 -1
  17. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/device_mock.py +21 -18
  18. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_alarm.py +13 -13
  19. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_cloud_notifications.py +43 -15
  20. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_local_notifications.py +22 -22
  21. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_sensor.py +1 -1
  22. pyg90alarm-2.7.1/tests/test_system_commands.py +128 -0
  23. pyg90alarm-2.6.0/.github/workflows/release.yaml +0 -45
  24. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.github/CODEOWNERS +0 -0
  25. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.github/dependabot.yml +0 -0
  26. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.gitignore +0 -0
  27. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.pylintrc +0 -0
  28. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/.readthedocs.yaml +0 -0
  29. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/LICENSE +0 -0
  30. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/MANIFEST.in +0 -0
  31. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/.DS_Store +0 -0
  32. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/.gitignore +0 -0
  33. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/api-docs.rst +0 -0
  34. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/cloud-protocol.rst +0 -0
  35. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/conf.py +0 -0
  36. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/index.rst +0 -0
  37. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/docs/requirements.txt +0 -0
  38. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/pyproject.toml +0 -0
  39. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/setup.cfg +0 -0
  40. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/setup.py +0 -0
  41. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/sonar-project.properties +0 -0
  42. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/__init__.py +0 -0
  43. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/callback.py +0 -0
  44. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/__init__.py +0 -0
  45. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/cloud/const.py +0 -0
  46. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/dataclass/__init__.py +0 -0
  47. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/dataclass/load_save.py +0 -0
  48. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/dataclass/validation.py +0 -0
  49. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/definitions/__init__.py +0 -0
  50. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/definitions/base.py +0 -0
  51. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/definitions/devices.py +0 -0
  52. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/definitions/sensors.py +0 -0
  53. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/__init__.py +0 -0
  54. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/base_entity.py +0 -0
  55. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/base_list.py +0 -0
  56. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/device.py +0 -0
  57. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/device_list.py +0 -0
  58. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/sensor.py +0 -0
  59. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/entities/sensor_list.py +0 -0
  60. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/event_mapping.py +0 -0
  61. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/exceptions.py +0 -0
  62. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/__init__.py +0 -0
  63. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/alarm_phones.py +0 -0
  64. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/alert_config.py +0 -0
  65. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/config.py +0 -0
  66. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/discovery.py +0 -0
  67. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/history.py +0 -0
  68. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/host_config.py +0 -0
  69. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/host_info.py +0 -0
  70. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/host_status.py +0 -0
  71. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/net_config.py +0 -0
  72. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/paginated_result.py +0 -0
  73. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/targeted_discovery.py +0 -0
  74. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/local/user_data_crc.py +0 -0
  75. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/notifications/__init__.py +0 -0
  76. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/notifications/base.py +0 -0
  77. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/notifications/protocol.py +0 -0
  78. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm/py.typed +0 -0
  79. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  80. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
  81. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  82. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/__init__.py +0 -0
  83. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_alarm_phones.py +0 -0
  84. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_base_commands.py +0 -0
  85. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_config.py +0 -0
  86. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_devices.py +0 -0
  87. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_discovery.py +0 -0
  88. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_history.py +0 -0
  89. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_host_config.py +0 -0
  90. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_net_config.py +0 -0
  91. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/test_paginated_commands.py +0 -0
  92. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/dataclass/test_dataclass_load_save.py +0 -0
  93. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/dataclass/test_dataclass_load_save_descriptor.py +0 -0
  94. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/dataclass/test_dataclass_load_save_serialize.py +0 -0
  95. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/dataclass/test_validation.py +0 -0
  96. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/entities/test_base_list.py +0 -0
  97. {pyg90alarm-2.6.0 → pyg90alarm-2.7.1}/tests/unit/test_exceptions.py +0 -0
  98. {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: github.event_name != 'release'
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.6.0
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 (not customizable at panel side). To process the cloud notifications all the traffic from panel towards the cloud (IP address ``47.88.7.61`` and TCP port ``5678`` as of writing) needs to be diverted to the node where the package is running - depending on your network equipment it could be port forwarding, DNAT or other means. It is unclear whether the panel utilizes DNS to resolve the cloud service IP address, hence the documentation only mentions IP-based traffic redirection.
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
- # Optional, see note above redirecting cloud traffic from panel
238
- local_port=5678,
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
- # Optional, see note above redirecting cloud traffic from panel
256
- local_port=5678,
257
- # See note above re: cloud service and sending packets to it
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 (not customizable at panel side). To process the cloud notifications all the traffic from panel towards the cloud (IP address ``47.88.7.61`` and TCP port ``5678`` as of writing) needs to be diverted to the node where the package is running - depending on your network equipment it could be port forwarding, DNAT or other means. It is unclear whether the panel utilizes DNS to resolve the cloud service IP address, hence the documentation only mentions IP-based traffic redirection.
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
- # Optional, see note above redirecting cloud traffic from panel
191
- local_port=5678,
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
- # Optional, see note above redirecting cloud traffic from panel
209
- local_port=5678,
210
- # See note above re: cloud service and sending packets to it
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, REMOTE_PORT,
63
+ G90Commands, G90SystemCommands,
64
+ REMOTE_PORT,
64
65
  REMOTE_TARGETED_DISCOVERY_PORT,
65
66
  LOCAL_TARGETED_DISCOVERY_PORT,
66
- LOCAL_NOTIFICATIONS_HOST,
67
+ LOCAL_NOTIFICATIONS_IP,
67
68
  LOCAL_NOTIFICATIONS_PORT,
68
- CLOUD_NOTIFICATIONS_HOST,
69
- CLOUD_NOTIFICATIONS_PORT,
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 (G90BaseCommand, G90BaseCommandData)
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[G90BaseCommandData] = None
267
- ) -> G90BaseCommandData:
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: G90BaseCommand = await G90BaseCommand(
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, notifications_local_host: str = LOCAL_NOTIFICATIONS_HOST,
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
- local_host=notifications_local_host,
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, cloud_local_host: str = CLOUD_NOTIFICATIONS_HOST,
1325
- cloud_local_port: int = CLOUD_NOTIFICATIONS_PORT,
1326
- upstream_host: Optional[str] = REMOTE_CLOUD_HOST,
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
- local_host=cloud_local_host,
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, REMOTE_CLOUD_HOST, REMOTE_CLOUD_PORT,
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
- # Simulated cloud response always contains known IP address of the vendor's
216
- # cloud service - that is, all interactions between alarm panel and
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 = REMOTE_CLOUD_PORT
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.cloud_host.encode()
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 local_host: Local host to bind the server to
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
- local_host: str, local_port: int,
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._local_host = local_host
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
- local_host=self._local_host,
169
+ local_ip=self._local_ip,
166
170
  local_port=self._local_port,
167
- cloud_host=REMOTE_CLOUD_HOST,
168
- cloud_port=REMOTE_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._local_host,
378
+ self._local_ip,
375
379
  self._local_port)
376
380
  self._server = await loop.create_server(
377
381
  lambda: self,
378
- self._local_host, self._local_port
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._local_host, self._local_port
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
- local_host: str
76
+ local_ip: str
77
77
  local_port: int
78
78
  remote_host: str
79
79
  remote_port: int
80
- cloud_host: str
80
+ cloud_ip: str
81
81
  cloud_port: int
82
82
  upstream_host: Optional[str]
83
83
  upstream_port: Optional[int]