4
4
5
5
import logging
6
6
from enum import Enum
7
+ from typing import Literal , overload
7
8
8
9
from ...exceptions import KasaException
9
10
from ...feature import Feature
10
- from ..iotmodule import IotModule
11
+ from ..iotmodule import IotModule , merge
11
12
12
13
_LOGGER = logging .getLogger (__name__ )
13
14
@@ -20,6 +21,9 @@ class Range(Enum):
20
21
Near = 2
21
22
Custom = 3
22
23
24
+ def __str__ (self ) -> str :
25
+ return self .name
26
+
23
27
24
28
class Motion (IotModule ):
25
29
"""Implements the motion detection (PIR) module."""
@@ -30,6 +34,11 @@ def _initialize_features(self) -> None:
30
34
if "get_config" not in self .data :
31
35
return
32
36
37
+ # Require that ADC value is also present.
38
+ if "get_adc_value" not in self .data :
39
+ _LOGGER .warning ("%r initialized, but no get_adc_value in response" )
40
+ return
41
+
33
42
if "enable" not in self .config :
34
43
_LOGGER .warning ("%r initialized, but no enable in response" )
35
44
return
@@ -48,20 +57,78 @@ def _initialize_features(self) -> None:
48
57
)
49
58
)
50
59
60
+ self ._add_feature (
61
+ Feature (
62
+ device = self ._device ,
63
+ container = self ,
64
+ id = "pir_range" ,
65
+ name = "Motion Sensor Range" ,
66
+ icon = "mdi:motion-sensor" ,
67
+ attribute_getter = "range" ,
68
+ attribute_setter = "_set_range_cli" ,
69
+ type = Feature .Type .Choice ,
70
+ choices_getter = "ranges" ,
71
+ value_parser = "parse_range_value" ,
72
+ category = Feature .Category .Config ,
73
+ )
74
+ )
75
+
76
+ self ._add_feature (
77
+ Feature (
78
+ device = self ._device ,
79
+ container = self ,
80
+ id = "pir_threshold" ,
81
+ name = "Motion Sensor Threshold" ,
82
+ icon = "mdi:motion-sensor" ,
83
+ attribute_getter = "threshold" ,
84
+ attribute_setter = "set_threshold" ,
85
+ type = Feature .Type .Number ,
86
+ category = Feature .Category .Config ,
87
+ )
88
+ )
89
+
90
+ self ._add_feature (
91
+ Feature (
92
+ device = self ._device ,
93
+ container = self ,
94
+ id = "pir_adc_value" ,
95
+ name = "PIR ADC Value" ,
96
+ icon = "mdi:motion-sensor" ,
97
+ attribute_getter = "adc_value" ,
98
+ attribute_setter = None ,
99
+ type = Feature .Type .Sensor ,
100
+ category = Feature .Category .Primary ,
101
+ )
102
+ )
103
+
104
+ self ._add_feature (
105
+ Feature (
106
+ device = self ._device ,
107
+ container = self ,
108
+ id = "pir_triggered" ,
109
+ name = "PIR Triggered" ,
110
+ icon = "mdi:motion-sensor" ,
111
+ attribute_getter = "is_triggered" ,
112
+ attribute_setter = None ,
113
+ type = Feature .Type .Sensor ,
114
+ category = Feature .Category .Primary ,
115
+ )
116
+ )
117
+
51
118
def query (self ) -> dict :
52
119
"""Request PIR configuration."""
53
- return self .query_for_command ("get_config" )
120
+ req = merge (
121
+ self .query_for_command ("get_config" ),
122
+ self .query_for_command ("get_adc_value" ),
123
+ )
124
+
125
+ return req
54
126
55
127
@property
56
128
def config (self ) -> dict :
57
129
"""Return current configuration."""
58
130
return self .data ["get_config" ]
59
131
60
- @property
61
- def range (self ) -> Range :
62
- """Return motion detection range."""
63
- return Range (self .config ["trigger_index" ])
64
-
65
132
@property
66
133
def enabled (self ) -> bool :
67
134
"""Return True if module is enabled."""
@@ -71,23 +138,99 @@ async def set_enabled(self, state: bool) -> dict:
71
138
"""Enable/disable PIR."""
72
139
return await self .call ("set_enable" , {"enable" : int (state )})
73
140
141
+ def _parse_range_value (self , value : str ) -> int | Range | None :
142
+ """Attempt to parse a range value from the given string."""
143
+ _LOGGER .debug ("Parse Range Value: %s" , value )
144
+ parsed : int | Range | None = None
145
+ try :
146
+ parsed = int (value )
147
+ _LOGGER .debug ("Parse Range Value: %s is an integer." , value )
148
+ return parsed
149
+ except ValueError :
150
+ _LOGGER .debug ("Parse Range Value: %s is not an integer." , value )
151
+ value = value .strip ().upper ()
152
+ if value in Range ._member_names_ :
153
+ _LOGGER .debug ("Parse Range Value: %s is an enumeration." , value )
154
+ parsed = Range [value ]
155
+ return parsed
156
+ _LOGGER .debug ("Parse Range Value: %s is not a Range Value." , value )
157
+ return None
158
+
159
+ @property
160
+ def ranges (self ) -> list [Range ]:
161
+ """Return set of supported range classes."""
162
+ range_min = 0
163
+ range_max = len (self .config ["array" ])
164
+ valid_ranges = list ()
165
+ for r in Range :
166
+ if (r .value >= range_min ) and (r .value < range_max ):
167
+ valid_ranges .append (r )
168
+ return valid_ranges
169
+
170
+ @property
171
+ def range (self ) -> Range :
172
+ """Return motion detection Range."""
173
+ return Range (self .config ["trigger_index" ])
174
+
175
+ @overload
176
+ async def set_range (self , * , range : Range ) -> dict : ...
177
+
178
+ @overload
179
+ async def set_range (self , * , range : Literal [Range .Custom ], value : int ) -> dict : ...
180
+
181
+ @overload
182
+ async def set_range (self , * , value : int ) -> dict : ...
183
+
74
184
async def set_range (
75
- self , * , range : Range | None = None , custom_range : int | None = None
185
+ self , * , range : Range | None = None , value : int | None = None
76
186
) -> dict :
77
- """Set the range for the sensor.
187
+ """Set the Range for the sensor.
78
188
79
- :param range : for using standard ranges
80
- :param custom_range: range in decimeters, overrides the range parameter
189
+ :param Range : for using standard Ranges
190
+ :param custom_Range: Range in decimeters, overrides the Range parameter
81
191
"""
82
- if custom_range is not None :
83
- payload = {"index" : Range .Custom .value , "value" : custom_range }
192
+ if value is not None :
193
+ if range is not None and range is not Range .Custom :
194
+ raise KasaException (
195
+ "Refusing to set non-custom range %s to value %d." % (range , value )
196
+ )
197
+ elif value is None :
198
+ raise KasaException ("Custom range threshold may not be set to None." )
199
+ payload = {"index" : Range .Custom .value , "value" : value }
84
200
elif range is not None :
85
201
payload = {"index" : range .value }
86
202
else :
87
- raise KasaException ("Either range or custom_range need to be defined" )
203
+ raise KasaException ("Either range or value needs to be defined" )
88
204
89
205
return await self .call ("set_trigger_sens" , payload )
90
206
207
+ async def _set_range_cli (self , input : Range | int ) -> dict :
208
+ if isinstance (input , Range ):
209
+ return await self .set_range (range = input )
210
+ elif isinstance (input , int ):
211
+ return await self .set_range (value = input )
212
+ else :
213
+ raise KasaException (
214
+ "Invalid type: %s given to cli motion set." % (type (input ))
215
+ )
216
+
217
+ def get_range_threshold (self , range_type : Range ) -> int :
218
+ """Get the distance threshold at which the PIR sensor is will trigger."""
219
+ if range_type .value < 0 or range_type .value >= len (self .config ["array" ]):
220
+ raise KasaException (
221
+ "Range type is outside the bounds of the configured device ranges."
222
+ )
223
+ return int (self .config ["array" ][range_type .value ])
224
+
225
+ @property
226
+ def threshold (self ) -> int :
227
+ """Return motion detection Range."""
228
+ return self .get_range_threshold (self .range )
229
+
230
+ async def set_threshold (self , value : int ) -> dict :
231
+ """Set the distance threshold at which the PIR sensor is will trigger."""
232
+ return await self .set_range (value = value )
233
+
91
234
@property
92
235
def inactivity_timeout (self ) -> int :
93
236
"""Return inactivity timeout in milliseconds."""
@@ -100,3 +243,13 @@ async def set_inactivity_timeout(self, timeout: int) -> dict:
100
243
to avoid reverting this back to 60 seconds after a period of time.
101
244
"""
102
245
return await self .call ("set_cold_time" , {"cold_time" : timeout })
246
+
247
+ @property
248
+ def adc_value (self ) -> int :
249
+ """Return motion adc value."""
250
+ return int (self .data ["get_adc_value" ]["value" ])
251
+
252
+ @property
253
+ def is_triggered (self ) -> bool :
254
+ """Return if the motion sensor has been triggered."""
255
+ return (self .enabled ) and (self .adc_value < self .range .value )
0 commit comments