feat(core): add S200B/S200D button and dial event support
released
## Summary
- Adds `EventEntity` support for Tapo S200B button and S200D dial/button hub children
- Button event entity fires `single_click` events; dial event entity fires `rotation` events with `degrees` attribute
- Includes adaptive polling with a budget-based utilization algorithm (EWMA latency + jitter tracking + hysteresis) and a configurable Poll Utilization slider (5–50%)
- Shared event log cache reduces hub API calls from 3 to 1 per polling cycle
- Trigger button devices default to 1s polling (vs 30s for other hub children)
## Why polling?
A push-based pattern (e.g. Matter event subscriptions) would be ideal, but the S200B/S200D don't support it yet. TP-Link's [product page](https://www.tp-link.com/us/home-networking/smart-switch/tapo-s200d/) advertises Matter compatibility, and their [Matter FAQ](https://www.tp-link.com/us/support/faq/3754/) indicates button support via Matter bridging is planned but not yet available. When that lands, this polling approach can be replaced. Until then, the adaptive polling algorithm keeps it efficient — only the hub is polled (not the buttons), so there's zero battery impact, and hub load stays within a configurable budget.
## Important notes
- **Double-click is not supported.** The S200D hub does not emit native `doubleClick` events — double-clicks are reported as two rapid `SingleClickEvent`s and cannot be reliably distinguished from two intentional single clicks.
- The `plugp100` library already parses `doubleClick` if the hub ever sends it ([see code](https://github.com/petretiandrea/plugp100/blob/main/plugp100/responses/hub_childs/s200b_device_state.py)), so if TP-Link adds firmware support in the future, it will work automatically.
## New entities (per S200B/S200D device)
| Entity | Type | Description |
|--------|------|-------------|
| Button Event | `event` | Fires on button press (`single_click`) |
| Dial Event | `event` | Fires on dial rotation (`rotation` with `degrees`) |
| Poll Latency | `sensor` (diagnostic) | Measures hub API round-trip time in ms |
| Poll Utilization | `number` (config) | Slider to tune adaptive polling aggressiveness (default 35%) |
## Example blueprints
Ready-to-use blueprints for button toggle and dial brightness control: [import from gist](https://gist.github.com/erosen14/8e427e9da6351abf8554cfc12bea421d)
## Adaptive polling algorithm
The integration needs to poll the hub's event log to detect button presses and dial rotations. Polling too fast wastes hub resources, too slow and the dial feels laggy. Instead of a fixed interval, the algorithm measures how long each API call actually takes and adjusts accordingly.
Each cycle, it tracks two things:
- **Latency (L)**: how long the hub took to respond, smoothed with an exponential moving average so one slow response doesn't cause overreaction
- **Jitter (J)**: how much that latency varies from cycle to cycle, also smoothed
The polling interval is then computed as: `interval = (L + 2×J) / u_max`, where `u_max` is the utilization target (the Poll Utilization slider, default 35%). This means the system spends at most 35% of its time actively talking to the hub, leaving the rest as idle headroom.
To avoid the interval bouncing around on small latency fluctuations, there's a 15% hysteresis dead band — the interval only changes when the new computed value differs from the current one by more than 15%. A 5-cycle cooldown after each change prevents rapid back-and-forth adjustments.
The Poll Utilization slider (exposed as a config entity in HA) lets users tune the tradeoff between responsiveness and hub load. At 50% the dial feels very responsive but the hub is working harder. At 10% there's more latency but the hub barely notices the polling. 35% is a reasonable middle ground.
### Scaling and hub safety
With multiple devices on the same hub, a per-hub lock serializes API requests so they don't overlap and cause timeouts. The hub only ever handles one request at a time, so it can't be overwhelmed with concurrent connections regardless of how many buttons are configured. A 200ms shared cache also means that when multiple device coordinators poll within the same window, only one actual HTTP request hits the hub.
As latency increases with more devices, the adaptive algorithm automatically widens polling intervals to compensate — this is self-correcting. The hub will never crash or become unresponsive because the algorithm backs off proportionally to observed load. Other Tapo devices on the same hub (sensors, plugs, etc.) use the standard HA coordinator with 30s polling intervals. Since button polling runs on its own async loop and the lock only gates event log fetches, there's always idle time between polls for other devices to get their requests through.
Tested with 1 and 2 devices:
| Devices | Per-device latency | Button response | Dial feel |
|---------|-------------------|-----------------|-----------|
| 1 (measured) | ~50-60ms | <200ms | Smooth |
| 2 (measured, 24hr) | median 96ms, p99 327ms | <200ms | Smooth |
| 3-4 (projected) | ~100-150ms | <300ms | Usable |
| 5-6 (projected) | ~150-250ms | <400ms | Laggy for dial, fine for button |
| 8+ (projected) | ~250ms+ | <1s | Too slow for dial, still OK for button |
Buttons are tolerant of higher latency — a 400ms response is fine for toggling a light. Dials are more sensitive since users expect continuous feedback while turning. For setups with many buttons and few dials, lowering the utilization slider to 10-15% keeps hub load minimal while still providing acceptable button response.
## Test plan
- [x] Add S200B or S200D to a Tapo H100 hub
- [x] Verify button and dial event entities appear in HA
- [x] Create automations using the event entities (e.g., button toggle, dial brightness)
- [x] Verify Poll Latency sensor reports values and adaptive polling stabilizes
- [x] Adjust Poll Utilization slider and confirm polling interval changes accordingly
- [x] Verify no double-click events are emitted (expected: two single_click events)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
合并状态:未合并 关闭于 2026-03-10 2 条评论