pyg90alarm 1.12.1__py3-none-any.whl → 1.14.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.
pyg90alarm/base_cmd.py CHANGED
@@ -21,43 +21,46 @@
21
21
  """
22
22
  Provides support for basic commands of G90 alarm panel.
23
23
  """
24
-
24
+ from __future__ import annotations
25
25
  import logging
26
26
  import json
27
27
  import asyncio
28
- from typing import NamedTuple, Optional
28
+ from asyncio import Future
29
+ from asyncio.protocols import DatagramProtocol
30
+ from asyncio.transports import DatagramTransport, BaseTransport
31
+ from typing import Optional, Tuple, List, Any
32
+ from dataclasses import dataclass
29
33
  from .exceptions import (G90Error, G90TimeoutError)
34
+ from .const import G90Commands
30
35
 
31
36
 
32
37
  _LOGGER = logging.getLogger(__name__)
38
+ G90BaseCommandData = List[Any]
33
39
 
34
40
 
35
- class G90Header(NamedTuple):
41
+ @dataclass
42
+ class G90Header:
36
43
  """
37
44
  Represents JSON structure of the header used in alarm panel commands.
38
- Note that typing.NamedTuple is used (instead of collections.namedtuple) to
39
- provide support for Python 3.6 and higher, while providing default values.
40
45
 
41
46
  :meta private:
42
47
  """
43
48
  code: Optional[int] = None
44
- data: Optional[str] = None
49
+ data: Optional[G90BaseCommandData] = None
45
50
 
46
51
 
47
- class G90BaseCommand:
52
+ class G90BaseCommand(DatagramProtocol):
48
53
  """
49
- tbd
54
+ Implements basic command handling for alarm panel protocol.
50
55
  """
51
56
  # pylint: disable=too-many-instance-attributes
52
57
  # Lock need to be shared across all of the class instances
53
58
  _sk_lock = asyncio.Lock()
54
59
 
55
- def __init__(self, host, port, code,
56
- data=None, local_port=None,
57
- timeout=3.0, retries=3):
58
- """
59
- tbd
60
- """
60
+ def __init__(self, host: str, port: int, code: G90Commands,
61
+ data: Optional[G90BaseCommandData] = None,
62
+ local_port: Optional[int] = None,
63
+ timeout: float = 3.0, retries: int = 3) -> None:
61
64
  # pylint: disable=too-many-arguments
62
65
  self._remote_host = host
63
66
  self._remote_port = port
@@ -66,8 +69,10 @@ class G90BaseCommand:
66
69
  self._timeout = timeout
67
70
  self._retries = retries
68
71
  self._data = '""'
69
- self._result = None
70
- self._connection_result = None
72
+ self._result: G90BaseCommandData = []
73
+ self._connection_result: Optional[
74
+ Future[Tuple[str, int, bytes]]
75
+ ] = None
71
76
  if data:
72
77
  self._data = json.dumps([code, data],
73
78
  # No newlines to be inserted
@@ -78,19 +83,19 @@ class G90BaseCommand:
78
83
 
79
84
  # Implementation of datagram protocol,
80
85
  # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
81
- def connection_made(self, transport):
86
+ def connection_made(self, transport: BaseTransport) -> None:
82
87
  """
83
- tbd
88
+ Invoked when connection is established.
84
89
  """
85
90
 
86
- def connection_lost(self, exc):
91
+ def connection_lost(self, exc: Optional[Exception]) -> None:
87
92
  """
88
- tbd
93
+ Invoked when connection is lost.
89
94
  """
90
95
 
91
- def datagram_received(self, data, addr):
96
+ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
92
97
  """
93
- tbd
98
+ Invoked when datagram is received.
94
99
  """
95
100
  if asyncio.isfuture(self._connection_result):
96
101
  if self._connection_result.done():
@@ -100,9 +105,9 @@ class G90BaseCommand:
100
105
  return
101
106
  self._connection_result.set_result((*addr, data))
102
107
 
103
- def error_received(self, exc):
108
+ def error_received(self, exc: Exception) -> None:
104
109
  """
105
- tbd
110
+ Invoked when error is received.
106
111
  """
107
112
  if (
108
113
  asyncio.isfuture(self._connection_result) and not
@@ -110,9 +115,11 @@ class G90BaseCommand:
110
115
  ):
111
116
  self._connection_result.set_exception(exc)
112
117
 
113
- async def _create_connection(self):
118
+ async def _create_connection(self) -> (
119
+ Tuple[DatagramTransport, DatagramProtocol]
120
+ ):
114
121
  """
115
- tbd
122
+ Creates UDP connection to the alarm panel.
116
123
  """
117
124
  try:
118
125
  loop = asyncio.get_running_loop()
@@ -121,38 +128,38 @@ class G90BaseCommand:
121
128
 
122
129
  _LOGGER.debug('Creating UDP endpoint for %s:%s',
123
130
  self.host, self.port)
124
- extra_kwargs = {}
131
+ local_addr = None
125
132
  if self._local_port:
126
- extra_kwargs['local_addr'] = ('0.0.0.0', self._local_port)
133
+ local_addr = ('0.0.0.0', self._local_port)
127
134
 
128
135
  transport, protocol = await loop.create_datagram_endpoint(
129
136
  lambda: self,
130
137
  remote_addr=(self.host, self.port),
131
- **extra_kwargs,
132
- allow_broadcast=True)
138
+ allow_broadcast=True,
139
+ local_addr=local_addr)
133
140
 
