Skip to content

Commit 9f28e3f

Browse files
committed
Improve PIR Sensor Readings
- Fix: Read PIR as ADC value centered around ADC midpoint/zeropoint.
1 parent d3b5988 commit 9f28e3f

File tree

1 file changed

+121
-12
lines changed

1 file changed

+121
-12
lines changed

kasa/iot/modules/motion.py

+121-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import logging
6+
import math
67
from enum import Enum
78
from typing import Literal, overload
89

@@ -84,6 +85,50 @@ def _initialize_features(self) -> None:
8485
attribute_setter="set_threshold",
8586
type=Feature.Type.Number,
8687
category=Feature.Category.Config,
88+
range_getter=lambda: (0, 100),
89+
)
90+
)
91+
92+
self._add_feature(
93+
Feature(
94+
device=self._device,
95+
container=self,
96+
id="pir_triggered",
97+
name="PIR Triggered",
98+
icon="mdi:motion-sensor",
99+
attribute_getter="pir_triggered",
100+
attribute_setter=None,
101+
type=Feature.Type.Sensor,
102+
category=Feature.Category.Primary,
103+
)
104+
)
105+
106+
self._add_feature(
107+
Feature(
108+
device=self._device,
109+
container=self,
110+
id="pir_value",
111+
name="PIR Reading",
112+
icon="mdi:motion-sensor",
113+
attribute_getter="pir_value",
114+
attribute_setter=None,
115+
type=Feature.Type.Sensor,
116+
category=Feature.Category.Info,
117+
)
118+
)
119+
120+
self._add_feature(
121+
Feature(
122+
device=self._device,
123+
container=self,
124+
id="pir_percent",
125+
name="PIR Percentage",
126+
icon="mdi:motion-sensor",
127+
attribute_getter="pir_percent",
128+
attribute_setter=None,
129+
type=Feature.Type.Sensor,
130+
category=Feature.Category.Info,
131+
unit_getter=lambda: "%",
87132
)
88133
)
89134

@@ -97,21 +142,49 @@ def _initialize_features(self) -> None:
97142
attribute_getter="adc_value",
98143
attribute_setter=None,
99144
type=Feature.Type.Sensor,
100-
category=Feature.Category.Primary,
145+
category=Feature.Category.Debug,
101146
)
102147
)
103148

104149
self._add_feature(
105150
Feature(
106151
device=self._device,
107152
container=self,
108-
id="pir_triggered",
109-
name="PIR Triggered",
153+
id="pir_adc_min",
154+
name="PIR ADC Min",
110155
icon="mdi:motion-sensor",
111-
attribute_getter="is_triggered",
156+
attribute_getter="adc_min",
112157
attribute_setter=None,
113158
type=Feature.Type.Sensor,
114-
category=Feature.Category.Primary,
159+
category=Feature.Category.Debug,
160+
)
161+
)
162+
163+
self._add_feature(
164+
Feature(
165+
device=self._device,
166+
container=self,
167+
id="pir_adc_mid",
168+
name="PIR ADC Mid",
169+
icon="mdi:motion-sensor",
170+
attribute_getter="adc_midpoint",
171+
attribute_setter=None,
172+
type=Feature.Type.Sensor,
173+
category=Feature.Category.Debug,
174+
)
175+
)
176+
177+
self._add_feature(
178+
Feature(
179+
device=self._device,
180+
container=self,
181+
id="pir_adc_max",
182+
name="PIR ADC Max",
183+
icon="mdi:motion-sensor",
184+
attribute_getter="adc_max",
185+
attribute_setter=None,
186+
type=Feature.Type.Sensor,
187+
category=Feature.Category.Debug,
115188
)
116189
)
117190

@@ -134,6 +207,28 @@ def enabled(self) -> bool:
134207
"""Return True if module is enabled."""
135208
return bool(self.config["enable"])
136209

210+
@property
211+
def adc_min(self) -> int:
212+
"""Return minimum ADC sensor value."""
213+
return int(self.config["min_adc"])
214+
215+
@property
216+
def adc_max(self) -> int:
217+
"""Return maximum ADC sensor value."""
218+
return int(self.config["max_adc"])
219+
220+
@property
221+
def adc_midpoint(self) -> int:
222+
"""
223+
Return the midpoint for the ADC.
224+
225+
The midpoint represents the zero point for the PIR sensor waveform.
226+
227+
Currently this is estimated by:
228+
math.floor(abs(adc_max - adc_min) / 2)
229+
"""
230+
return math.floor(abs(self.adc_max - self.adc_min) / 2)
231+
137232
async def set_enabled(self, state: bool) -> dict:
138233
"""Enable/disable PIR."""
139234
return await self.call("set_enable", {"enable": int(state)})
@@ -192,7 +287,7 @@ async def set_range(
192287
if value is not None:
193288
if range is not None and range is not Range.Custom:
194289
raise KasaException(
195-
"Refusing to set non-custom range %s to value %d." % (range, value)
290+
f"Refusing to set non-custom range {range} to value {value}."
196291
)
197292
elif value is None:
198293
raise KasaException("Custom range threshold may not be set to None.")
@@ -210,9 +305,7 @@ async def _set_range_cli(self, input: Range | int) -> dict:
210305
elif isinstance(input, int):
211306
return await self.set_range(value=input)
212307
else:
213-
raise KasaException(
214-
"Invalid type: %s given to cli motion set." % (type(input))
215-
)
308+
raise KasaException(f"Invalid type: {type(input)} given to cli motion set.")
216309

217310
def get_range_threshold(self, range_type: Range) -> int:
218311
"""Get the distance threshold at which the PIR sensor is will trigger."""
@@ -247,9 +340,25 @@ async def set_inactivity_timeout(self, timeout: int) -> dict:
247340
@property
248341
def adc_value(self) -> int:
249342
"""Return motion adc value."""
250-
return int(self.data["get_adc_value"]["value"])
343+
return self.data["get_adc_value"]["value"]
344+
345+
@property
346+
def pir_value(self) -> int:
347+
"""Return the computed PIR sensor value."""
348+
return self.adc_midpoint - self.adc_value
349+
350+
@property
351+
def pir_percent(self) -> float:
352+
"""Return the computed PIR sensor value, in percentile form."""
353+
amp = self.pir_value
354+
per: float
355+
if amp < 0:
356+
per = (float(amp) / (self.adc_midpoint - self.adc_min)) * 100
357+
else:
358+
per = (float(amp) / (self.adc_max - self.adc_midpoint)) * 100
359+
return per
251360

252361
@property
253-
def is_triggered(self) -> bool:
362+
def pir_triggered(self) -> bool:
254363
"""Return if the motion sensor has been triggered."""
255-
return (self.enabled) and (self.adc_value < self.threshold)
364+
return (self.enabled) and (abs(self.pir_percent) > (100 - self.threshold))

0 commit comments

Comments
 (0)