Skip to content

Add ADC Value to PIR Enabled Switches #1263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 25, 2025
Merged

Conversation

ryenitcher
Copy link
Contributor

  • Add: ADC value reporting to PIR enabled switches, so that it may be used in polling automations.
  • Add: ADC trigger state value reporting, for simpler automations.
  • Add: Ability for features to use custom value parsers.

@ryenitcher
Copy link
Contributor Author

ryenitcher commented Nov 15, 2024

Example outputs for KS200M:

Nothing triggering PIR:

== Room Lights - KS200M(US) ==
Host: 1.2.3.4
Port: 9999
Device state: False
Time:         2024-11-15 00:15:00-07:00 (tz: MST7MDT)
Hardware:     1.0
Software:     1.0.10 Build 221019 Rel.194527
MAC (rssi):   AB:CD... (-38)

== Primary features ==
State (state): False
PIR ADC Value (pir_adc_value): 2079
PIR Triggered (pir_triggered): False
Ambient Light (ambient_light): 0 %

== Information ==
On since (on_since): None
Cloud connection (cloud_connection): True

== Configuration ==
LED (led): True
PIR enabled (pir_enabled): True
Motion Sensor Range (pir_range): *Far* Mid Near Custom
Motion Sensor Threshold (pir_threshold): 80 (range: 0-65536)
Ambient light enabled (ambient_light_enabled): True

== Debug ==
RSSI (rssi): -38 dBm
Reboot (reboot): <Action>

Hand in front of switch Triggering PIR:

== Room Lights - KS200M(US) ==
Host: 1.2.3.4
Port: 9999
Device state: True
Time:         2024-11-15 00:25:28-07:00 (tz: MST7MDT)
Hardware:     1.0
Software:     1.0.10 Build 221019 Rel.194527
MAC (rssi):   aa:bb:cc (-38)

== Primary features ==
State (state): True
PIR ADC Value (pir_adc_value): 0
PIR Triggered (pir_triggered): True
Ambient Light (ambient_light): 39 %

== Information ==
On since (on_since): 2024-11-15 00:25:27-07:00
Cloud connection (cloud_connection): True

== Configuration ==
LED (led): True
PIR enabled (pir_enabled): True
Motion Sensor Range (pir_range): *Far* Mid Near Custom
Motion Sensor Threshold (pir_threshold): 80 (range: 0-65536)
Ambient light enabled (ambient_light_enabled): True

== Debug ==
RSSI (rssi): -38 dBm
Reboot (reboot): <Action>

Copy link

codecov bot commented Nov 15, 2024

Codecov Report

Attention: Patch coverage is 94.82759% with 6 lines in your changes missing coverage. Please review.

Project coverage is 92.61%. Comparing base (5b9b897) to head (1774b81).
Report is 24 commits behind head on master.

Files with missing lines Patch % Lines
kasa/iot/modules/motion.py 94.49% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1263      +/-   ##
==========================================
+ Coverage   92.57%   92.61%   +0.03%     
==========================================
  Files         147      147              
  Lines        9185     9288     +103     
  Branches      938      940       +2     
==========================================
+ Hits         8503     8602      +99     
- Misses        492      494       +2     
- Partials      190      192       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ryenitcher ryenitcher marked this pull request as draft November 15, 2024 07:59
@ryenitcher
Copy link
Contributor Author

ryenitcher commented Nov 15, 2024

