pyg90alarm 2.3.0__py3-none-any.whl

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 (42) hide show
  1. pyg90alarm/__init__.py +84 -0
  2. pyg90alarm/alarm.py +1274 -0
  3. pyg90alarm/callback.py +146 -0
  4. pyg90alarm/cloud/__init__.py +31 -0
  5. pyg90alarm/cloud/const.py +56 -0
  6. pyg90alarm/cloud/messages.py +593 -0
  7. pyg90alarm/cloud/notifications.py +410 -0
  8. pyg90alarm/cloud/protocol.py +518 -0
  9. pyg90alarm/const.py +273 -0
  10. pyg90alarm/definitions/__init__.py +3 -0
  11. pyg90alarm/definitions/base.py +247 -0
  12. pyg90alarm/definitions/devices.py +366 -0
  13. pyg90alarm/definitions/sensors.py +843 -0
  14. pyg90alarm/entities/__init__.py +3 -0
  15. pyg90alarm/entities/base_entity.py +93 -0
  16. pyg90alarm/entities/base_list.py +268 -0
  17. pyg90alarm/entities/device.py +97 -0
  18. pyg90alarm/entities/device_list.py +156 -0
  19. pyg90alarm/entities/sensor.py +891 -0
  20. pyg90alarm/entities/sensor_list.py +183 -0
  21. pyg90alarm/exceptions.py +63 -0
  22. pyg90alarm/local/__init__.py +0 -0
  23. pyg90alarm/local/base_cmd.py +293 -0
  24. pyg90alarm/local/config.py +157 -0
  25. pyg90alarm/local/discovery.py +103 -0
  26. pyg90alarm/local/history.py +272 -0
  27. pyg90alarm/local/host_info.py +89 -0
  28. pyg90alarm/local/host_status.py +52 -0
  29. pyg90alarm/local/notifications.py +117 -0
  30. pyg90alarm/local/paginated_cmd.py +132 -0
  31. pyg90alarm/local/paginated_result.py +135 -0
  32. pyg90alarm/local/targeted_discovery.py +162 -0
  33. pyg90alarm/local/user_data_crc.py +46 -0
  34. pyg90alarm/notifications/__init__.py +0 -0
  35. pyg90alarm/notifications/base.py +481 -0
  36. pyg90alarm/notifications/protocol.py +127 -0
  37. pyg90alarm/py.typed +0 -0
  38. pyg90alarm-2.3.0.dist-info/METADATA +277 -0
  39. pyg90alarm-2.3.0.dist-info/RECORD +42 -0
  40. pyg90alarm-2.3.0.dist-info/WHEEL +5 -0
  41. pyg90alarm-2.3.0.dist-info/licenses/LICENSE +21 -0
  42. pyg90alarm-2.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyg90alarm
3
+ Version: 2.3.0
4
+ Summary: G90 Alarm system protocol
5
+ Home-page: https://github.com/hostcc/pyg90alarm
6
+ Author: Ilia Sotnikov
7
+ Author-email: hostcc@gmail.com
8
+ Project-URL: Bug Reports, https://github.com/hostcc/pyg90alarm/issues
9
+ Project-URL: Source, https://github.com/hostcc/pyg90alarm/
10
+ Keywords: g90,alarm,protocol
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Topic :: Home Automation
15
+ Classifier: Topic :: System :: Hardware
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3 :: Only
24
+ Requires-Python: >=3.9, <4
25
+ Description-Content-Type: text/x-rst
26
+ License-File: LICENSE
27
+ Provides-Extra: dev
28
+ Requires-Dist: check-manifest; extra == "dev"
29
+ Provides-Extra: test
30
+ Requires-Dist: coverage; extra == "test"
31
+ Requires-Dist: asynctest; extra == "test"
32
+ Provides-Extra: docs
33
+ Requires-Dist: sphinx; extra == "docs"
34
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
35
+ Dynamic: author
36
+ Dynamic: author-email
37
+ Dynamic: classifier
38
+ Dynamic: description
39
+ Dynamic: description-content-type
40
+ Dynamic: home-page
41
+ Dynamic: keywords
42
+ Dynamic: license-file
43
+ Dynamic: project-url
44
+ Dynamic: provides-extra
45
+ Dynamic: requires-python
46
+ Dynamic: summary
47
+
48
+ .. image:: https://github.com/hostcc/pyg90alarm/actions/workflows/main.yml/badge.svg?branch=master
49
+ :target: https://github.com/hostcc/pyg90alarm/tree/master
50
+ :alt: Github workflow status
51
+ .. image:: https://readthedocs.org/projects/pyg90alarm/badge/?version=stable
52
+ :target: https://pyg90alarm.readthedocs.io/en/stable
53
+ :alt: ReadTheDocs status
54
+ .. image:: https://img.shields.io/github/v/release/hostcc/pyg90alarm
55
+ :target: https://github.com/hostcc/pyg90alarm/releases/latest
56
+ :alt: Latest GitHub release
57
+ .. image:: https://img.shields.io/pypi/v/pyg90alarm
58
+ :target: https://pypi.org/project/pyg90alarm/
59
+ :alt: Latest PyPI version
60
+
61
+ Description
62
+ ===========
63
+
64
+ Python package to control G90-based alarm systems.
65
+
66
+ Many manufacturers sell such systems under different brands - Golden Security,
67
+ PST, Kerui and others. Those are cheap low-end systems, typically equipped with
68
+ WiFi and possible GSM interfaces for connectivity, and support different range
69
+ of peripherals:
70
+
71
+ * Wired and wireless sensors
72
+ * Relays (switches)
73
+
74
+ ... and probably others
75
+
76
+ The package implements asynchronous I/O over most of code paths using
77
+ `asyncio <https://docs.python.org/3/library/asyncio.html>`_.
78
+
79
+ Disclaimer
80
+ ==========
81
+
82
+ The author has no affiliation or any relationship to any of the hardware
83
+ vendors in question. The code has been created upon many trial and error
84
+ iterations.
85
+
86
+ Motivation
87
+ ==========
88
+
89
+ The primary motivation creating the code is the comfort of using the security
90
+ system - the mobile applications provided by the vendor, called "Carener", is
91
+ slow and crashes sometimes. Instead, it would be awesome to have the system
92
+ integrated into larger ecosystems, like Home Assistant, HomeKit and such.
93
+ Hence, the code has been created to interact with the security system using
94
+ Python, and it opens up a way for further integrations.
95
+
96
+ Supported hardware
97
+ ==================
98
+
99
+ It might not be possible to list every system supported by the package due to
100
+ manufacturers naming the products differently. Here is the list of hardware
101
+ known to work with the package:
102
+
103
+ * `PST G90B Plus <http://www.cameralarms.com/products/auto_dial_alarm_system/185.html>`_
104
+
105
+ And the list of sensors, actual set of device should be notable larger as many
106
+ of other manufacturers produce similar items. The names in parenthesis are
107
+ taken from the alarm system documentation, for example, `Home Alarm GB90-Plus <https://archive.org/details/HomeAlarmGB90-Plus/G90B%20plus%20WIFIGSMGPRS%20alarm%20system%20user%20manual/page/n7/mode/2up>`_.
108
+
109
+ * Wired PIR sensors
110
+ * Wireless PIR sensors (WPD01, WMS08)
111
+ * Door/window sensors (WDS07, WRDS01)
112
+ * Water leak sensors (LSTC01)
113
+ * Smoke sensors (WSD02)
114
+ * Gas sensors (WGD01)
115
+ * Switches/relays (JDQ)
116
+
117
+ Basically, the alarm system uses 433 MHz communications for the wireless
118
+ devices using EV1527, PT2262 protocols. The mobile application also mentions
119
+ some devices using 2.4GHz, although details of the protocols haven't been
120
+ identified as no such hardware has been available for experimentation.
121
+
122
+ Known caveats
123
+ =============
124
+
125
+ * Wireless shutter sensor (WRDS01) doesn't send anything on sensor closed, only
126
+ when opened. In contrast, WDS07 wireless door sensor does both.
127
+ * Wireless relays (at least JDQ) use same RF code for switching on and off,
128
+ when configured in toggle mode. That means a RF signal repeater will make
129
+ controlling such relays unpredictable, since the code will be sent more than
130
+ once.
131
+ * Low battery notifications for wireless sensors (at least for WDS07 and WSD02)
132
+ are often missing, either due to the sensors not sending them or the device
133
+ doesn't receive those.
134
+ * Wired sensors toggle on line state change, i.e. those aren't limited to have
135
+ normal closed (NC) or normal open (NO) contacts only. Best used with NC
136
+ contact sensors though, since an intruder cutting the line will trigger the
137
+ alarm.
138
+
139
+ Device notifications
140
+ ====================
141
+
142
+ Local notifications
143
+ -------------------
144
+
145
+ There is a hidden device capability to send protocol notifications over the
146
+ WiFi interface, thus called local. The notifications are done using broadcast UDP packets with source/destination ports being ``45000:12901`` (non-configurable), and sent when the device has IP address of its WiFi interface set to ``10.10.10.250``. That is the same IP the device will allocate to the WiFi interface when AP (access point is enabled). Please note enabling the AP *is not* required for the notifications to be sent, only the IP address matters. Likely the firmware does a check internally and enables those when corresponding IP address is found on the WiFi interface.
147
+
148
+ Depending on your network setup, ensuring the `10.10.10.250` IP address is
149
+ allocated to the WiFi interface of the device might be as simple as DHCP
150
+ reservation. Please check the documentation of your networking gear on how to
151
+ set the IP address allocation up.
152
+
153
+ .. note:: Since the IP address trick above isn't something the device exposes
154
+ to the user, the functionality might change or even cease functioning upon a
155
+ firmware upgrade!
156
+
157
+ .. note:: The device notifications in question are fully local with no
158
+ dependency on the cloud or Internet connection on the device.
159
+
160
+ .. note:: If IP address trick doesn't work for you by a reason, the package
161
+ will still be able to perform the key functions - for example, arming or
162
+ disarming the panel, or reading the list of sensors. However, the sensor
163
+ status will not be reflected and those will always be reported as inactive,
164
+ since there is no way to read their state in a polled manner.
165
+
166
+ To work that limitation around the package now supports simulating device
167
+ notifications from periodically polling the history it records - the
168
+ simulation works only for the alerts, not notifications (e.g. notifications
169
+ include low battery events and alike). This also requires the particular
170
+ alert to be enabled in the mobile application, otherwise it won't be
171
+ recorded in the history.
172
+
173
+ For the local notifications to be enabled the ``G90Alarm.use_local_notifications()`` needs to be called upon constructing an instance of ``G90Alarm`` class, then ``G90Alarm.listen_notifications()`` to start processing those coming from the panel - the code fragment below demonstrates that though being incomplete since callbacks (e.g. ``G90Alarm.on_armdisarm()``) should be set for the actual processing of the notifications.
174
+
175
+ .. code:: python
176
+
177
+ from pyg90alarm import G90Alarm
178
+
179
+ # Create an instance of the alarm panel
180
+ alarm = G90Alarm(host='10.10.10.250')
181
+ # Enable local notifications
182
+ await alarm.use_local_notifications()
183
+ # Start listening for notifications
184
+ await alarm.listen_notifications()
185
+
186
+ Cloud notifications
187
+ -------------------
188
+
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
+ 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
+
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.
193
+
194
+ Please see
195
+ `the section <docs/cloud-protocol.rst>`_ for further details on the protocol.
196
+
197
+ The benefit of the cloud notifications is that the panel no longer required to have ``10.10.10.250`` IP address.
198
+
199
+ The package could act as:
200
+
201
+ - Standalone cloud server with no Internet connectivity or cloud service
202
+ required at all - good if you'd like to avoid having a vendor service involved. Please note the mobile application will show panel as offline in this mode
203
+ - Chained cloud server, where in addition to interpreting the notifications it
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
+
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
+ 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
+
227
+ **Standalone mode**
228
+
229
+ .. code:: python
230
+
231
+ from pyg90alarm import G90Alarm
232
+
233
+ # Create an instance of the alarm panel
234
+ alarm = G90Alarm(host='<panel IP address>')
235
+ # Enable cloud notifications
236
+ await alarm.use_cloud_notifications(
237
+ # Optional, see note above redirecting cloud traffic from panel
238
+ local_port=5678,
239
+ upstream_host=None
240
+ )
241
+ # Start listening for notifications
242
+ await alarm.listen_notifications()
243
+
244
+
245
+ **Chained mode**
246
+
247
+ .. code:: python
248
+
249
+ from pyg90alarm import G90Alarm
250
+
251
+ # Create an instance of the alarm panel
252
+ alarm = G90Alarm(host='<panel IP address>')
253
+ # Enable cloud notifications
254
+ 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
258
+ upstream_host='47.88.7.61',
259
+ upstream_port=5678
260
+ )
261
+ # Start listening for notifications
262
+ await alarm.listen_notifications()
263
+
264
+
265
+ Quick start
266
+ ===========
267
+
268
+ .. code:: shell
269
+
270
+ pip install pyg90alarm
271
+
272
+ Documentation
273
+ =============
274
+
275
+ Please see `online documentation <https://pyg90alarm.readthedocs.io>`_ for
276
+ details on the protocol, its security, supported commands and the API package
277
+ provides.
@@ -0,0 +1,42 @@
1
+ pyg90alarm/__init__.py,sha256=9RyHo4m46owUxAdPMFOkrr0VSd1lXK9QWyXMQlia7tM,3018
2
+ pyg90alarm/alarm.py,sha256=Y80AsrC5-s4xfsCnFcVpNPKbIWBZ8yDo_bTCsthL4iM,47033
3
+ pyg90alarm/callback.py,sha256=9PVtjRs2MLn80AgiM-UJNL8ZJF4_PxcopJIpxMmB3vc,4707
4
+ pyg90alarm/const.py,sha256=w2BzsVRDxfdwOb9590zsntGMlVWEH80zbQ4djojft9U,6851
5
+ pyg90alarm/exceptions.py,sha256=9LleQC2SkJXjv80FlWMeaHs9ZjXxCvZx9UtYdrvejyY,1951
6
+ pyg90alarm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ pyg90alarm/cloud/__init__.py,sha256=u7ZzrIwCsdgmBmgtpCe6c_Kipdo8nG8PEOEcaAHXCxI,1407
8
+ pyg90alarm/cloud/const.py,sha256=FYUxkj0DSI7nnXZHsJMfv8RNQiXKV3TYKGHSdfHdMjg,2113
9
+ pyg90alarm/cloud/messages.py,sha256=L3cpP3IbDRdw3W6FeQxTPXoDlpUv7Fm4t-RkH9Uj4dg,18032
10
+ pyg90alarm/cloud/notifications.py,sha256=0RxCBVcvDuwE0I1m3SLDXDQqCJimDcN4f45gr-Hvt1A,15669
11
+ pyg90alarm/cloud/protocol.py,sha256=82l2IXSM12tv_iWkTrAQZ-aw5UR4tmWFQJKVcgBfIww,16159
12
+ pyg90alarm/definitions/__init__.py,sha256=s0NZnkW_gMH718DJbgez28z9WA231CyszUf1O_ojUiI,68
13
+ pyg90alarm/definitions/base.py,sha256=Q2DQXfFks9u-ke65_5xVTqCUXJmJGxBxFTeWnite1bw,6757
14
+ pyg90alarm/definitions/devices.py,sha256=K0DQnyE-1hlhIGwRfZojKJYMSmlJzmyWcZ_98urixsc,12042
15
+ pyg90alarm/definitions/sensors.py,sha256=bDecBGyUo7wFVNuD5Fu1JNZQHcMDv-234BuNKioaQQs,27426
16
+ pyg90alarm/entities/__init__.py,sha256=hHb6AOiC4Tz--rOWiiICMdLaZDs1Tf_xpWk_HeS_gO4,66
17
+ pyg90alarm/entities/base_entity.py,sha256=hNhhuuwNuir54uVMZzPbjE0N6WL8wKvoW_KZa4R8L8U,2680
18
+ pyg90alarm/entities/base_list.py,sha256=Ot4kd44CUheSvYETjEQBQNT_HaOTWZhKa-4L5pr8LMI,9276
19
+ pyg90alarm/entities/device.py,sha256=eWE_N83hiDs5I-TT5F_W0Vb8sVugLldrDc9Lj9sgVLo,3700
20
+ pyg90alarm/entities/device_list.py,sha256=PKnHEazeT3iUEaz70bW1OaWh0wq-7WOY-7dpV4FVCTc,5984
21
+ pyg90alarm/entities/sensor.py,sha256=hunu-X-RhC8Wp52VMeanfqQg8rBSP8TG0qacJiFZEhg,27653
22
+ pyg90alarm/entities/sensor_list.py,sha256=0S88bhTn91O45WgbIIMQ0iXaNjlUWmMBOOFwj2Hg73U,6993
23
+ pyg90alarm/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ pyg90alarm/local/base_cmd.py,sha256=f0PozHJjErIg08vn0sAacHbigJSFor8q5jdxfXiM27c,10491
25
+ pyg90alarm/local/config.py,sha256=AYDswfeM1aZImoAr6ZB-4fVIvMuTiNgkF_bLIROVckI,4922
26
+ pyg90alarm/local/discovery.py,sha256=8YVIXuNe57lhas0VSRf7QXZH83pEDGNj9oehNY4Kh2U,3576
27
+ pyg90alarm/local/history.py,sha256=v_deRl62tHeysydyeLRvyHJwcBMnKonsEbXXSXb8XP8,9138
28
+ pyg90alarm/local/host_info.py,sha256=4lFIaFEpYd3EvgNrDJmKijTrzX9i29nFISLLlXGnkmE,2759
29
+ pyg90alarm/local/host_status.py,sha256=WHGtw-A0wHILqVWP4DnZhXj8DPRGyS26AV0bL1isTz4,1863
30
+ pyg90alarm/local/notifications.py,sha256=Vs6NQJciYqDALV-WwzH6wIcTGdX_UD4XBuHWjSOpCDY,4591
31
+ pyg90alarm/local/paginated_cmd.py,sha256=5pPVP8f4ydjgu8Yq6MwqINJAUt52fFlD17wO4AI88Pc,4467
32
+ pyg90alarm/local/paginated_result.py,sha256=2tW3kMYkfCoNIhXi0N8acoIAhGhZYIbWdc347CH-Pjg,5474
33
+ pyg90alarm/local/targeted_discovery.py,sha256=Ik2C2VBtVLurf3-RKko4O2R3B6MrmFdOskd457uyASU,5516
34
+ pyg90alarm/local/user_data_crc.py,sha256=JQBOPY3RlOgVtvR55R-rM8OuKjYW-BPXQ0W4pi6CEH0,1689
35
+ pyg90alarm/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ pyg90alarm/notifications/base.py,sha256=_YX0kL9bqMAOdKBRZ5M-SIrwR4xotnZwgxqWoyEgxAk,16032
37
+ pyg90alarm/notifications/protocol.py,sha256=mvn6hVNopOUgwQy4nWHKp7ydt5r84lsDWCCaWYJjL9Q,4422
38
+ pyg90alarm-2.3.0.dist-info/licenses/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
39
+ pyg90alarm-2.3.0.dist-info/METADATA,sha256=bPvzjRYbZ8Cq_pO0pCbD4ensWw0m_HICuo0x4FbeORQ,12568
40
+ pyg90alarm-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
+ pyg90alarm-2.3.0.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
42
+ pyg90alarm-2.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Ilia Sotnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pyg90alarm