pyg90alarm 2.4.1__py3-none-any.whl → 2.5.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/__init__.py +6 -0
- pyg90alarm/const.py +2 -0
- pyg90alarm/dataclass/__init__.py +0 -0
- pyg90alarm/{local/dataclass_load_save.py → dataclass/load_save.py} +125 -5
- pyg90alarm/dataclass/validation.py +572 -0
- pyg90alarm/entities/sensor.py +2 -2
- pyg90alarm/local/alarm_phones.py +39 -12
- pyg90alarm/local/host_config.py +58 -16
- pyg90alarm/local/net_config.py +39 -13
- pyg90alarm/local/paginated_result.py +5 -3
- {pyg90alarm-2.4.1.dist-info → pyg90alarm-2.5.0.dist-info}/METADATA +1 -1
- {pyg90alarm-2.4.1.dist-info → pyg90alarm-2.5.0.dist-info}/RECORD +15 -13
- {pyg90alarm-2.4.1.dist-info → pyg90alarm-2.5.0.dist-info}/WHEEL +0 -0
- {pyg90alarm-2.4.1.dist-info → pyg90alarm-2.5.0.dist-info}/licenses/LICENSE +0 -0
- {pyg90alarm-2.4.1.dist-info → pyg90alarm-2.5.0.dist-info}/top_level.txt +0 -0
pyg90alarm/__init__.py
CHANGED
|
@@ -52,6 +52,10 @@ from .local.host_config import (
|
|
|
52
52
|
from .local.alarm_phones import G90AlarmPhones
|
|
53
53
|
from .local.net_config import G90NetConfig, G90APNAuth
|
|
54
54
|
from .local.history import G90History
|
|
55
|
+
|
|
56
|
+
from .dataclass.validation import (
|
|
57
|
+
get_field_validation_constraints,
|
|
58
|
+
)
|
|
55
59
|
from .const import (
|
|
56
60
|
G90MessageTypes,
|
|
57
61
|
G90NotificationTypes,
|
|
@@ -95,4 +99,6 @@ __all__ = [
|
|
|
95
99
|
'G90AlarmPhones',
|
|
96
100
|
# History
|
|
97
101
|
'G90History',
|
|
102
|
+
# Dataclass validation
|
|
103
|
+
'get_field_validation_constraints',
|
|
98
104
|
]
|
pyg90alarm/const.py
CHANGED
|
File without changes
|
|
@@ -22,16 +22,106 @@
|
|
|
22
22
|
Base class for loading/saving dataclasses to a device.
|
|
23
23
|
"""
|
|
24
24
|
from __future__ import annotations
|
|
25
|
-
from typing import
|
|
25
|
+
from typing import (
|
|
26
|
+
TYPE_CHECKING, Type, TypeVar, Optional, ClassVar, Any, Dict, List, cast
|
|
27
|
+
)
|
|
26
28
|
import logging
|
|
27
|
-
from dataclasses import dataclass,
|
|
29
|
+
from dataclasses import dataclass, asdict, field, fields
|
|
30
|
+
from .validation import ValidatorBase
|
|
28
31
|
from ..const import G90Commands
|
|
29
32
|
if TYPE_CHECKING:
|
|
30
33
|
from ..alarm import G90Alarm
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
_LOGGER = logging.getLogger(__name__)
|
|
34
|
-
|
|
37
|
+
DataclassLoadSaveT = TypeVar('DataclassLoadSaveT', bound='DataclassLoadSave')
|
|
38
|
+
T = TypeVar('T')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Metadata:
|
|
42
|
+
"""
|
|
43
|
+
Metadata keys for DataclassLoadSave fields.
|
|
44
|
+
"""
|
|
45
|
+
# pylint: disable=too-few-public-methods
|
|
46
|
+
NO_SERIALIZE = 'no_serialize'
|
|
47
|
+
SKIP_NONE = 'skip_none'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ReadOnlyIfNotProvided(ValidatorBase[T]):
|
|
51
|
+
"""
|
|
52
|
+
Descriptor for dataclass fields to be read-only if not provided during
|
|
53
|
+
initialization.
|
|
54
|
+
|
|
55
|
+
The field can be read, but attempts to modify it will raise a
|
|
56
|
+
ValueError if the field was not provided during initialization. In
|
|
57
|
+
other words, the only way to set the value is during object creation.
|
|
58
|
+
|
|
59
|
+
Example usage:
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Example:
|
|
63
|
+
read_only_field: Optional[int] = field_readonly_if_not_provided(
|
|
64
|
+
default=None
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Works ok
|
|
68
|
+
ex = Example(read_only_field=42)
|
|
69
|
+
print(ex.read_only_field) # Outputs: 42
|
|
70
|
+
ex.read_only_field = 100 # Works ok
|
|
71
|
+
|
|
72
|
+
# Raises ValueError
|
|
73
|
+
ex2 = Example()
|
|
74
|
+
print(ex2.read_only_field) # Outputs: None
|
|
75
|
+
ex2.read_only_field = 100 # Raises ValueError
|
|
76
|
+
|
|
77
|
+
:param default: Default value to return upon read if not provided during
|
|
78
|
+
initialization.
|
|
79
|
+
"""
|
|
80
|
+
# pylint: disable=too-few-public-methods
|
|
81
|
+
|
|
82
|
+
def __validate__(self, obj: Any, value: T) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Validation method.
|
|
85
|
+
"""
|
|
86
|
+
# Prevent setting the value if it was not provided during
|
|
87
|
+
# initialization. The condition is determined by checking if the
|
|
88
|
+
# current value is `self` - i.e. the descriptor instance hasn't been
|
|
89
|
+
# replaced with an actual value
|
|
90
|
+
if getattr(obj, self.__field_name__, self._default) is self:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f'Field {self.__unmangled_name__} is read-only because'
|
|
93
|
+
' it was not provided during initialization'
|
|
94
|
+
)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def field_readonly_if_not_provided(
|
|
99
|
+
*args: Any, default: Optional[T] = None, **kwargs: Any
|
|
100
|
+
) -> T:
|
|
101
|
+
"""
|
|
102
|
+
Helper function to create a dataclass field with ReadOnlyIfNotProvided
|
|
103
|
+
descriptor.
|
|
104
|
+
|
|
105
|
+
:param args: Positional arguments to pass to `dataclasses.field()`.
|
|
106
|
+
:param default: Default value to return upon read if not provided during
|
|
107
|
+
initialization.
|
|
108
|
+
:param kwargs: Keyword arguments to pass to `dataclasses.field()`.
|
|
109
|
+
:return: A dataclass field with ReadOnlyIfNotProvided descriptor attached.
|
|
110
|
+
"""
|
|
111
|
+
# Also set SKIP_NONE metadata if default is None, so that the field is
|
|
112
|
+
# skipped during serialization when its value is None
|
|
113
|
+
if default is None:
|
|
114
|
+
if 'metadata' not in kwargs:
|
|
115
|
+
kwargs['metadata'] = {}
|
|
116
|
+
kwargs['metadata'][Metadata.SKIP_NONE] = True
|
|
117
|
+
|
|
118
|
+
# Instantiate the field with ReadOnlyIfNotProvided descriptor and rest of
|
|
119
|
+
# the provided arguments
|
|
120
|
+
# pylint: disable=invalid-field-call
|
|
121
|
+
return cast(T, field(
|
|
122
|
+
*args, **kwargs,
|
|
123
|
+
default=ReadOnlyIfNotProvided[T](default)
|
|
124
|
+
))
|
|
35
125
|
|
|
36
126
|
|
|
37
127
|
@dataclass
|
|
@@ -79,6 +169,34 @@ class DataclassLoadSave:
|
|
|
79
169
|
# declared here to avoid being part of dataclass fields
|
|
80
170
|
self._parent: Optional[G90Alarm] = None
|
|
81
171
|
|
|
172
|
+
def serialize(self) -> List[Any]:
|
|
173
|
+
"""
|
|
174
|
+
Returns the dataclass fields as a list.
|
|
175
|
+
|
|
176
|
+
Handles specific metadata for the fields.
|
|
177
|
+
:seealso:`Metadata`.
|
|
178
|
+
|
|
179
|
+
:return: Dataclass serialized as list.
|
|
180
|
+
"""
|
|
181
|
+
result = []
|
|
182
|
+
|
|
183
|
+
for f in fields(self):
|
|
184
|
+
# Skip fields marked with NO_SERIALIZE metadata
|
|
185
|
+
if f.metadata.get(Metadata.NO_SERIALIZE, False):
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# Skip fields with None value if SKIP_NONE metadata is set
|
|
189
|
+
if (
|
|
190
|
+
f.metadata.get(Metadata.SKIP_NONE, False)
|
|
191
|
+
and getattr(self, f.name) is None
|
|
192
|
+
):
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
# Append field value to the result list
|
|
196
|
+
result.append(getattr(self, f.name))
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
|
|
82
200
|
async def save(self) -> None:
|
|
83
201
|
"""
|
|
84
202
|
Save the current data to the device.
|
|
@@ -89,11 +207,13 @@ class DataclassLoadSave:
|
|
|
89
207
|
_LOGGER.debug('Setting data to the device: %s', str(self))
|
|
90
208
|
await self._parent.command(
|
|
91
209
|
self.SAVE_COMMAND,
|
|
92
|
-
|
|
210
|
+
self.serialize()
|
|
93
211
|
)
|
|
94
212
|
|
|
95
213
|
@classmethod
|
|
96
|
-
async def load(
|
|
214
|
+
async def load(
|
|
215
|
+
cls: Type[DataclassLoadSaveT], parent: G90Alarm
|
|
216
|
+
) -> DataclassLoadSaveT:
|
|
97
217
|
"""
|
|
98
218
|
Create an instance with values loaded from the device.
|
|
99
219
|
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
# Copyright (c) 2026 Ilia Sotnikov
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
"""
|
|
21
|
+
Validation descriptors for dataclass fields.
|
|
22
|
+
|
|
23
|
+
This module provides descriptor-based validation for dataclass fields,
|
|
24
|
+
supporting integer range validation and string length validation.
|
|
25
|
+
|
|
26
|
+
Example usage:
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pyg90alarm.dataclass.validation import (
|
|
30
|
+
validated_int_field,
|
|
31
|
+
validated_string_field,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Config:
|
|
36
|
+
# Integer field with range validation
|
|
37
|
+
count: int = validated_int_field(min_value=0, max_value=100)
|
|
38
|
+
|
|
39
|
+
# String field with length validation
|
|
40
|
+
name: str = validated_string_field(min_length=1, max_length=50)
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
from typing import (
|
|
44
|
+
Optional, Any, cast, TYPE_CHECKING, overload, TypeVar, Generic
|
|
45
|
+
)
|
|
46
|
+
import logging
|
|
47
|
+
from dataclasses import dataclass, field, fields, is_dataclass
|
|
48
|
+
|
|
49
|
+
from ..const import BUG_REPORT_URL
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from _typeshed import DataclassInstance
|
|
52
|
+
|
|
53
|
+
_LOGGER = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# pylint: disable=too-few-public-methods
|
|
57
|
+
class ValidationConstraintsAbsent:
|
|
58
|
+
"""
|
|
59
|
+
Sentinel/marker type indicating that no validation constraints are defined.
|
|
60
|
+
|
|
61
|
+
This is used in type annotations and metadata to distinguish the absence
|
|
62
|
+
of constraints from an explicit constraints container.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class IntValidationConstraints:
|
|
68
|
+
"""
|
|
69
|
+
Container for integer validation constraints.
|
|
70
|
+
"""
|
|
71
|
+
min_value: Optional[int] = None
|
|
72
|
+
max_value: Optional[int] = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class StrValidationConstraints:
|
|
77
|
+
"""
|
|
78
|
+
Container for string validation constraints.
|
|
79
|
+
"""
|
|
80
|
+
min_length: Optional[int] = None
|
|
81
|
+
max_length: Optional[int] = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
METADATA_KEY = 'validation_constraints'
|
|
85
|
+
T = TypeVar('T')
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ValidatorBase(Generic[T]):
|
|
89
|
+
"""
|
|
90
|
+
Base dataclass descriptor for validating field values.
|
|
91
|
+
|
|
92
|
+
:param default: Default value to return upon read if not provided during
|
|
93
|
+
initialization.
|
|
94
|
+
:param trust_initial_value: If True, skips validation during dataclass
|
|
95
|
+
initialization assuming init value is valid.
|
|
96
|
+
"""
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
default: Optional[T] = None,
|
|
100
|
+
trust_initial_value: bool = False
|
|
101
|
+
) -> None:
|
|
102
|
+
self._name: Optional[str] = None
|
|
103
|
+
self._default = default
|
|
104
|
+
self._trust_initial_value = trust_initial_value
|
|
105
|
+
|
|
106
|
+
def __validate__(self, obj: Any, value: T) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Validates the value before setting.
|
|
109
|
+
|
|
110
|
+
This method should be overridden by subclasses to implement specific
|
|
111
|
+
validation logic.
|
|
112
|
+
|
|
113
|
+
:param obj: The dataclass instance.
|
|
114
|
+
:param value: The value to validate.
|
|
115
|
+
:raises ValueError: Exception to raise if the value is invalid.
|
|
116
|
+
:return: True if the value is valid, False otherwise. Only returned if
|
|
117
|
+
no exception is raised.
|
|
118
|
+
"""
|
|
119
|
+
raise NotImplementedError(
|
|
120
|
+
'Subclasses must implement __validate__() method'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Stores the name of the attribute this descriptor is assigned to.
|
|
126
|
+
|
|
127
|
+
Note that the name is prepended with an underscore to avoid recursion
|
|
128
|
+
between __get__() and __set__(), since those aren't called for such an
|
|
129
|
+
attribute
|
|
130
|
+
|
|
131
|
+
:param owner: The owner class.
|
|
132
|
+
:param name: The name of the attribute.
|
|
133
|
+
"""
|
|
134
|
+
self._name = "_" + name
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def __unmangled_name__(self) -> str:
|
|
138
|
+
"""
|
|
139
|
+
Returns the unmangled field name.
|
|
140
|
+
|
|
141
|
+
:return: Unmangled field name.
|
|
142
|
+
"""
|
|
143
|
+
return self.__field_name__[1:]
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def __field_name__(self) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Returns the field name the descriptor is assigned to.
|
|
149
|
+
|
|
150
|
+
Note that the name is mangled, see :py:meth:`__set_name__`.
|
|
151
|
+
|
|
152
|
+
:return: Field name.
|
|
153
|
+
"""
|
|
154
|
+
assert self._name is not None, 'Descriptor not initialized properly'
|
|
155
|
+
return self._name
|
|
156
|
+
|
|
157
|
+
def __get__(
|
|
158
|
+
self, obj: Any, objtype: Optional[type] = None
|
|
159
|
+
) -> Optional[T]:
|
|
160
|
+
"""
|
|
161
|
+
Retrieves the field value, returning default if not set.
|
|
162
|
+
|
|
163
|
+
:param obj: The dataclass instance.
|
|
164
|
+
:param objtype: The dataclass type.
|
|
165
|
+
:return: The field value or default if not set.
|
|
166
|
+
"""
|
|
167
|
+
# Dataclass requests the default value for the field
|
|
168
|
+
if obj is None:
|
|
169
|
+
return self._default
|
|
170
|
+
|
|
171
|
+
# Return stored value if it exists, otherwise return default if the
|
|
172
|
+
# value is the descriptor itself, i.e. not set
|
|
173
|
+
value = getattr(obj, self.__field_name__, self._default)
|
|
174
|
+
if value is self:
|
|
175
|
+
_LOGGER.debug(
|
|
176
|
+
"%s: Getting default value '%s'",
|
|
177
|
+
self.__unmangled_name__, self._default
|
|
178
|
+
)
|
|
179
|
+
value = self._default
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
def __set__(self, obj: Any, value: T) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Sets the field value after validating it
|
|
185
|
+
|
|
186
|
+
The validation is skipped if `trust_initial_value` is True and this is
|
|
187
|
+
the first time setting the value during dataclass initialization.
|
|
188
|
+
|
|
189
|
+
:param obj: The dataclass instance.
|
|
190
|
+
:param value: The value to set.
|
|
191
|
+
"""
|
|
192
|
+
# Default value assignment, e.g. when field not provided during
|
|
193
|
+
# initialization thus being assigned the descriptor instance itself
|
|
194
|
+
if value is self and self._default is not None:
|
|
195
|
+
_LOGGER.debug(
|
|
196
|
+
"%s: Assigning default value '%s'",
|
|
197
|
+
self.__unmangled_name__, self._default
|
|
198
|
+
)
|
|
199
|
+
value = self._default
|
|
200
|
+
|
|
201
|
+
# First time setting the value during dataclass initialization and its
|
|
202
|
+
# value should be trusted if `trust_initial_value` is True
|
|
203
|
+
trusted_init_value = (
|
|
204
|
+
not hasattr(obj, self.__field_name__)
|
|
205
|
+
and self._trust_initial_value
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Validate the value before setting
|
|
209
|
+
_LOGGER.debug(
|
|
210
|
+
"%s: Validating value '%s'", self.__unmangled_name__, value
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
if not self.__validate__(obj, value):
|
|
215
|
+
# For unification, raise ValueError if validation fails if the
|
|
216
|
+
# validation method just returned False
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Invalid value '{value}' for field"
|
|
219
|
+
f' {self.__unmangled_name__}'
|
|
220
|
+
)
|
|
221
|
+
except ValueError as exc:
|
|
222
|
+
# Validation failed for the value not being trusted during initial
|
|
223
|
+
# assignment, re-raise the exception
|
|
224
|
+
if not trusted_init_value:
|
|
225
|
+
_LOGGER.debug(
|
|
226
|
+
"%s: Validation failed for value '%s'",
|
|
227
|
+
self.__unmangled_name__, value
|
|
228
|
+
)
|
|
229
|
+
raise
|
|
230
|
+
|
|
231
|
+
# Log a warning about the validation failure during trusted init,
|
|
232
|
+
# so that constraints can be revised. The value will still be set,
|
|
233
|
+
# since it is trusted.
|
|
234
|
+
_LOGGER.warning(
|
|
235
|
+
"%s: Validation failed during initialization for trusted value"
|
|
236
|
+
" '%s' (%s). Please create bug report at %s if you see this"
|
|
237
|
+
" warning.",
|
|
238
|
+
self.__unmangled_name__, value, exc, BUG_REPORT_URL
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Set the validated value
|
|
242
|
+
_LOGGER.debug(
|
|
243
|
+
"%s: Setting value to '%s'", self.__unmangled_name__, value
|
|
244
|
+
)
|
|
245
|
+
setattr(obj, self.__field_name__, value)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class IntRangeValidator(ValidatorBase[int]):
|
|
249
|
+
"""
|
|
250
|
+
Descriptor for validating integer field values against min/max constraints.
|
|
251
|
+
|
|
252
|
+
The field value is validated when set. A ValueError is raised if the value
|
|
253
|
+
is outside the specified range.
|
|
254
|
+
|
|
255
|
+
Example usage:
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class Example:
|
|
259
|
+
value: int = validated_int_field(min_value=0, max_value=100)
|
|
260
|
+
|
|
261
|
+
ex = Example(value=50) # OK
|
|
262
|
+
ex.value = 100 # OK
|
|
263
|
+
ex.value = 101 # Raises ValueError
|
|
264
|
+
|
|
265
|
+
:param min_value: Minimum allowed value (inclusive), or None for no
|
|
266
|
+
minimum.
|
|
267
|
+
:param max_value: Maximum allowed value (inclusive), or None for no
|
|
268
|
+
maximum.
|
|
269
|
+
:param *: Rest of the params are passed to base class.
|
|
270
|
+
"""
|
|
271
|
+
def __init__(
|
|
272
|
+
self, *,
|
|
273
|
+
min_value: Optional[int] = None,
|
|
274
|
+
max_value: Optional[int] = None,
|
|
275
|
+
**kwargs: Any
|
|
276
|
+
) -> None:
|
|
277
|
+
super().__init__(**kwargs)
|
|
278
|
+
self._min_value = min_value
|
|
279
|
+
self._max_value = max_value
|
|
280
|
+
|
|
281
|
+
def __validate__(self, obj: Any, value: int) -> bool:
|
|
282
|
+
# Validate the value before setting
|
|
283
|
+
if value is None:
|
|
284
|
+
msg = (
|
|
285
|
+
f'{self.__unmangled_name__}: None value is not allowed'
|
|
286
|
+
)
|
|
287
|
+
_LOGGER.debug(msg)
|
|
288
|
+
raise ValueError(msg)
|
|
289
|
+
|
|
290
|
+
if self._min_value is not None and value < self._min_value:
|
|
291
|
+
msg = (
|
|
292
|
+
f'{self.__unmangled_name__}: Value {value} is below minimum'
|
|
293
|
+
f' allowed value {self._min_value}'
|
|
294
|
+
)
|
|
295
|
+
_LOGGER.debug(msg)
|
|
296
|
+
raise ValueError(msg)
|
|
297
|
+
|
|
298
|
+
if self._max_value is not None and value > self._max_value:
|
|
299
|
+
msg = (
|
|
300
|
+
f'{self.__unmangled_name__}: Value {value} is above maximum'
|
|
301
|
+
f' allowed value {self._max_value}'
|
|
302
|
+
)
|
|
303
|
+
_LOGGER.debug(msg)
|
|
304
|
+
raise ValueError(msg)
|
|
305
|
+
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class StringLengthValidator(ValidatorBase[str]):
|
|
310
|
+
"""
|
|
311
|
+
Descriptor for validating string field values against length constraints.
|
|
312
|
+
|
|
313
|
+
The field value is validated when set. A ValueError is raised if the string
|
|
314
|
+
length is outside the specified range.
|
|
315
|
+
|
|
316
|
+
Example usage:
|
|
317
|
+
|
|
318
|
+
@dataclass
|
|
319
|
+
class Example:
|
|
320
|
+
name: str = validated_string_field(min_length=1, max_length=50)
|
|
321
|
+
|
|
322
|
+
ex = Example(name="hello") # OK
|
|
323
|
+
ex.name = "a" # OK
|
|
324
|
+
ex.name = "" # Raises ValueError
|
|
325
|
+
|
|
326
|
+
:param min_length: Minimum string length (inclusive), or None for no
|
|
327
|
+
minimum.
|
|
328
|
+
:param max_length: Maximum string length (inclusive), or None for no
|
|
329
|
+
maximum.
|
|
330
|
+
:param *: Rest of the params are passed to base class.
|
|
331
|
+
"""
|
|
332
|
+
def __init__(
|
|
333
|
+
self,
|
|
334
|
+
*,
|
|
335
|
+
min_length: Optional[int] = None,
|
|
336
|
+
max_length: Optional[int] = None,
|
|
337
|
+
**kwargs: Any
|
|
338
|
+
) -> None:
|
|
339
|
+
super().__init__(**kwargs)
|
|
340
|
+
self._min_length = min_length
|
|
341
|
+
self._max_length = max_length
|
|
342
|
+
|
|
343
|
+
def __validate__(self, obj: Any, value: str) -> bool:
|
|
344
|
+
if value is None:
|
|
345
|
+
msg = (
|
|
346
|
+
f'{self.__unmangled_name__}: None value is not allowed'
|
|
347
|
+
)
|
|
348
|
+
_LOGGER.debug(msg)
|
|
349
|
+
raise ValueError(msg)
|
|
350
|
+
|
|
351
|
+
length = len(value)
|
|
352
|
+
|
|
353
|
+
# Validate the length before setting
|
|
354
|
+
if self._min_length is not None and length < self._min_length:
|
|
355
|
+
msg = (
|
|
356
|
+
f'{self.__unmangled_name__}: String length {length} is below'
|
|
357
|
+
f' minimum allowed length {self._min_length}'
|
|
358
|
+
)
|
|
359
|
+
_LOGGER.debug(msg)
|
|
360
|
+
raise ValueError(msg)
|
|
361
|
+
|
|
362
|
+
if self._max_length is not None and length > self._max_length:
|
|
363
|
+
msg = (
|
|
364
|
+
f'{self.__unmangled_name__}: String length {length} is above'
|
|
365
|
+
f' maximum allowed length {self._max_length}'
|
|
366
|
+
)
|
|
367
|
+
_LOGGER.debug(msg)
|
|
368
|
+
raise ValueError(msg)
|
|
369
|
+
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def validated_int_field(
|
|
374
|
+
*,
|
|
375
|
+
min_value: Optional[int] = None,
|
|
376
|
+
max_value: Optional[int] = None,
|
|
377
|
+
default: Optional[int] = None,
|
|
378
|
+
trust_initial_value: bool = False,
|
|
379
|
+
**kwargs: Any
|
|
380
|
+
) -> int:
|
|
381
|
+
"""
|
|
382
|
+
Create a dataclass field with integer range validation.
|
|
383
|
+
|
|
384
|
+
The field value will be validated when set, raising ValueError if outside
|
|
385
|
+
the specified range. Validation constraints are stored in field metadata
|
|
386
|
+
for later retrieval.
|
|
387
|
+
|
|
388
|
+
:param min_value: Minimum allowed value (inclusive), or None for no
|
|
389
|
+
minimum.
|
|
390
|
+
:param max_value: Maximum allowed value (inclusive), or None for no
|
|
391
|
+
maximum.
|
|
392
|
+
:param default: Default value for the field, or None for no default.
|
|
393
|
+
:param trust_initial_value: If True, skips validation during dataclass
|
|
394
|
+
initialization assuming init value is valid.
|
|
395
|
+
:param kwargs: Additional keyword arguments to pass to dataclasses.field().
|
|
396
|
+
:return: A dataclass field with IntRangeValidator descriptor and metadata.
|
|
397
|
+
|
|
398
|
+
Example:
|
|
399
|
+
|
|
400
|
+
@dataclass
|
|
401
|
+
class Config:
|
|
402
|
+
count: int = validated_int_field(
|
|
403
|
+
min_value=0, max_value=100, default=50
|
|
404
|
+
)
|
|
405
|
+
"""
|
|
406
|
+
# Store validation constraints in metadata
|
|
407
|
+
if 'metadata' not in kwargs:
|
|
408
|
+
kwargs['metadata'] = {}
|
|
409
|
+
else:
|
|
410
|
+
# Avoid modifying caller's dict
|
|
411
|
+
kwargs['metadata'] = dict(kwargs['metadata'])
|
|
412
|
+
|
|
413
|
+
metadata = IntValidationConstraints(min_value, max_value)
|
|
414
|
+
kwargs['metadata'][METADATA_KEY] = metadata
|
|
415
|
+
|
|
416
|
+
# Create the field with the descriptor as default, passing along default
|
|
417
|
+
# value
|
|
418
|
+
# pylint: disable=invalid-field-call
|
|
419
|
+
return cast(int, field(
|
|
420
|
+
**kwargs,
|
|
421
|
+
default=IntRangeValidator(
|
|
422
|
+
min_value=min_value, max_value=max_value, default=default,
|
|
423
|
+
trust_initial_value=trust_initial_value
|
|
424
|
+
)
|
|
425
|
+
))
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def validated_string_field(
|
|
429
|
+
*,
|
|
430
|
+
min_length: Optional[int] = None,
|
|
431
|
+
max_length: Optional[int] = None,
|
|
432
|
+
default: Optional[str] = None,
|
|
433
|
+
trust_initial_value: bool = False,
|
|
434
|
+
**kwargs: Any
|
|
435
|
+
) -> str:
|
|
436
|
+
"""
|
|
437
|
+
Create a dataclass field with string length validation.
|
|
438
|
+
|
|
439
|
+
The field value will be validated when set, raising ValueError if the
|
|
440
|
+
string length is outside the specified range. Validation constraints are
|
|
441
|
+
stored in field metadata for later retrieval.
|
|
442
|
+
|
|
443
|
+
:param min_length: Minimum string length (inclusive), or None for no
|
|
444
|
+
minimum.
|
|
445
|
+
:param max_length: Maximum string length (inclusive), or None for no
|
|
446
|
+
maximum.
|
|
447
|
+
:param default: Default value for the field, or None for no default.
|
|
448
|
+
:param trust_initial_value: If True, skips validation during dataclass
|
|
449
|
+
initialization assuming init value is valid.
|
|
450
|
+
:param kwargs: Additional keyword arguments to pass to dataclasses.field().
|
|
451
|
+
:return: A dataclass field with StringLengthValidator descriptor and
|
|
452
|
+
metadata.
|
|
453
|
+
|
|
454
|
+
Example:
|
|
455
|
+
|
|
456
|
+
@dataclass
|
|
457
|
+
class Config:
|
|
458
|
+
name: str = validated_string_field(
|
|
459
|
+
min_length=1, max_length=50, default="default"
|
|
460
|
+
)
|
|
461
|
+
"""
|
|
462
|
+
# Store validation constraints in metadata
|
|
463
|
+
if 'metadata' not in kwargs:
|
|
464
|
+
kwargs['metadata'] = {}
|
|
465
|
+
else:
|
|
466
|
+
# Avoid modifying caller's dict
|
|
467
|
+
kwargs['metadata'] = dict(kwargs['metadata'])
|
|
468
|
+
|
|
469
|
+
metadata = StrValidationConstraints(min_length, max_length)
|
|
470
|
+
kwargs['metadata'][METADATA_KEY] = metadata
|
|
471
|
+
|
|
472
|
+
# Create the field with the descriptor as default, passing along default
|
|
473
|
+
# value
|
|
474
|
+
# pylint: disable=invalid-field-call
|
|
475
|
+
return cast(str, field(
|
|
476
|
+
**kwargs,
|
|
477
|
+
default=StringLengthValidator(
|
|
478
|
+
min_length=min_length, max_length=max_length, default=default,
|
|
479
|
+
trust_initial_value=trust_initial_value
|
|
480
|
+
)
|
|
481
|
+
))
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# `get_field_validation_constraints` overload for `int` type
|
|
485
|
+
@overload
|
|
486
|
+
def get_field_validation_constraints(
|
|
487
|
+
dataclass_type: DataclassInstance | type[DataclassInstance],
|
|
488
|
+
field_name: str, expected_type: type[int]
|
|
489
|
+
) -> IntValidationConstraints:
|
|
490
|
+
...
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# `get_field_validation_constraints` overload for `str` type
|
|
494
|
+
@overload
|
|
495
|
+
def get_field_validation_constraints(
|
|
496
|
+
dataclass_type: DataclassInstance | type[DataclassInstance],
|
|
497
|
+
field_name: str, expected_type: type[str]
|
|
498
|
+
) -> StrValidationConstraints:
|
|
499
|
+
...
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# `get_field_validation_constraints` overload when no expected type is given
|
|
503
|
+
@overload
|
|
504
|
+
def get_field_validation_constraints(
|
|
505
|
+
dataclass_type: DataclassInstance | type[DataclassInstance],
|
|
506
|
+
field_name: str, expected_type: None = None
|
|
507
|
+
) -> ValidationConstraintsAbsent:
|
|
508
|
+
...
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# `get_field_validation_constraints` overload when non-dataclass is given
|
|
512
|
+
@overload
|
|
513
|
+
def get_field_validation_constraints(
|
|
514
|
+
dataclass_type: Any,
|
|
515
|
+
field_name: str, expected_type: None = None
|
|
516
|
+
) -> ValidationConstraintsAbsent:
|
|
517
|
+
...
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def get_field_validation_constraints(
|
|
521
|
+
dataclass_type: DataclassInstance | type[DataclassInstance],
|
|
522
|
+
field_name: str, expected_type: Optional[type] = None
|
|
523
|
+
) -> Any:
|
|
524
|
+
"""
|
|
525
|
+
Retrieve validation constraints for a specific dataclass field.
|
|
526
|
+
|
|
527
|
+
:param dataclass_type: The dataclass type or instance to inspect.
|
|
528
|
+
:param field_name: The name of the field to get constraints for.
|
|
529
|
+
:param expected_type: Expected type of the field to
|
|
530
|
+
determine which constraints to retrieve.
|
|
531
|
+
:return: Validation constraints container, or ValidationConstraintsAbsent
|
|
532
|
+
if none found. The latter is to avoid returning None and free callers from
|
|
533
|
+
having to check for it.
|
|
534
|
+
|
|
535
|
+
Example:
|
|
536
|
+
|
|
537
|
+
constraints = get_field_validation_constraints(Config, 'count', int)
|
|
538
|
+
# Returns: object with `constraints.min_value`, `constraints.max_value`
|
|
539
|
+
"""
|
|
540
|
+
if not is_dataclass(dataclass_type):
|
|
541
|
+
# Not a dataclass, return absent indicator
|
|
542
|
+
return ValidationConstraintsAbsent()
|
|
543
|
+
|
|
544
|
+
fields_list = fields(dataclass_type)
|
|
545
|
+
for f in fields_list:
|
|
546
|
+
# Find the field by name
|
|
547
|
+
if f.name != field_name:
|
|
548
|
+
continue
|
|
549
|
+
|
|
550
|
+
# Retrieve validation constraints from metadata per expected type
|
|
551
|
+
for typ, klass in (
|
|
552
|
+
# Constraints for `int` field
|
|
553
|
+
(int, IntValidationConstraints),
|
|
554
|
+
# Constraints for `str` field
|
|
555
|
+
(str, StrValidationConstraints),
|
|
556
|
+
):
|
|
557
|
+
if expected_type is not typ:
|
|
558
|
+
continue
|
|
559
|
+
|
|
560
|
+
constraints = (
|
|
561
|
+
getattr(f, 'metadata', {}).get(METADATA_KEY, None)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Return stored constraints if available
|
|
565
|
+
if isinstance(constraints, klass):
|
|
566
|
+
return constraints
|
|
567
|
+
|
|
568
|
+
# Otherwise return empty constraints
|
|
569
|
+
return klass()
|
|
570
|
+
|
|
571
|
+
# No validation constraints found, return absent indicator
|
|
572
|
+
return ValidationConstraintsAbsent()
|
pyg90alarm/entities/sensor.py
CHANGED
|
@@ -460,7 +460,7 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
460
460
|
:return: Support for updates
|
|
461
461
|
"""
|
|
462
462
|
if not self.definition:
|
|
463
|
-
_LOGGER.
|
|
463
|
+
_LOGGER.debug(
|
|
464
464
|
'Manipulating with user flags for sensor index=%s'
|
|
465
465
|
' is unsupported - no sensor definition for'
|
|
466
466
|
' type=%s, subtype=%s, protocol=%s',
|
|
@@ -595,7 +595,7 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
595
595
|
return
|
|
596
596
|
|
|
597
597
|
if value & ~G90SensorUserFlags.USER_SETTABLE:
|
|
598
|
-
_LOGGER.
|
|
598
|
+
_LOGGER.debug(
|
|
599
599
|
'User flags for sensor index=%s contain non-user settable'
|
|
600
600
|
' flags, those will be ignored: %s',
|
|
601
601
|
self.index, repr(value & ~G90SensorUserFlags.USER_SETTABLE)
|
pyg90alarm/local/alarm_phones.py
CHANGED
|
@@ -24,7 +24,8 @@ from __future__ import annotations
|
|
|
24
24
|
from typing import Dict, Any
|
|
25
25
|
from dataclasses import dataclass
|
|
26
26
|
from ..const import G90Commands
|
|
27
|
-
from .
|
|
27
|
+
from ..dataclass.load_save import DataclassLoadSave
|
|
28
|
+
from ..dataclass.validation import validated_string_field
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
@dataclass
|
|
@@ -36,28 +37,54 @@ class G90AlarmPhones(DataclassLoadSave):
|
|
|
36
37
|
LOAD_COMMAND = G90Commands.GETALMPHONE
|
|
37
38
|
SAVE_COMMAND = G90Commands.SETALMPHONE
|
|
38
39
|
|
|
40
|
+
# The field constraints below have been determined experimentally by
|
|
41
|
+
# entering various values into panel configuration manually. All values
|
|
42
|
+
# received from the panel remotely are trusted (i.e. bypass validation)
|
|
43
|
+
|
|
39
44
|
# Password to operate the panel via SMS or incoming call.
|
|
40
|
-
panel_password: str
|
|
45
|
+
panel_password: str = validated_string_field(
|
|
46
|
+
min_length=4, max_length=4, trust_initial_value=True
|
|
47
|
+
)
|
|
41
48
|
# Phone number of the alarm panel's SIM card.
|
|
42
|
-
|
|
49
|
+
# Might be empty, the panel will then presumably refuse handling SMS/calls
|
|
50
|
+
# with remote commands.
|
|
51
|
+
panel_phone_number: str = validated_string_field(
|
|
52
|
+
max_length=14, trust_initial_value=True
|
|
53
|
+
)
|
|
43
54
|
# Alarm phone number to be called on alarm.
|
|
44
|
-
# Should be in country code + number format.
|
|
45
|
-
phone_number_1: str
|
|
55
|
+
# Should be in country code + number format, or empty to disable.
|
|
56
|
+
phone_number_1: str = validated_string_field(
|
|
57
|
+
max_length=14, trust_initial_value=True
|
|
58
|
+
)
|
|
46
59
|
# Same, but for second alarm phone number.
|
|
47
|
-
phone_number_2: str
|
|
60
|
+
phone_number_2: str = validated_string_field(
|
|
61
|
+
max_length=14, trust_initial_value=True
|
|
62
|
+
)
|
|
48
63
|
# Same, but for third alarm phone number.
|
|
49
|
-
phone_number_3: str
|
|
64
|
+
phone_number_3: str = validated_string_field(
|
|
65
|
+
max_length=14, trust_initial_value=True
|
|
66
|
+
)
|
|
50
67
|
# Same, but for fourth alarm phone number.
|
|
51
|
-
phone_number_4: str
|
|
68
|
+
phone_number_4: str = validated_string_field(
|
|
69
|
+
max_length=14, trust_initial_value=True
|
|
70
|
+
)
|
|
52
71
|
# Same, but for fifth alarm phone number.
|
|
53
|
-
phone_number_5: str
|
|
72
|
+
phone_number_5: str = validated_string_field(
|
|
73
|
+
max_length=14, trust_initial_value=True
|
|
74
|
+
)
|
|
54
75
|
# Same, but for sixth alarm phone number.
|
|
55
|
-
phone_number_6: str
|
|
76
|
+
phone_number_6: str = validated_string_field(
|
|
77
|
+
max_length=14, trust_initial_value=True
|
|
78
|
+
)
|
|
56
79
|
# Phone number to send SMS notifications on alarm.
|
|
57
80
|
# Should be in country code + number format.
|
|
58
|
-
sms_push_number_1: str
|
|
81
|
+
sms_push_number_1: str = validated_string_field(
|
|
82
|
+
max_length=14, trust_initial_value=True
|
|
83
|
+
)
|
|
59
84
|
# Same, but for second SMS notification phone number.
|
|
60
|
-
sms_push_number_2: str
|
|
85
|
+
sms_push_number_2: str = validated_string_field(
|
|
86
|
+
max_length=14, trust_initial_value=True
|
|
87
|
+
)
|
|
61
88
|
|
|
62
89
|
def _asdict(self) -> Dict[str, Any]:
|
|
63
90
|
"""
|
pyg90alarm/local/host_config.py
CHANGED
|
@@ -21,10 +21,13 @@
|
|
|
21
21
|
Protocol entity for G90 alarm panel config.
|
|
22
22
|
"""
|
|
23
23
|
from __future__ import annotations
|
|
24
|
-
from typing import Dict, Any
|
|
24
|
+
from typing import Dict, Any, Optional
|
|
25
25
|
from dataclasses import dataclass
|
|
26
26
|
from enum import IntEnum
|
|
27
|
-
from .
|
|
27
|
+
from ..dataclass.load_save import (
|
|
28
|
+
DataclassLoadSave, field_readonly_if_not_provided,
|
|
29
|
+
)
|
|
30
|
+
from ..dataclass.validation import validated_int_field
|
|
28
31
|
from ..const import G90Commands
|
|
29
32
|
|
|
30
33
|
|
|
@@ -70,28 +73,60 @@ class G90HostConfig(DataclassLoadSave):
|
|
|
70
73
|
LOAD_COMMAND = G90Commands.GETHOSTCONFIG
|
|
71
74
|
SAVE_COMMAND = G90Commands.SETHOSTCONFIG
|
|
72
75
|
|
|
76
|
+
# The field constraints below have been determined experimentally by
|
|
77
|
+
# entering various values into panel configuration manually. All values
|
|
78
|
+
# received from the panel remotely are trusted (i.e. bypass validation)
|
|
79
|
+
|
|
73
80
|
# Duration of the alarm siren when triggered, in seconds
|
|
74
|
-
alarm_siren_duration: int
|
|
81
|
+
alarm_siren_duration: int = validated_int_field(
|
|
82
|
+
min_value=0, max_value=999, trust_initial_value=True
|
|
83
|
+
)
|
|
75
84
|
# Delay before arming the panel, in seconds
|
|
76
|
-
arm_delay: int
|
|
85
|
+
arm_delay: int = validated_int_field(
|
|
86
|
+
min_value=0, max_value=255, trust_initial_value=True
|
|
87
|
+
)
|
|
77
88
|
# Delay before the alarm is triggered, in seconds
|
|
78
|
-
alarm_delay: int
|
|
89
|
+
alarm_delay: int = validated_int_field(
|
|
90
|
+
min_value=0, max_value=255, trust_initial_value=True
|
|
91
|
+
)
|
|
79
92
|
# Duration of the backlight, in seconds
|
|
80
|
-
backlight_duration: int
|
|
93
|
+
backlight_duration: int = validated_int_field(
|
|
94
|
+
min_value=0, max_value=255, trust_initial_value=True
|
|
95
|
+
)
|
|
81
96
|
# Alarm volume level, applies to panel's built-in speaker
|
|
82
|
-
_alarm_volume_level: int
|
|
97
|
+
_alarm_volume_level: int = validated_int_field(
|
|
98
|
+
min_value=min(G90VolumeLevel), max_value=max(G90VolumeLevel),
|
|
99
|
+
trust_initial_value=True
|
|
100
|
+
)
|
|
83
101
|
# Speech volume level
|
|
84
|
-
_speech_volume_level: int
|
|
102
|
+
_speech_volume_level: int = validated_int_field(
|
|
103
|
+
min_value=min(G90VolumeLevel), max_value=max(G90VolumeLevel),
|
|
104
|
+
trust_initial_value=True
|
|
105
|
+
)
|
|
85
106
|
# Duration of the ring for the incoming call, in seconds
|
|
86
|
-
ring_duration: int
|
|
107
|
+
ring_duration: int = validated_int_field(
|
|
108
|
+
min_value=0, max_value=255, trust_initial_value=True
|
|
109
|
+
)
|
|
87
110
|
# Speech language
|
|
88
|
-
_speech_language: int
|
|
111
|
+
_speech_language: int = validated_int_field(
|
|
112
|
+
min_value=min(G90SpeechLanguage), max_value=max(G90SpeechLanguage),
|
|
113
|
+
trust_initial_value=True
|
|
114
|
+
)
|
|
89
115
|
# Key tone volume level
|
|
90
|
-
_key_tone_volume_level: int
|
|
116
|
+
_key_tone_volume_level: int = validated_int_field(
|
|
117
|
+
min_value=min(G90VolumeLevel), max_value=max(G90VolumeLevel),
|
|
118
|
+
trust_initial_value=True
|
|
119
|
+
)
|
|
91
120
|
# Timezone offset, in minutes
|
|
92
|
-
timezone_offset_m: int
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
timezone_offset_m: int = validated_int_field(
|
|
122
|
+
min_value=-720, max_value=720, trust_initial_value=True
|
|
123
|
+
)
|
|
124
|
+
# Ring volume level for incoming calls, could only be modified if the
|
|
125
|
+
# device has sent a value for it when loading the data (i.e. has a cellular
|
|
126
|
+
# module) otherwise it is read-only and None
|
|
127
|
+
_ring_volume_level: Optional[int] = field_readonly_if_not_provided(
|
|
128
|
+
default=None
|
|
129
|
+
)
|
|
95
130
|
|
|
96
131
|
@property
|
|
97
132
|
def speech_language(self) -> G90SpeechLanguage:
|
|
@@ -138,10 +173,16 @@ class G90HostConfig(DataclassLoadSave):
|
|
|
138
173
|
self._key_tone_volume_level = value.value
|
|
139
174
|
|
|
140
175
|
@property
|
|
141
|
-
def ring_volume_level(self) -> G90VolumeLevel:
|
|
176
|
+
def ring_volume_level(self) -> Optional[G90VolumeLevel]:
|
|
142
177
|
"""
|
|
143
178
|
Returns the ring volume level as an enum.
|
|
179
|
+
|
|
180
|
+
:return: Ring volume level, or `None` if the device does not have
|
|
181
|
+
cellular module.
|
|
144
182
|
"""
|
|
183
|
+
if self._ring_volume_level is None:
|
|
184
|
+
return None
|
|
185
|
+
|
|
145
186
|
return G90VolumeLevel(self._ring_volume_level)
|
|
146
187
|
|
|
147
188
|
@ring_volume_level.setter
|
|
@@ -163,5 +204,6 @@ class G90HostConfig(DataclassLoadSave):
|
|
|
163
204
|
'speech_language': self.speech_language.name,
|
|
164
205
|
'key_tone_volume_level': self.key_tone_volume_level.name,
|
|
165
206
|
'timezone_offset_m': self.timezone_offset_m,
|
|
166
|
-
|
|
207
|
+
# The field is optional
|
|
208
|
+
'ring_volume_level': getattr(self.ring_volume_level, 'name', None)
|
|
167
209
|
}
|
pyg90alarm/local/net_config.py
CHANGED
|
@@ -22,10 +22,11 @@ Interprets network configuration data fields of GETAPINFO/SETAPINFO commands.
|
|
|
22
22
|
"""
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
from enum import IntEnum
|
|
25
|
-
from dataclasses import dataclass
|
|
26
|
-
from typing import Any, Dict
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Dict, Optional
|
|
27
27
|
from ..const import G90Commands
|
|
28
|
-
from .
|
|
28
|
+
from ..dataclass.load_save import DataclassLoadSave, Metadata
|
|
29
|
+
from ..dataclass.validation import validated_int_field, validated_string_field
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class G90APNAuth(IntEnum):
|
|
@@ -47,27 +48,52 @@ class G90NetConfig(DataclassLoadSave):
|
|
|
47
48
|
LOAD_COMMAND = G90Commands.GETAPINFO
|
|
48
49
|
SAVE_COMMAND = G90Commands.SETAPINFO
|
|
49
50
|
|
|
51
|
+
# The field constraints below have been determined experimentally by
|
|
52
|
+
# entering various values into panel configuration manually. All values
|
|
53
|
+
# received from the panel remotely are trusted (i.e. bypass validation)
|
|
54
|
+
|
|
50
55
|
# Whether the access point is enabled, so that the device can be accessed
|
|
51
56
|
# via WiFi
|
|
52
|
-
_ap_enabled: int
|
|
57
|
+
_ap_enabled: int = validated_int_field(
|
|
58
|
+
min_value=0, max_value=1, trust_initial_value=True
|
|
59
|
+
)
|
|
53
60
|
# Access point password
|
|
54
|
-
ap_password: str
|
|
61
|
+
ap_password: str = validated_string_field(
|
|
62
|
+
min_length=9, max_length=64, trust_initial_value=True
|
|
63
|
+
)
|
|
55
64
|
# Whether WiFi is enabled, so that the device can connect to WiFi network
|
|
56
|
-
_wifi_enabled: int
|
|
65
|
+
_wifi_enabled: int = validated_int_field(
|
|
66
|
+
min_value=0, max_value=1, trust_initial_value=True
|
|
67
|
+
)
|
|
57
68
|
# Whether GPRS is enabled, so that the device can connect via cellular
|
|
58
69
|
# network
|
|
59
|
-
_gprs_enabled: int
|
|
70
|
+
_gprs_enabled: int = validated_int_field(
|
|
71
|
+
min_value=0, max_value=1, trust_initial_value=True
|
|
72
|
+
)
|
|
60
73
|
# Access Point Name (APN) for GPRS connection, as provided by the cellular
|
|
61
74
|
# operator
|
|
62
|
-
apn_name: str
|
|
75
|
+
apn_name: str = validated_string_field(
|
|
76
|
+
min_length=1, max_length=100, trust_initial_value=True
|
|
77
|
+
)
|
|
63
78
|
# User name for APN authentication, as provided by the cellular operator
|
|
64
|
-
apn_user: str
|
|
79
|
+
apn_user: str = validated_string_field(
|
|
80
|
+
min_length=0, max_length=64, trust_initial_value=True
|
|
81
|
+
)
|
|
65
82
|
# Password for APN authentication, as provided by the cellular operator
|
|
66
|
-
apn_password: str
|
|
83
|
+
apn_password: str = validated_string_field(
|
|
84
|
+
min_length=0, max_length=64, trust_initial_value=True
|
|
85
|
+
)
|
|
67
86
|
# APN authentication method, as provided by the cellular operator
|
|
68
|
-
_apn_auth: int
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
_apn_auth: int = validated_int_field(
|
|
88
|
+
min_value=min(G90APNAuth), max_value=max(G90APNAuth),
|
|
89
|
+
trust_initial_value=True
|
|
90
|
+
)
|
|
91
|
+
# GSM operator code, optional for devices lacking cellular module.
|
|
92
|
+
# The field is always skipped when saving to device.
|
|
93
|
+
gsm_operator: Optional[str] = field(
|
|
94
|
+
metadata={Metadata.NO_SERIALIZE: True},
|
|
95
|
+
default=None
|
|
96
|
+
)
|
|
71
97
|
|
|
72
98
|
@property
|
|
73
99
|
def ap_enabled(self) -> bool:
|
|
@@ -95,9 +95,11 @@ class G90PaginatedResult:
|
|
|
95
95
|
# The supplied end record number is higher than total records
|
|
96
96
|
# available, reset to the latter
|
|
97
97
|
if self._end > cmd.total:
|
|
98
|
-
_LOGGER.
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
_LOGGER.debug(
|
|
99
|
+
'Requested record range (%i) exceeds number of available'
|
|
100
|
+
' records (%i), setting to the latter',
|
|
101
|
+
self._end, cmd.total
|
|
102
|
+
)
|
|
101
103
|
self._end = cmd.total
|
|
102
104
|
|
|
103
105
|
_LOGGER.debug('Retrieved %i records in the iteration,'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
pyg90alarm/__init__.py,sha256
|
|
1
|
+
pyg90alarm/__init__.py,sha256=-sDi1njCH8ihdEY4zprzwOlkas4IHnc7aqQUg1b3gR0,3617
|
|
2
2
|
pyg90alarm/alarm.py,sha256=BDBVQ8qRa2Bx2-786uytShTw8znlFEWakxweCGvgxME,50130
|
|
3
3
|
pyg90alarm/callback.py,sha256=9PVtjRs2MLn80AgiM-UJNL8ZJF4_PxcopJIpxMmB3vc,4707
|
|
4
|
-
pyg90alarm/const.py,sha256=
|
|
4
|
+
pyg90alarm/const.py,sha256=cpaMlb8rqE1EfvmUEh3Yd_29dNO0usIo_Y-esPBh8B0,8012
|
|
5
5
|
pyg90alarm/event_mapping.py,sha256=hSmRWkkuA5hlauGvYakdOrw8QFt0TMNfUuDQ4J3vHpQ,3438
|
|
6
6
|
pyg90alarm/exceptions.py,sha256=9LleQC2SkJXjv80FlWMeaHs9ZjXxCvZx9UtYdrvejyY,1951
|
|
7
7
|
pyg90alarm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -10,6 +10,9 @@ pyg90alarm/cloud/const.py,sha256=FYUxkj0DSI7nnXZHsJMfv8RNQiXKV3TYKGHSdfHdMjg,211
|
|
|
10
10
|
pyg90alarm/cloud/messages.py,sha256=L3cpP3IbDRdw3W6FeQxTPXoDlpUv7Fm4t-RkH9Uj4dg,18032
|
|
11
11
|
pyg90alarm/cloud/notifications.py,sha256=0RxCBVcvDuwE0I1m3SLDXDQqCJimDcN4f45gr-Hvt1A,15669
|
|
12
12
|
pyg90alarm/cloud/protocol.py,sha256=82l2IXSM12tv_iWkTrAQZ-aw5UR4tmWFQJKVcgBfIww,16159
|
|
13
|
+
pyg90alarm/dataclass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
pyg90alarm/dataclass/load_save.py,sha256=I2WDeW4GingN8AEiwZm9Epf1gVL_Ynkise2h781Dn3I,8604
|
|
15
|
+
pyg90alarm/dataclass/validation.py,sha256=yd3Q86S0L5NekzoiVFW5SwpefbIhWpwvsS-S15X4Sf8,19107
|
|
13
16
|
pyg90alarm/definitions/__init__.py,sha256=s0NZnkW_gMH718DJbgez28z9WA231CyszUf1O_ojUiI,68
|
|
14
17
|
pyg90alarm/definitions/base.py,sha256=Q2DQXfFks9u-ke65_5xVTqCUXJmJGxBxFTeWnite1bw,6757
|
|
15
18
|
pyg90alarm/definitions/devices.py,sha256=K0DQnyE-1hlhIGwRfZojKJYMSmlJzmyWcZ_98urixsc,12042
|
|
@@ -19,30 +22,29 @@ pyg90alarm/entities/base_entity.py,sha256=hNhhuuwNuir54uVMZzPbjE0N6WL8wKvoW_KZa4
|
|
|
19
22
|
pyg90alarm/entities/base_list.py,sha256=vNP0T8qg7lgrES0IBs4zOggQM6Uxyfarfds46bbAnH0,9482
|
|
20
23
|
pyg90alarm/entities/device.py,sha256=eWE_N83hiDs5I-TT5F_W0Vb8sVugLldrDc9Lj9sgVLo,3700
|
|
21
24
|
pyg90alarm/entities/device_list.py,sha256=PKnHEazeT3iUEaz70bW1OaWh0wq-7WOY-7dpV4FVCTc,5984
|
|
22
|
-
pyg90alarm/entities/sensor.py,sha256=
|
|
25
|
+
pyg90alarm/entities/sensor.py,sha256=pq4tARSKQC_AJlT_ZaBcqTDtaXKuyo5nz6BqmHohSQk,27649
|
|
23
26
|
pyg90alarm/entities/sensor_list.py,sha256=0S88bhTn91O45WgbIIMQ0iXaNjlUWmMBOOFwj2Hg73U,6993
|
|
24
27
|
pyg90alarm/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
-
pyg90alarm/local/alarm_phones.py,sha256=
|
|
28
|
+
pyg90alarm/local/alarm_phones.py,sha256=bGg4aEJwtl7oMlLdOGpW2Vv4q4zS9wzkpW4q3Qy2JCU,4375
|
|
26
29
|
pyg90alarm/local/alert_config.py,sha256=n2dycEf6TH0MKefQXMcRsCFJyYvUMV55LRsy4FARtDI,6027
|
|
27
30
|
pyg90alarm/local/base_cmd.py,sha256=f0PozHJjErIg08vn0sAacHbigJSFor8q5jdxfXiM27c,10491
|
|
28
31
|
pyg90alarm/local/config.py,sha256=QYetAc6QLrAN8T-37D4Esifvao52w_uJ01nHciLbGME,1390
|
|
29
|
-
pyg90alarm/local/dataclass_load_save.py,sha256=wmHT5834_rOlgPMe0ryZW0SbdFNTLC6-4NjJlSt6mhY,4738
|
|
30
32
|
pyg90alarm/local/discovery.py,sha256=8YVIXuNe57lhas0VSRf7QXZH83pEDGNj9oehNY4Kh2U,3576
|
|
31
33
|
pyg90alarm/local/history.py,sha256=sL6_Z1BNYkuUwAZUi78d4p5hhcCfsXKw29i4Qu1O60M,10811
|
|
32
|
-
pyg90alarm/local/host_config.py,sha256=
|
|
34
|
+
pyg90alarm/local/host_config.py,sha256=KMtrkM7IPm2b38f_kCIVGxYvPOd9ZDoA3nIoT-2SBKg,7376
|
|
33
35
|
pyg90alarm/local/host_info.py,sha256=4lFIaFEpYd3EvgNrDJmKijTrzX9i29nFISLLlXGnkmE,2759
|
|
34
36
|
pyg90alarm/local/host_status.py,sha256=WHGtw-A0wHILqVWP4DnZhXj8DPRGyS26AV0bL1isTz4,1863
|
|
35
|
-
pyg90alarm/local/net_config.py,sha256=
|
|
37
|
+
pyg90alarm/local/net_config.py,sha256=x-hrisOktdy1ChacCoVBjaOq75KiDJ36nT1JsPGS42g,5453
|
|
36
38
|
pyg90alarm/local/notifications.py,sha256=Vs6NQJciYqDALV-WwzH6wIcTGdX_UD4XBuHWjSOpCDY,4591
|
|
37
39
|
pyg90alarm/local/paginated_cmd.py,sha256=5pPVP8f4ydjgu8Yq6MwqINJAUt52fFlD17wO4AI88Pc,4467
|
|
38
|
-
pyg90alarm/local/paginated_result.py,sha256=
|
|
40
|
+
pyg90alarm/local/paginated_result.py,sha256=p_e8QAVznp1Q5Xi9ifjb9Bx-S3ZiAkVlPKrY6r0bYLs,5483
|
|
39
41
|
pyg90alarm/local/targeted_discovery.py,sha256=Ik2C2VBtVLurf3-RKko4O2R3B6MrmFdOskd457uyASU,5516
|
|
40
42
|
pyg90alarm/local/user_data_crc.py,sha256=JQBOPY3RlOgVtvR55R-rM8OuKjYW-BPXQ0W4pi6CEH0,1689
|
|
41
43
|
pyg90alarm/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
44
|
pyg90alarm/notifications/base.py,sha256=d3N_zNPa_jcTX4QpA78jdgMHDhmrgwqyM3HdvuO14Jk,16682
|
|
43
45
|
pyg90alarm/notifications/protocol.py,sha256=TlZQ3P8-N-E2X5bzkGefz432x4lBYyIBF9VriwYn9ds,4790
|
|
44
|
-
pyg90alarm-2.
|
|
45
|
-
pyg90alarm-2.
|
|
46
|
-
pyg90alarm-2.
|
|
47
|
-
pyg90alarm-2.
|
|
48
|
-
pyg90alarm-2.
|
|
46
|
+
pyg90alarm-2.5.0.dist-info/licenses/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
|
|
47
|
+
pyg90alarm-2.5.0.dist-info/METADATA,sha256=3PEKVWjqGrheDbXaFgGGApdIMVaF86b9acuQb3dWUB0,12568
|
|
48
|
+
pyg90alarm-2.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
49
|
+
pyg90alarm-2.5.0.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
|
|
50
|
+
pyg90alarm-2.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|