What I've managed to learn:

  1. The Kasa Motion Switches have a PIR and Ambient Light sensor.
  2. The PIR and AMB(idk) are connected behind a 12-bit ADC (analog to digital sensor).
  3. The PIR sensor appears to have analog functionality only, likely as a cost saving measure, since the switch will always be wired to power, low power PIR is not required.
    • This datasheet seems to give a decent explaination of PIR sensors.
  4. The kasa software appears to do the work of adjusting the ADC sensor range automatically so that the reading stays centered in the 0-4096 range.
  5. PIR sensors work in such a way that they generate a waveform that has a peak and inverse peak, with order depending on the direction of motion, thanks to the fresnel lens.
  6. It is not clear if the kasa software triggers on both the peak and the valley, the peak only, the valley only, or some other exotic trigger condition.
  7. The PIR config array values do not make sense, as they are not 12 bit integer thresholds, and they are ordered in opposite magnitude than would be expected for Far/Mid/Near. The best guess is that it is a value that is subtracted from some internal constant PIR threshold prior to triggering, however it could represent a constant or multiplicand for some higher order calibration function that is not immediately clear.
  8. To provide a crude approximation of the current trigger state, we can normalize the value around the midpoint in the +-100 range, and then compare the value against (100-array[trigger_index]). This does not always match the actual trigger times and states, as it does not track the hysteresis periods nor can it do rolling averages, but it is better than nothing.
  9. The switch API will only let you pull the ADC value about 6-10 times per second, so perfect readings are likely not possible, and would come at the expense of high network bandwidth.
  10. Motion events are relatively short lived, so to have any chance of catching them, you likely need to poll at 1Hz or greater.

Here is a sample csv with time, adc_value readings for reference.

Note:
Since the CLI must scan for devices prior to the connection, things like watch -n 0.5 kasa ... do not meaningfully work.
It may be worth adding a --watch option to the CLI, or the ability to set the scan timeout to fractional values greater than zero but less than 1.

@ryenitcher ryenitcher marked this pull request as ready for review November 20, 2024 03:53
@sdb9696
Copy link
Collaborator

sdb9696 commented Nov 20, 2024

I'm not sure about the complication with the range value parsing. Wouldn't it be simpler to just have two different features pir_range_type and pir_range_value. pir_range_type can get/set Range and pir_range_value the value?

If this needs to query every second to be effective there should be a separate async function to get the current adc. This way a consumer can run that query every second in isolation from the rest of the device queries.

Could you please separate out the dump_devinfo changes into a separate PR as I think they are ready to go now.