134
- return transport, protocol
141
+ return (transport, protocol)
135
142
 
136
- def to_wire(self):
143
+ def to_wire(self) -> bytes:
137
144
  """
138
- tbd
145
+ Returns the command in wire format.
139
146
  """
140
147
  wire = bytes(f'ISTART[{self._code},{self._code},{self._data}]IEND\0',
141
148
  'utf-8')
142
149
  _LOGGER.debug('Encoded to wire format %s', wire)
143
150
  return wire
144
151
 
145
- def from_wire(self, data):
152
+ def from_wire(self, data: bytes) -> G90BaseCommandData:
146
153
  """
147
- tbd
154
+ Parses the response from the alarm panel.
148
155
  """
149
156
  _LOGGER.debug('To be decoded from wire format %s', data)
150
157
  self._parse(data.decode('utf-8', errors='ignore'))
151
- return self._resp.data
158
+ return self._resp.data or []
152
159
 
153
- def _parse(self, data):
160
+ def _parse(self, data: str) -> None:
154
161
  """
155
- tbd
162
+ Processes the response from the alarm panel.
156
163
  """
157
164
  if not data.startswith('ISTART'):
158
165
  raise G90Error('Missing start marker in data')
@@ -189,39 +196,40 @@ class G90BaseCommand:
189
196
  f"{self._resp.code}, expected code {self._code}")
190
197
 
191
198
  @property
192
- def result(self):
199
+ def result(self) -> G90BaseCommandData:
193
200
  """
194
- tbd
201
+ The result of the command.
195
202
  """
196
203
  return self._result
197
204
 
198
205
  @property
199
- def host(self):
206
+ def host(self) -> str:
200
207
  """
201
- tbd
208
+ The hostname/IP address of the alarm panel.
202
209
  """
203
210
  return self._remote_host
204
211
 
205
212
  @property
206
- def port(self):
213
+ def port(self) -> int:
207
214
  """
208
- tbd
215
+ The port of the alarm panel.
209
216
  """
210
217
  return self._remote_port
211
218
 
212
- async def process(self):
219
+ async def process(self) -> G90BaseCommand:
213
220
  """
214
- tbd
221
+ Processes the command.
215
222
  """
223
+ # Disallow using `NONE` command, which is intended to use by inheriting
224
+ # classes overriding `process()` method
225
+ if self._code == G90Commands.NONE:
226
+ raise G90Error("'NONE' command code is disallowed")
216
227
 
217
228
  transport, _ = await self._create_connection()
218
229
  attempts = self._retries
219
230
  while True:
220
231
  attempts = attempts - 1
221
- try:
222
- loop = asyncio.get_running_loop()
223
- except AttributeError:
224
- loop = asyncio.get_event_loop()
232
+ loop = asyncio.get_running_loop()
225
233
  self._connection_result = loop.create_future()
226
234
  async with self._sk_lock:
227
235
  _LOGGER.debug('(code %s) Sending request to %s:%s',
@@ -253,9 +261,9 @@ class G90BaseCommand:
253
261
  self._result = ret
254
262
  return self
255
263
 
256
- def __repr__(self):
264
+ def __repr__(self) -> str:
257
265
  """
258
- tbd
266
+ Returns string representation of the command.
259
267
  """
260
268
  return f'Command: {self._code}, request: {self._data},' \
261
269
  f' response: {self._resp.data}'
pyg90alarm/callback.py CHANGED
@@ -21,22 +21,33 @@
21
21
  """
22
22
  Implements callbacks.
23
23
  """
24
-
24
+ from __future__ import annotations
25
25
  import asyncio
26
26
  from functools import (partial, wraps)
27
+ from asyncio import Task
28
+ from typing import Any, Callable, Coroutine, cast, Optional, Union
27
29
  import logging
28
30
 
29
31
  _LOGGER = logging.getLogger(__name__)
30
32
 
33
+ Callback = Optional[
34
+ Union[
35
+ Callable[..., None],
36
+ Callable[..., Coroutine[None, None, None]],
37
+ ]
38
+ ]
39
+
31
40
 
32
41
  class G90Callback:
33
42
  """
34
- tbd
43
+ Implements callbacks.
35
44
  """
36
45
  @staticmethod
37
- def invoke(callback, *args, **kwargs):
46
+ def invoke(
47
+ callback: Callback, *args: Any, **kwargs: Any
48
+ ) -> None:
38
49
  """
39
- tbd
50
+ Invokes the callback.
40
51
  """
41
52
  if not callback:
42
53
  return
@@ -46,73 +57,52 @@ class G90Callback:
46
57
  callback, args, kwargs)
47
58
 
48
59
  if not asyncio.iscoroutinefunction(callback):
49
- def async_wrapper(func):
60
+ def async_wrapper(
61
+ func: Callable[..., None]
62
+ ) -> Callable[..., Coroutine[Any, Any, None]]:
50
63
  """
51
64
  Wraps the regular callback function into coroutine, so it could
52
65
  later be created as async task.
53
66
  """
54
67
  @wraps(func)
55
- async def wrapper(*args, **kwds):
68
+ async def wrapper(
69
+ *args: Any, **kwds: Any
70
+ ) -> None:
56
71
  return func(*args, **kwds)
57
- return wrapper
58
72
 
59
- callback = async_wrapper(callback)
73
+ return cast(Callable[..., Coroutine[Any, Any, None]], wrapper)
60
74
 
61
- if hasattr(asyncio, 'create_task'):
62
- task = asyncio.create_task(callback(*args, **kwargs))
75
+ task = asyncio.create_task(
76
+ async_wrapper(
77
+ cast(Callable[..., None], callback)
78
+ )(*args, **kwargs)
79
+ )
63
80
  else:
64
- # Python 3.6 has only `ensure_future` method
65
- task = asyncio.ensure_future(callback(*args, **kwargs))
81
+ task = asyncio.create_task(callback(*args, **kwargs))
66
82
 
67
- def reap_callback_exception(task):
83
+ def reap_callback_exception(task: Task[Any]) -> None:
68
84
  """
69
85
  Reaps an exception (if any) from the task logging it, to prevent
70
86
  `asyncio` reporting that task exception was never retrieved.
71
87
  """
72
88
  exc = task.exception()
73
89
  if exc:
74
- _LOGGER.error('Got exception when invoking'
75
- " callback '%s(...)':",
76
- task.get_coro().__qualname__,
77
- exc_info=exc, stack_info=False)
90
+ _LOGGER.error(
91
+ "Got exception when invoking callback '%s(...)':",
92
+ cast(
93
+ Coroutine[Any, Any, None], task.get_coro()
94
+ ).__qualname__,
95
+ exc_info=exc, stack_info=False
96
+ )
78
97
 
79
98
  task.add_done_callback(reap_callback_exception)
80
99
 
81
100
  @staticmethod
82
- def invoke_delayed(delay, callback, *args, **kwargs):
101
+ def invoke_delayed(
102
+ delay: float, callback: Callable[..., None], *args: Any, **kwargs: Any
103
+ ) -> None:
83
104
  """
84
- tbd
105
+ Invokes the callback after a delay.
85
106
  """
86
- if hasattr(asyncio, 'get_running_loop'):
87
- loop = asyncio.get_running_loop()
88
- else:
89
- # Python 3.6 has no `get_running_loop`, only `get_event_loop`
90
- loop = asyncio.get_event_loop()
107
+ loop = asyncio.get_running_loop()
91
108
  loop.call_later(delay, partial(callback, *args, **kwargs))
92
-
93
-
94
- def async_as_sync(func):
95
- """
96
- Invokes an async function as regular one via :py:func:`G90Callback.invoke`.
97
- One of possible use cases is implementing property setter for async code,
98
- where the function could be used an decorator:
99
-
100
- .. code-block:: python
101
-
102
- @property
103
- async def a_property(...):
104
- ...
105
-
106
- @a_property.setter
107
- @async_as_sync
108
- async def a_property(...):
109
- ...
110
-
111
- Since the function internally submits the wrapped async code as
112
- :py:mod:`asyncio` task, it execution isn't guaranteed as the program could
113
- be terminated earlier that it is processed in the loop.
114
- """
115
- @wraps(func)
116
- def wrapper(*args, **kwargs):
117
- return G90Callback.invoke(func, *args, **kwargs)
118
- return wrapper
pyg90alarm/config.py CHANGED
@@ -21,12 +21,15 @@
21
21
  """
22
22
  Represents various configuration aspects of the alarm panel.
23
23
  """
24
+ from __future__ import annotations
24
25
  from enum import IntFlag
25
- from collections import namedtuple
26
+ from dataclasses import dataclass
26
27
 
27
28
 
28
29
  class G90AlertConfigFlags(IntFlag):
29
- """ Alert configuration flags, used bitwise """
30
+ """
31
+ Alert configuration flags, used bitwise
32
+ """
30
33
  AC_POWER_FAILURE = 1
31
34
  AC_POWER_RECOVER = 2
32
35
  ARM_DISARM = 4
@@ -41,15 +44,16 @@ class G90AlertConfigFlags(IntFlag):
41
44
  UNKNOWN2 = 8192
42
45
 
43
46
 
44
- class G90AlertConfig(namedtuple('G90AlertConfig', ['flags_data'])):
47
+ @dataclass
48
+ class G90AlertConfig:
45
49
  """
46
50
  Represents alert configuration as received from the alarm panel.
47
51
  """
52
+ flags_data: int
48
53
 
49
54
  @property
50
- def flags(self):
55
+ def flags(self) -> G90AlertConfigFlags:
51
56
  """
52
- :return: Instance of :class:`G90AlertConfigFlags` that provides
53
- symbolic names to corresponding flag bits
57
+ :return: Symbolic names for corresponding flag bits
54
58
  """
55
59
  return G90AlertConfigFlags(self.flags_data)
pyg90alarm/const.py CHANGED
@@ -21,8 +21,9 @@
21
21
  """
22
22
  Definies different constants for G90 alarm panel.
23
23
  """
24
-
24
+ from __future__ import annotations
25
25
  from enum import IntEnum
26
+ from typing import Optional
26
27
 
27
28
  REMOTE_PORT = 12368
28
29
  REMOTE_TARGETED_DISCOVERY_PORT = 12900
@@ -40,7 +41,7 @@ class G90Commands(IntEnum):
40
41
  comprehensive or complete.
41
42
  """
42
43
 
43
- def __new__(cls, value, doc=None):
44
+ def __new__(cls, value: int, doc: Optional[str] = None) -> G90Commands:
44
45
  """
45
46
  Allows to set the docstring along with the value to enum entry.
46
47
  """
@@ -49,6 +50,12 @@ class G90Commands(IntEnum):
49
50
  obj.__doc__ = doc
50
51
  return obj
51
52
 
53
+ NONE = (0, """
54
+ Pseudo command, to be used for proper typing with subclasses of
55
+ `G90BaseCommand` invoking its constructor but implementing special
56
+ processing
57
+ """)
58
+
52
59
  # Host status
53
60
  GETHOSTSTATUS = (100, 'Get host status')
54
61
  SETHOSTSTATUS = (101, 'Set host status')
@@ -22,7 +22,7 @@
22
22
  Sensor definitions for G90 devices, required when modifying them since writing
23
23
  a sensor to the device requires values not present on read.
24
24
  """
25
- from collections import namedtuple
25
+ from typing import NamedTuple
26
26
  from enum import IntEnum
27
27
 
28
28
 
@@ -44,21 +44,22 @@ class SensorRwMode(IntEnum):
44
44
  READ_WRITE = 2
45
45
 
46
46
 
47
- class SensorDefinition(
48
- namedtuple(
49
- 'SensorDefinition', [
50
- 'type', 'subtype', 'rx', 'tx',
51
- 'private_data', 'rwMode', 'matchMode',
52
- ]
53
- )
54
- ):
47
+ class SensorDefinition(NamedTuple):
55
48
  """
56
49
  Holds sensor definition data.
57
50
  """
51
+ type: int
52
+ subtype: int
53
+ rx: int
54
+ tx: int
55
+ private_data: str
56
+ rwMode: SensorRwMode
57
+ matchMode: SensorMatchMode
58
+
58
59
  @property
59
- def reserved_data(self):
60
+ def reserved_data(self) -> int:
60
61
  """
61
- Returns sensor 'reserved_data' field to be written, combined of match
62
+ Sensor's 'reserved_data' field to be written, combined of match
62
63
  and RW mode values bitwise.
63
64
  """
64
65
  return self.matchMode.value << 4 | self.rwMode.value