Skip to content

Conversation

@mrwogu
Copy link

@mrwogu mrwogu commented Nov 27, 2025

Add preset support for Tapo cameras (firmware 1.4.7+)

Description

This PR adds support for PTZ presets for Tapo cameras running newer firmware versions (e.g., C210 with firmware 1.4.7).

Problem

Newer firmware versions for Tapo cameras (like 1.4.7 for C210) have removed support for the generic motor module commands (get and do on motor preset), returning error code -40210 (Function not supported). This broke existing integrations that relied on these commands for preset management.

Solution

This PR implements the new API methods required by these firmware versions:

  • getPresetConfig: To retrieve the list of presets.
  • motorMoveToPreset: To move the camera to a specific preset.
  • addMotorPostion: To save a new preset (note: the typo in Postion is part of the official API).

The following methods have been added to the PanTilt module:

  • get_presets()
  • goto_preset(preset_id)
  • save_preset(name)

Testing

Tested locally with a Tapo C210 running firmware 1.4.7.

  • get_presets() correctly returns the list of presets.
  • goto_preset() successfully moves the camera.
  • save_preset() successfully saves a new preset.

@mrwogu mrwogu force-pushed the fix/tapo-c210-presets branch from e75e186 to c7fe2f9 Compare November 27, 2025 23:15
@codecov
Copy link

codecov bot commented Nov 27, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.84%. Comparing base (347bf9a) to head (73503d6).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1615      +/-   ##
==========================================
+ Coverage   92.82%   92.84%   +0.02%     
==========================================
  Files         157      157              
  Lines        9643     9678      +35     
  Branches      976      980       +4     
==========================================
+ Hits         8951     8986      +35     
  Misses        492      492              
  Partials      200      200              

☔ 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.

@mrwogu mrwogu force-pushed the fix/tapo-c210-presets branch 5 times, most recently from bd856bf to 20e8f10 Compare November 27, 2025 23:46
@mrwogu mrwogu force-pushed the fix/tapo-c210-presets branch from 20e8f10 to 43e77ce Compare November 27, 2025 23:48
Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

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

Thanks for the PR @mrwogu! This looks good to me on the surface, and it is ready to go after the tests are improved a bit.

I would suggest exposing this also through the feature interface as that makes it much easier to use in downstream (i.e., in the cli tool and in homeassistant), but I leave it for you to decide.

@mrwogu mrwogu force-pushed the fix/tapo-c210-presets branch from ee98828 to c5897ac Compare November 29, 2025 21:06
@mrwogu mrwogu force-pushed the fix/tapo-c210-presets branch 5 times, most recently from cfc865d to dc69f12 Compare November 30, 2025 09:57
@mrwogu mrwogu force-pushed the fix/tapo-c210-presets branch from dc69f12 to 1eee003 Compare November 30, 2025 10:15
@mrwogu
Copy link
Author

mrwogu commented Nov 30, 2025

Thanks for the suggestions, @rytilahti ! All changes have been implemented.

Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

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

Thanks for the updates, looks much better! I added some more comments, mostly about making it cleaner and more consistent with the rest of our code base.

Comment on lines 94 to 96
presets_response = await self._device._query_helper(
"getPresetConfig", {"preset": {"name": ["preset"]}}
)
Copy link
Member

Choose a reason for hiding this comment

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

There is no need to manually query here, the query() will be used automatically.

You can then use the data property to access the module data like this:

@property
def alarm_sound(self) -> Annotated[str, FeatureAttribute()]:
"""Return current alarm sound."""
return self.data["getSirenConfig"]["siren_type"]

Comment on lines 107 to 124
if self._presets and "preset" not in self._module_features:

async def set_preset(preset_name: str) -> None:
preset_id = self._presets.get(preset_name)
if preset_id:
await self.goto_preset(preset_id)

feature = Feature(
self._device,
"preset",
"Preset position",
container=self,
attribute_getter=lambda x: next(iter(self._presets.keys()), None),
attribute_setter=set_preset,
choices_getter=lambda: list(self._presets.keys()),
type=Feature.Type.Choice,
)
self._add_feature(feature)
Copy link
Member

Choose a reason for hiding this comment

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

Please move this into _initialize_features().

There is no need to check if the feature already exists, as it will be called only once after the first update (and it's _post_update_hook), so you can keep updating the internal _presets in the post update hook.


feature = Feature(
self._device,
"preset",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"preset",
"ptz_preset",

Perhaps? There are other devices that have presets (e.g., lights), so it would be a good idea to avoid potential naming conflict if support for other presets is added at some point.

assert preset_feature.value == first_preset_name

# Mock the protocol query for testing set_value
# This allows set_preset function body to be executed (lines 110-112)
Copy link
Member

Choose a reason for hiding this comment

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

Avoid naming lines as the code will live and these won't match anymore at some point.

call_args = mock_protocol_query.call_args
assert "motor" in str(call_args) or "preset" in str(call_args).lower()

# Reset mock
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Reset mock

No need for obvious comments, please check other similar comments in the tests, and ask yourself if they bring extra value for the reader :-)

Comment on lines 68 to 69
if pantilt is None:
pytest.skip("Device does not have PanTilt module")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if pantilt is None:
pytest.skip("Device does not have PanTilt module")

Instead of doing this check for every separate test, it would be cleaner to define a new parametrization and then use it instead of @device_smartcam, for inspiration see:

childsetup = parametrize(
"supports pairing", component_filter="childQuickSetup", protocol_filter={"SMARTCAM"}
)
@childsetup
async def test_childsetup_features(dev: Device):

Comment on lines 109 to 112
try:
await preset_feature.set_value(invalid_preset_name)
# goto_preset should NOT be called because preset_id is empty string (falsy)
mock_query.assert_not_called()
Copy link
Member

Choose a reason for hiding this comment

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

Trying to set an invalid value should raise a ValueError, right? You could use with pytest.raises(ValueError) to make sure that is happening.

@rytilahti rytilahti added the enhancement New feature or request label Nov 30, 2025
@rytilahti rytilahti changed the title Add preset support for Tapo cameras (firmware 1.4.7+) Add preset support for Tapo cameras Nov 30, 2025
@mrwogu mrwogu force-pushed the fix/tapo-c210-presets branch from 145a6fa to 73503d6 Compare December 1, 2025 12:19
@mrwogu
Copy link
Author

mrwogu commented Dec 1, 2025

Thanks again for your valuable feedback, @rytilahti. They helped me understand exactly how this should work. I refactored it. It should be fine now.

@mrwogu mrwogu requested a review from rytilahti December 1, 2025 23:13
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