(If you update to latest master the attribute_setter now accepts a callable so you can use a lambda to call the set_range with the appropriate values.

@sdb9696 sdb9696 added the enhancement New feature or request label Nov 20, 2024
@ryenitcher
Copy link
Contributor Author

I'm not sure about the complication with the range value parsing. Wouldn't it be simpler to just have two different features pir_range_type and pir_range_value. pir_range_type can get/set Range and pir_range_value the value?

If this needs to query every second to be effective there should be a separate async function to get the current adc. This way a consumer can run that query every second in isolation from the rest of the device queries.

I agree with you that the complication on the range value parsing is far from optimal. The goal there was to maintain the existing behavior where the range could be set to either a number or a range enum value, but I never really liked that feature to begin with. If you are good with removing it, since I doubt anyone has even used it yet, I can strip it out and simplify the logic so that the range class and range value are independent entities that don't require the advanced parsing.

@ryenitcher
Copy link
Contributor Author

The other remaining question I had is if it's wise to expose a simulated pir_triggered value or not given that the exact logic of the switch is not known, and we cannot simulate it exactly. I would put it as a debug feature but then I would worry that someone in Home Assistant would find it and then get confused as to why it is seemingly broken.

I personally find value in it, but I mostly understand the logic and limitations of the pir_triggered value.

@ryenitcher ryenitcher force-pushed the dev-adc branch 3 times, most recently from 821a358 to 786cd93 Compare November 22, 2024 06:06
@ryenitcher
Copy link
Contributor Author

If this needs to query every second to be effective there should be a separate async function to get the current adc. This way a consumer can run that query every second in isolation from the rest of the device queries.

The motion events are fairly jittery, but appear to last about 2-5 seconds for a pulse, give or take. Adding an async function to monitor the value is a decent idea, do you have a function like that implemented somewhere else that I can copy?

For reference, here's what the PIR ADC sensor value looks like when polling continuously:
motion
The bottom graph is the absolute value of the reading (relative to the midpoint), scaled from 0-100.

@sdb9696
Copy link
Collaborator

sdb9696 commented Nov 22, 2024

Adding an async function to monitor the value is a decent idea, do you have a function like that implemented somewhere else that I can copy?

https://github.com/python-kasa/python-kasa/blob/master/kasa/smart/modules/energy.py#L76

@ryenitcher
Copy link
Contributor Author

I've added an async function to get the current ADC status. Using the async function from a test script shows that it is possible to poll the sensor at rates at least as high as 15Hz. I'm not certain as to if that is wise or not.

Let me know if there is anything else I should change for this. I'm still missing some coverage over parts of the CLI parameter handling, but I'm not sure if it needs the additional tests, and/or if the additional tests should go in the test_cli.py or the test_motion.py.

Copy link
Collaborator

@sdb9696 sdb9696 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nearly there with just a few nits left. I think the test coverage is ok btw.

payload = {"index": range.value}
return await self.call("set_trigger_sens", payload)

def _parse_range_value(self, value: str) -> Range:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the library is python >= 3.11 could this all be made simpler by converting Range to StrEnum?

Copy link
Contributor Author

@ryenitcher ryenitcher Dec 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can tell, StrEnum will not improve this use case unfortunately, as it still does not define a proper Enum.get(key, default).

The enum is also a mapping of the integer returned by trigger_index, so perhaps we could use IntEnum? But then it comes down to how you want the range to be presented to the user, as 0, 1, 2, 3 or FAR, NEAR, MID, CUSTOM. Currently it will accept the range enum names, in upper or lower case, but of course that relies on the range classes not changing on future devices.

It's cool to learn about the new StrEnum.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't completely follow the Enum.get(key, default). Can't you just catch a value error? StrEnum would mean the method could take str | StrEnum which could mean more flexibility when we have a common interface across devices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Enum.get() is kind of pointless now that I think about it more, since in a failure case an exception will be thrown anyways, so there is no point in avoiding the underlying exception to begin with.

StrEnum is a subclass of str so I don't think that StringEnum gains us anything here, since that would still require a mapping to the index IDs of the level.

Hopefully this latest revision makes more sense?

@@ -30,6 +93,11 @@ def _initialize_features(self) -> None:
if "get_config" not in self.data:
return

# Require that ADC value is also present.
if "get_adc_value" not in self.data:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the expectation that all PIR devices will have get_adc_value or could some only have get_config causing this line to impact those devices?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect that all PIR devices will have the get_adc_value, but of course I have no way of actually proving that for future devices. It's possible an updated model could come out with a digital/event driven PIR sensor instead, but then I would expect the get_config to take on a different form than what is currently supported as well.

I can remove the check if that's better for device support.

@ryenitcher
Copy link
Contributor Author

Sorry for the delay in updating this one, I think it should be good to go now, minus anything new you find.

@ryenitcher
Copy link
Contributor Author

@sdb9696 do you have any thoughts on this latest version?

@sdb9696
Copy link
Collaborator

sdb9696 commented Dec 11, 2024

I started to review it today but had to put it on pause. Will endeavour to finish tomorrow am.

Copy link
Collaborator

@sdb9696 sdb9696 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few suggestions that are hopefully not too onerous.

Comment on lines 379 to 382
async def get_pir_state(self) -> PIRStatus:
"""Return real-time PIR status."""
current = await self.call("get_adc_value")
return PIRStatus(current["value"])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this update the internal data before returning.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be resolved with the latest commit.

payload = {"index": range.value}
return await self.call("set_trigger_sens", payload)

def _parse_range_value(self, value: str) -> Range:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't completely follow the Enum.get(key, default). Can't you just catch a value error? StrEnum would mean the method could take str | StrEnum which could mean more flexibility when we have a common interface across devices.



@dataclass
class PIRStatus:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should take PIRConfig in it's constructor as init_only and all the get_ methods can then attributes. If you include the mashumaro DataClassMixin the data class will then get to_dict() method and you can set all the attributes in post init. I think that is more intuitive than returning a status that then needs to be provided config to get more values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree updating the PIRStatus to be more of a standalone makes sense. I had originally written it thinking that there might be cases where the ADC value is read and then the config is updated, but that's rather unlikely now that I think about it.

Hopefully using properties is agreeable to you, in doing so we can avoid computing values that are never used, and slowing down polling of get_pir_state().

@sdb9696
Copy link
Collaborator

sdb9696 commented Jan 22, 2025

Hey @ryenitcher, any plans to finish this off before we cut the next release in the next day or so? There's not much left to do afaict.

- Add: ADC value reporting to PIR enabled switches, so that it may be used in polling automations.
- Add: ADC trigger state value reporting, for simpler automations.
- Add: Ability for features to use custom value parsers.
- Fix: Read PIR as ADC value centered around ADC midpoint/zeropoint.
- Fix: Back out custom value parser logic.
- Fix: Remove overloaded range set functionality.
- Fix: Improve error messages when user forgets quotes around cli arguments.
- Add: Async function to get the current state of the ADC PIR sensor.
- Fix: Move ADC PIR state and config into proper dataclasses.
- Add: Add CLI test for passing bad quotes to feature set.
- Fix: Decrease log message severity in `feature.py`.
- Fix: Rename `_set_range_cli` to `_set_range_from_str`.
- Add: Basic tests for string to Motion Range value converter function.
- Fix: Make PIRStatus into proper dataclass.
- Fix: Update internal PIR state when async polling.
@ryenitcher
Copy link
Contributor Author

Hey @ryenitcher, any plans to finish this off before we cut the next release in the next day or so? There's not much left to do afaict.

Sorry for the delay in getting back to this, the end of the year is always far more busy than I expect.

Copy link
Collaborator

@sdb9696 sdb9696 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good with the latest updates, thanks @ryenitcher!

@sdb9696 sdb9696 merged commit 7f2a1be into python-kasa:master Jan 25, 2025
18 checks passed
@sdb9696 sdb9696 mentioned this pull request Jan 26, 2025
sdb9696 added a commit that referenced this pull request Jan 26, 2025
## [0.10.0](https://github.com/python-kasa/python-kasa/tree/0.10.0) (2025-01-26)

[Full Changelog](0.9.1...0.10.0)

**Release summary:**

This release brings support for many new devices, including completely new device types:

- Support for Tapo robot vacuums. Special thanks to @steveredden, @MAXIGAMESSUPPER, and veep60 for helping to get this implemented!
- Support for hub attached cameras and doorbells (H200)
- Improved support for hubs (including pairing & better chime controls)
- Support for many new camera and doorbell device models, including C220, C720, D100C, D130, and D230

Many thanks to testers and new contributors - @steveredden, @DawidPietrykowski, @Obbay2, @andrewome, @ryenitcher and @etmmvdp!

**Breaking changes:**

- `uses_http` is now a readonly property of device config. Consumers that relied on `uses_http` to be persisted with `DeviceConfig.to_dict()` will need to store the value separately.
- `is_color`, `is_dimmable`, `is_variable_color_temp`, `valid_temperate_range`, and `has_effects` attributes from the `Light` module are deprecated, consumers should use `has_feature("hsv")`, `has_feature("brightness")`, `has_feature("color_temp")`, `get_feature("color_temp").range`, and `Module.LightEffect in dev.modules` respectively. Calling the deprecated attributes will emit a `DeprecationWarning` and type checkers will fail them.
-  `alarm_volume` on the `smart.Alarm` module is changed from `str` to `int`

**Breaking changes:**

- Make uses\_http a readonly property of device config [\#1449](#1449) (@sdb9696)
- Allow passing alarm parameter overrides [\#1340](#1340) (@rytilahti)
- Deprecate legacy light module is\_capability checks [\#1297](#1297) (@sdb9696)

**Implemented enhancements:**

- Expose more battery sensors for D230 [\#1451](#1451)
- dumping HTTP POST Body for Tapo Vacuum \(RV30 Plus\) [\#937](#937)
- Add common alarm interface [\#1479](#1479) (@sdb9696)
- Add common childsetup interface [\#1470](#1470) (@sdb9696)
- Add childsetup module to smartcam hubs [\#1469](#1469) (@sdb9696)
- Add smartcam pet detection toggle module [\#1465](#1465) (@DawidPietrykowski)
- Only log one warning per unknown clean error code and status [\#1462](#1462) (@rytilahti)
- Add childlock module for vacuums [\#1461](#1461) (@rytilahti)
- Add ultra mode \(fanspeed = 5\) for vacuums [\#1459](#1459) (@rytilahti)
- Add setting to change carpet clean mode [\#1458](#1458) (@rytilahti)
- Add setting to change clean count [\#1457](#1457) (@rytilahti)
- Add mop module [\#1456](#1456) (@rytilahti)
- Enable dynamic hub child creation and deletion on update [\#1454](#1454) (@sdb9696)
- Expose current cleaning information [\#1453](#1453) (@rytilahti)
- Add battery module to smartcam devices [\#1452](#1452) (@sdb9696)
- Allow update of camera modules after setting values [\#1450](#1450) (@sdb9696)
- Update hub children on first update and delay subsequent updates [\#1438](#1438) (@sdb9696)
- Add support for doorbells and chimes [\#1435](#1435) (@steveredden)
- Implement vacuum dustbin module \(dust\_bucket\) [\#1423](#1423) (@rytilahti)
- Allow https for klaptransport [\#1415](#1415) (@rytilahti)
- Add smartcam child device support for smartcam hubs [\#1413](#1413) (@sdb9696)
- Add powerprotection module [\#1337](#1337) (@rytilahti)
- Add vacuum speaker controls [\#1332](#1332) (@rytilahti)
- Add consumables module for vacuums [\#1327](#1327) (@rytilahti)
- Add ADC Value to PIR Enabled Switches [\#1263](#1263) (@ryenitcher)
- Add support for cleaning records [\#945](#945) (@rytilahti)
- Initial support for vacuums \(clean module\) [\#944](#944) (@rytilahti)
- Add support for pairing devices with hubs [\#859](#859) (@rytilahti)

**Fixed bugs:**

- TP-Link HS300 Wi-Fi Power-Strip - "Parent On/Off" not functioning. [\#637](#637)
- Convert carpet\_clean\_mode to carpet\_boost switch [\#1486](#1486) (@rytilahti)
- Change category for empty dustbin feature from Primary to Config [\#1485](#1485) (@rytilahti)
- Report 0 for instead of None for zero current and voltage [\#1483](#1483) (@ryenitcher)
- Disable iot camera creation until more complete [\#1480](#1480) (@sdb9696)
- ssltransport: use debug logger for sending requests [\#1443](#1443) (@rytilahti)
- Fix discover cli command with host [\#1437](#1437) (@sdb9696)
- Fallback to is\_low for batterysensor's battery\_low [\#1420](#1420) (@rytilahti)
- Fix iot strip turn on and off from parent [\#639](#639) (@Obbay2)

**Added support for devices:**

- Add D130\(US\) 1.0 1.1.9 fixture [\#1476](#1476) (@sdb9696)
- Add D100C\(US\) 1.0 1.1.3 fixture [\#1475](#1475) (@sdb9696)
- Add C220\(EU\) 1.0 1.2.2 camera fixture [\#1466](#1466) (@DawidPietrykowski)
- Add D230\(EU\) 1.20 1.1.19 fixture [\#1448](#1448) (@sdb9696)
- Add fixture for C720 camera [\#1433](#1433) (@steveredden)

**Project maintenance:**

- Update ruff to 0.9 [\#1482](#1482) (@sdb9696)
- Cancel in progress CI workflows after new pushes [\#1481](#1481) (@sdb9696)
- Update test framework to support smartcam device discovery. [\#1477](#1477) (@sdb9696)
- Add error code 7 for clean module [\#1474](#1474) (@rytilahti)
- Enable CI workflow on PRs to feat/ fix/ and janitor/ [\#1471](#1471) (@sdb9696)
- Add commit-hook to prettify JSON files [\#1455](#1455) (@rytilahti)
- Add required sphinx.configuration [\#1446](#1446) (@rytilahti)
- Add more redactors for smartcams [\#1439](#1439) (@sdb9696)
- Add KS230\(US\) 2.0 1.0.11 IOT Fixture [\#1430](#1430) (@ZeliardM)
- Add tests for dump\_devinfo parent/child smartcam fixture generation [\#1428](#1428) (@sdb9696)
- Raise errors on single smartcam child requests [\#1427](#1427) (@sdb9696)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants