GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [WIP] Kasa Cam support by bstrdsmkr · Pull Request #537 · python-kasa/python-kasa

Conversation

@bstrdsmkr

@bstrdsmkr bstrdsmkr commented Oct 30, 2023 •
edited by rytilahti
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Supports controlling Kasa Cam devices such as the EC70(US)

Closes #530

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

WingsLikeEagles and UserDerezzed reacted with thumbs up emoji
All reactions
  • 👍 2 reactions
@bstrdsmkr
bstrdsmkr force-pushed the kasacam_support branch 3 times, most recently from f9179ab to b2c78a1 Compare October 31, 2023 16:04
@codecov

codecov Bot commented Oct 31, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 92.48555% with 13 lines in your changes missing coverage. Please review.

Project coverage is 81.35%. Comparing base (805e4b8) to head (e2d8f52).
Report is 693 commits behind head on master.

Files with missing lines Patch % Lines
kasa/smartcameraprotocol.py 83.33% 8 Missing and 3 partials ⚠️
kasa/modules/ptz.py 94.11% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #537      +/-   ##
==========================================
+ Coverage   80.36%   81.35%   +0.99%     
==========================================
  Files          27       30       +3     
  Lines        2093     2274     +181     
  Branches      639      671      +32     
==========================================
+ Hits         1682     1850     +168     
- Misses        366      376      +10     
- Partials       45       48       +3     

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

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@rytilahti rytilahti added the enhancement New feature or request label Oct 31, 2023
rytilahti
rytilahti reviewed Oct 31, 2023
View reviewed changes

@rytilahti rytilahti left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this 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, @bstrdsmkr! I added some initial review comments inline.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread kasa/__init__.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/kasacamprotocol.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/kasacamprotocol.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/modules/module.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/modules/ptz.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/kasacamera.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/kasacamera.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/kasacamprotocol.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/protocol.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/smartdevice.py
def _set_sys_info(self, sys_info: Dict[str, Any]) -> None:
"""Set sys_info."""
self._sys_info = sys_info
self._sys_info = sys_info if "model" in sys_info else sys_info["system"]

@rytilahti rytilahti Oct 31, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Add a quick command why this is done.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@bstrdsmkr bstrdsmkr Nov 6, 2023

Copy link
Copy Markdown
Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

fixed

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@rytilahti rytilahti Nov 7, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I saw you did check earlier if the key system is inside the sysinfo, this probably should do the same for consistency?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@rytilahti rytilahti Nov 18, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

This was buried, so just a friendly ping :-)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@bstrdsmkr
bstrdsmkr force-pushed the kasacam_support branch 2 times, most recently from 2ea2141 to 3a1db0c Compare November 6, 2023 19:51
@bstrdsmkr bstrdsmkr mentioned this pull request Nov 6, 2023
contributing KasaCam support #530
Closed
rytilahti
rytilahti reviewed Nov 7, 2023
View reviewed changes
Comment thread devtools/dump_devinfo.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread devtools/dump_devinfo.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/__init__.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/kasacamera.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/kasacamera.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/modules/ptz.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/smartdevice.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/tests/conftest.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/smartdevice.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread kasa/smartdevice.py
def _set_sys_info(self, sys_info: Dict[str, Any]) -> None:
"""Set sys_info."""
self._sys_info = sys_info
self._sys_info = sys_info if "model" in sys_info else sys_info["system"]

@rytilahti rytilahti Nov 7, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I saw you did check earlier if the key system is inside the sysinfo, this probably should do the same for consistency?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@bstrdsmkr
bstrdsmkr force-pushed the kasacam_support branch 2 times, most recently from 7f83115 to 26ce51a Compare November 7, 2023 19:52
@rytilahti rytilahti mentioned this pull request Nov 8, 2023
Add klap protocol #509
Merged
@bstrdsmkr
bstrdsmkr force-pushed the kasacam_support branch 2 times, most recently from 63f7206 to ba63501 Compare November 9, 2023 01:22
Zach Price
Initial WIP review commit
e2d8f52
@bstrdsmkr
bstrdsmkr force-pushed the kasacam_support branch from ba63501 to e2d8f52 Compare November 9, 2023 14:24
@bstrdsmkr

bstrdsmkr commented Nov 9, 2023

Copy link
Copy Markdown
Author

@rytilahti the ptz stuff is most of what I personally care about for this but I've pulled a

List of known commands from the android app
# smartlife.cam.ipcamera.audio
async def get_mic_config(self):
    # , volume:int
    pass

async def get_quickres_list(self):
    # , files:list, max_files:int
    return await self.call("smartlife.cam.ipcamera.audio", "get_quickres_list", {})

async def set_chime(self, duration: int):
    pass

async def set_mic_config(self, volume: int):
    pass

async def set_quickres_off(self):
    pass

async def set_quickres_state(self, file_id: int, repeat: int):
    pass

# smartlife.cam.ipcamera.battery

async def get_power_save(self):
    pass

async def get_status(self):
    # public Integer battery_capacity;
    # public String battery_charging;
    # public Boolean battery_installed;
    # public String battery_model;
    # public Integer battery_percent;
    # public Integer battery_temperature;
    # public String battery_vendor;
    # public Integer battery_voltage;
    # public Boolean low_battery;
    # public String power_mode;
    pass

async def set_power_save(self):
    pass

# smartlife.cam.ipcamera.siren

async def get_config(self):
    # public Integer duration;
    # public Boolean enable;
    # public Integer volume;
    return await self.call("smartlife.cam.ipcamera.siren", "get_config", {})

async def get_state(self):
    # public Integer time_left;
    # public String value;
    pass

async def set_config(self, duration: int, enable: bool, volume: int):
    pass

async def set_state(self, state: str):
    pass

async def set_state_with_duration(self, state: str, duration: int):
    pass

# smartlife.cam.ipcamera.switch
async def get_is_enable(self):
    pass

async def set_is_enable(self, enabled: bool):
    pass

# smartlife.cam.ipcamera.cloud

async def get_info(self):
    # public Boolean binded;
    # public Boolean cld_connection;
    # public String default_svr;
    # public String fw_dl_page;
    # public Integer fw_notify_type;
    # public Integer illegal_type;
    # public String sef_domain;
    # public Integer stop_connect;
    # public String tcsp_info;
    # public Integer tcsp_status;
    # public String username;
    return await self.call("smartlife.cam.ipcamera.cloud", "get_info", {})

async def set_server_url(self):
    # Deprecated
    pass

async def set_n_server_url(self):
    # public String default_svr;
    # public String ipcserv_svr;
    # public String sef_domain
    pass

async def set_bind(self):
    pass

async def set_unbind(self, username: str, password: str):
    pass

# smartlife.cam.ipcamera.dateTime

async def get_status(self):
    pass

async def get_time(self):
    # public Long epoch_sec;
    pass

async def get_time_zone(self):
    # public String area;
    # public String timezone;
    pass

async def set_time(self, epoch_sec: int, mode: str):
    pass

async def set_time_zone(self, area: str, timezone: str):
    pass

# smartlife.cam.ipcamera.dayNight

async def get_force_lamp_state(self):
    pass

async def get_force_lamp_time(self):
    pass

async def get_lamp_status(self):
    # public Integer forcetime;
    # public String value;
    pass

async def get_mode(self):
    pass

async def get_night_vision_mode(self):
    pass

async def set_force_lamp_state(self):
    pass

async def set_force_lamp_time(self):
    pass

async def set_mode(self):
    pass

async def set_night_vision_mode(self):
    pass

# smartlife.cam.ipcamera.debug

async def set_http_server_switch(self):
    # public String av;
    # public String httpd;
    pass

# smartlife.cam.ipcamera.delivery

async def get_clip_audio_is_enable(self):
    pass

async def get_local_clip_is_enable(self):
    return await self.call(
        "smartlife.cam.ipcamera.delivery", "get_local_clip_is_enable", {}
    )

async def set_clip_audio_is_enable(self):
    pass

async def set_local_clip_is_enable(self):
    pass

# smartlife.cam.ipcamera.dndSchedule

async def add_rule(self):
    pass

async def delete_rule(self):
    pass

async def edit_rule(self):
    pass

async def get_dnd_enable(self):
    pass

async def get_rules(self):
    pass

async def set_dnd_enable(self):
    pass

# smartlife.cam.ipcamera.intelligence

async def get_all_pd_area(self):
    pass

async def get_bcd_enable(self):
    pass

async def get_bcd_sensitivity_level(self):
    pass

async def get_pd_area(self):
    # public List area;
    # public Integer max;
    # public Integer viewpoint
    pass

async def get_pd_enable(self):
    pass

async def get_pd_sensitivity_level(self):
    # public Integer day_mode_level;
    # public Integer night_mode_level
    pass

async def set_bcd_enable(self):
    pass

async def set_bcd_sensitivity_level(self):
    pass

async def set_pd_area(self, area: list, viewpoint: int):
    pass

async def set_pd_enable(self):
    pass

async def set_pd_sensitivity_level(
    self, day_mode_level: int, night_mode_level: int
):
    pass

# smartlife.cam.ipcamera.led

async def get_buttonled_status(self):
    pass

async def set_buttonled_status(self):
    pass

async def set_status(self):
    pass

async def get_status(self):
    pass

# smartlife.cam.ipcamera.motionDetect

async def get_detect_area(self):
    # public List area;
    # public Integer max;
    # public Integer viewpoint;
    pass

async def get_is_enable(self):
    pass

async def get_min_trigger_time(self):
    # public Integer day_mode_value;
    # public Integer night_mode_value;
    pass

async def get_sensitivity(self):
    pass

async def get_sensitivity_level(self):
    # public Integer day_mode_level;
    # public Integer night_mode_level;
    pass

async def set_detect_area(self, area: list, viewpoint: int):
    pass

async def set_is_enable(self):
    pass

async def set_min_trigger_time(self, day_mode_level: int, night_mode_level: int):
    pass

async def set_sensitivity(self):
    pass

async def set_sensitivity_level(self, day_mode_level: int, night_mode_level: int):
    pass

# smartlife.cam.ipcamera.OSD

async def get_logo_is_enable(self):
    pass

async def get_time_is_enable(self):
    pass

async def set_logo_is_enable(self):
    pass

async def set_time_is_enable(self):
    pass

# smartlife.cam.ipcamera.ptz

async def add_preset(self):
    # public String api_srv_url;
    # public Integer index;
    # public String name;
    # public String path;
    # public Integer wait_time;
    pass

async def delete_preset(self, index: int):
    pass

async def edit_preset(self, index: int, name: str, wait_time: int):
    pass

async def get_all_preset(self):
    # public Integer maximum;
    # public List preset_attr;
    pass

async def get_patrol_is_enable(self):
    pass

async def get_position(self):
    # public Integer x;
    # public Integer y;
    pass

async def get_ptz_rectify_state(self):
    pass

async def get_ptz_tracking_is_enable(self):
    pass

async def set_motor_rectify(self):
    pass

async def set_move(self, x: int, y: int):
    pass

async def set_patrol_is_enable(self):
    pass

async def set_ptz_tracking_is_enable(self):
    pass

async def set_run_to_preset(self, index: int):
    pass

async def set_stop(self):
    pass

async def set_target(self, direction: str, speed: int):
    pass

# smartlife.cam.ipcamera.relay

async def get_preview_snapshot(self):
    # public String api_srv_url;
    # public String path;
    # public String timestamp;
    pass

async def set_frame_type(self, frame_type: str):
    pass

async def set_prepare_relay(
    self,
    audio_type: str,
    cookie: str,
    frame_type: str,
    max_video_res: str,
    player_id: str,
    record_preview: int,
    resolution: str,
    start_time: int,
    stream_url: str,
    type: str,
    video_type: str,
):
    pass

# smartlife.cam.ipcamera.rtp

async def set_prepare_rtc_session(self, sessionId: str, speaker_occupied: bool):
    pass

async def set_rtc_session_status(
    self, connected: bool, sessionId: str, speaker_occupied: bool
):
    pass

# smartlife.cam.ipcamera.camSchedule

async def add_rule(self):
    pass

async def delete_all_rules(self):
    pass

async def delete_rule(self, id: str):
    pass

async def edit_rule(self):
    # public String conflict_id;
    # public Integer day;
    # public Integer eact;
    # public Integer emin;
    # public Boolean enable;
    # public Integer etime_opt;
    # public String id;
    # public Integer month;
    # public String name;
    # public Integer repeat;
    # public Integer sact;
    # public Integer smin;
    # public Integer stime_opt;
    # public List wday;
    # public Integer year;
    pass

async def get_rules(self):
    # public Boolean enable;
    # public List rule_list;
    pass

# smartlife.cam.ipcamera.sdCard

async def get_sd_enc_config(self):
    # public String sd_enc_enable;
    # public String user_key_enable;
    pass

async def get_sd_card_state(self):
    # public String detect_state;
    # public String free;
    # public String reserve_capacity;
    # public String sd_capacity;
    # public String state;
    # public String total;
    # public String used;
    pass

async def set_format_sd_card(self):
    pass

async def set_sd_enc_config(
    self, key: str, sd_enc_enable: str, user_key_enable: str
):
    pass

# smartlife.cam.ipcamera.soundDetect

async def get_is_enable(self):
    pass

async def get_sensitivity(self):
    pass

async def set_is_enable(self):
    pass

async def set_sensitivity(self):
    pass

# system

async def get_sysinfo(self):
    # public Integer a_type;
    # public String account_id;
    # public String alias;
    # public String battery_charging;
    # public Integer battery_percent;
    # public List c_opt;
    # public String camera_switch;
    # public String dev_name;
    # public String deviceId;
    # public String dnd_switch;
    # public List f_list;
    # public String hwId;
    # public String hw_ver;
    # public Long last_activity_timestamp;
    # public String mic_mac;
    # public String model;
    # public List new_feature;
    # public String oemId;
    # public Boolean online;
    # public String power;
    # public String resolution;
    # public Integer rssi;
    # public Integer stream_version;
    # public String sw_ver;
    # final LinkieCameraCommand.SysInfo this$0;
    # public String type;
    # public Boolean updating;
    pass

# smartlife.cam.ipcamera.system

async def set_alias(self):
    pass

async def set_change_local_password(self, c_opt: int, passphrase: str):
    pass

async def set_dev_location(self, lattitude: int, longitude: int):
    pass

async def set_onboarding_status(self):
    pass

async def set_reboot(self, delay_sec: int):
    pass

async def set_reset_config(self, isfullreset: bool):
    pass

# smartlife.cam.ipcamera.upgrade

async def set_download_firmware(self, flash_sec: int, reboot_sec: int):
    pass

async def get_download_status(self):
    pass

# smartlife.cam.ipcamera.upnpc

async def get_pub_ip(self):
    pass

async def get_upnp_commstatus(self):
    pass

async def get_upnp_info(self):
    return await self.call("smartlife.cam.ipcamera.upnpc", "get_upnp_info", {})

async def get_upnp_status(self):
    pass

async def set_upnp_commstatus(self, comm_status: str, desc: str, timestamp: int):
    pass

async def set_upnp_info(self, enabled: str, mode: str):
    pass

# smartlife.cam.ipcamera.videoControl

async def get_channel_quality(self):
    return await self.call(
        "smartlife.cam.ipcamera.videoControl", "get_channel_quality", {}
    )

async def get_power_frequency(self):
    pass

async def get_resolution(self):
    pass

async def get_rotation_degree(self):
    pass

async def set_channel_quality(self):
    pass

async def set_power_frequency(self):
    pass

async def set_resolution(self):
    pass

async def set_rotation_degree(self):
    pass

# smartlife.cam.ipcamera.vod

async def get_detect_event_state(self):
    pass

async def get_detect_zone_brief(self):
    pass

async def get_detect_zone_list(self):
    pass

async def get_detect_zone_range(self):
    pass

async def get_is_enable(self):
    return await self.call("smartlife.cam.ipcamera.vod", "get_is_enable", {})

async def get_playback_info(self):
    return await self.call("smartlife.cam.ipcamera.vod", "get_playback_info", {})

async def get_record_zone_brief(self):
    pass

async def get_record_zone_list(self):
    pass

async def get_record_zone_range(self):
    pass

async def get_vod_occupied_state(self):
    pass

async def reset_vod_connection(self):
    pass

async def set_download_playback(
    self, player_id: str, start_time: int, user_id: str
):
    pass

async def set_heartbeat_playback(self, player_id: str, user_id: str):
    pass

async def set_is_enable(self):
    pass

async def set_pause_playback(self, player_id: str, user_id: str):
    pass

async def set_start_playback(self, player_id: str, start_time: int, user_id: str):
    pass

# smartlife.cam.ipcamera.wireless

async def get_ap_list(self):
    pass

async def get_connect_status(self):
    pass

async def get_uplink(self):
    pass

async def set_apply_changes(self, delay: int):
    pass

async def set_start_scan(self, band: str, scan_type: str):
    pass

async def set_uplink(
    self,
    apply: bool,
    band: str,
    encryption: str,
    key_index: str,
    passphrase: str,
    ssid: str,
    wpa_mode: str,
):
    pass

I'd like to implement them eventually but don't want that to hold up this PR and likely won't have time for a bit. What are your thoughts on something like a property that returns a list of dicts to list all known commands for a device type? That would be a duct tape solution for folks to be able to call the raw commands and a place for contributors to start.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

bstrdsmkr
bstrdsmkr commented Nov 9, 2023
View reviewed changes
Comment thread kasa/modules/time.py
q = self.query_for_command("get_time")

merge(q, self.query_for_command("get_timezone"))
merge(q, self.query_for_command("get_time_zone"))

@bstrdsmkr bstrdsmkr Nov 9, 2023

Copy link
Copy Markdown
Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

@rytilahti the time module for my camera needs get_time_zone vs get_timezone for all other devices. This merge seems to work, but I'm concerned whether:

  1. This works with all other devices (I only have the camera and a couple of lights)
  2. Is this "The Right Way" to do this?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@rytilahti rytilahti Nov 18, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Maybe it's better to create a separate class that inherits from the Time, that is only used for the camera class? This way we would not send unnecessary requests to devices that do not support this.

edit: Or maybe just a keyword argument to __init__ that takes the name of the method, something like this:

def __init__(self, device: "SmartDevice", module: str, *, query_command="get_timezone"):
    self._query_command = query_command
    super().__init__(device, module)

and initializing it with get_time_zone for the camera.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread kasa/tests/fixtures/EC70(US)_2.0_2.3.14.json

@bstrdsmkr bstrdsmkr Nov 9, 2023

Copy link
Copy Markdown
Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

@rytilahti This was produced by support I added for cameras to the dump_dev_info script but backed it out to a separate, future PR after review discussion. It's not clear to me how much info these fixtures should contain. Should I manually add example responses for the PTZ module commands?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@rytilahti rytilahti Nov 18, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

If you wish to add, that could be helpful for the future. The whole idea behind the fixtures is to have some test responses for commands to hopefully avoid breaking and enabling some development by users (like me) who have just a few devices to test the library against.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@rytilahti

rytilahti commented Nov 18, 2023

Copy link
Copy Markdown
Member

I'd like to implement them eventually but don't want that to hold up this PR and likely won't have time for a bit. What are your thoughts on something like a property that returns a list of dicts to list all known commands for a device type? That would be a duct tape solution for folks to be able to call the raw commands and a place for contributors to start.

It's indeed a good idea to take this in small steps. Instead adding some unused code, how about creating an issue to the issue tracker?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

rytilahti
rytilahti reviewed Nov 18, 2023
View reviewed changes
Comment thread kasa/smartcamera.py
Comment on lines +44 to +45
credentials: Credentials,
*,

@rytilahti rytilahti Nov 18, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Suggested change
credentials: Credentials,
*,
*,
credentials: Credentials,

For consistency, all other classes expect it as kwarg-only.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread kasa/smartcamera.py

Examples:
>>> import asyncio
>>> camera = KasaCam("127.0.0.1")

@rytilahti rytilahti Nov 18, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Old name used here :-) Please add test_camera_examples to kasa/tests/test_readme_examples.py which will catch similar issues in the future.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@WingsLikeEagles WingsLikeEagles Jan 8, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Can you clarify or suggest the fix here?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@rytilahti rytilahti Jan 9, 2024

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

The issue is that the name of the class is SmartCamera but the example uses KasaCam.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@WingsLikeEagles WingsLikeEagles Jan 13, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Hopefullly I'm not to ignorant here, but is it using "KasaCam" because of the TYPE_TO_CLASS in the kasa/cli.py ?

python-kasa/kasa/cli.py

Line 51 in e2d8f52

"kasacam": SmartCamera,

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@WingsLikeEagles WingsLikeEagles Jan 13, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Suggested change
>>> camera = KasaCam("127.0.0.1")
>>> camera = SmartCamera("127.0.0.1")

Is that what you are suggesting?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@rytilahti rytilahti mentioned this pull request Dec 2, 2023
[Feature Request] KD110 support #467
Open
WingsLikeEagles
WingsLikeEagles reviewed Jan 8, 2024
View reviewed changes
Comment thread kasa/smartcamera.py
self,
target: str,
cmd: str,
arg: Optional[Dict] = None,

@WingsLikeEagles WingsLikeEagles Jan 8, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

If the Kasa Cameras requires {} in place of "None", why not just put that as the default? I'm not a super Python expert, so open to being wrong here if I misunderstand.

Suggested change
arg: Optional[Dict] = None,
arg: Optional[Dict] = {},

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@rytilahti rytilahti Jan 9, 2024

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Generally speaking it is a good idea to avoid passing mutable objects as parameters, as they are initialized at the time of declaration. I.e., if the method body would modify the arg somehow, all future calls would default to having that modified dictionary as input.

In this case it does not make any difference, but given this function overloads the base class one, it should keep using the same signature.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
WingsLikeEagles
WingsLikeEagles reviewed Jan 8, 2024
View reviewed changes
Comment thread kasa/tests/conftest.py

for d in CAMERAS:
if d in model:
return SmartCamera

@WingsLikeEagles WingsLikeEagles Jan 8, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Suggested change
return SmartCamera
return SmartCamera

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@WingsLikeEagles WingsLikeEagles Jan 8, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Adding blank line back in for consistency.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
WingsLikeEagles
WingsLikeEagles reviewed Jan 8, 2024
View reviewed changes
Comment thread kasa/tests/test_discovery.py

x = await Discover.discover_single(host, port=custom_port)
x = await Discover.discover_single(
host, port=custom_port, credentials=Credentials("user", "pass")

@WingsLikeEagles WingsLikeEagles Jan 8, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Suggested change
host, port=custom_port, credentials=Credentials("user", "pass")
host,
port=custom_port,
credentials=Credentials("user", "pass")

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@WingsLikeEagles WingsLikeEagles Jan 13, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I'm not sure if this is applicable for a test, but the default username/password for the EC70 appears to be admin/admin. But the password is base64 encoded. Which I believe works out to YWRtaW4=.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@WingsLikeEagles WingsLikeEagles Jan 13, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

This can be tested with

curl -vv -k -u admin:YWRtaW4= "https://192.168.0.10:19443/https/stream/mixed?video=h264&audio=g711&resolution=hd"

or

curl -vv -k -u admin:$(echo -n "admin" | base64) "https://192.168.0.10:19443/https/stream/mixed?video=h264&audio=g711&resolution=hd"

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
WingsLikeEagles
WingsLikeEagles reviewed Jan 8, 2024
View reviewed changes
Comment thread kasa/tests/test_discovery.py

dev = await Discover.connect_single(host, port=custom_port)
dev = await Discover.connect_single(
host, port=custom_port, credentials=Credentials("user", "pass")

@WingsLikeEagles WingsLikeEagles Jan 8, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Suggested change
host, port=custom_port, credentials=Credentials("user", "pass")
host,
port=custom_port,
credentials=Credentials("user", "pass")

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
rytilahti
rytilahti reviewed Jan 9, 2024
View reviewed changes

@rytilahti rytilahti left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Current git master has separated transport from communication protocols, so this PR requires rebasing.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread kasa/smartcamera.py

Examples:
>>> import asyncio
>>> camera = KasaCam("127.0.0.1")

@rytilahti rytilahti Jan 9, 2024

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

The issue is that the name of the class is SmartCamera but the example uses KasaCam.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread kasa/smartcamera.py
self,
target: str,
cmd: str,
arg: Optional[Dict] = None,

@rytilahti rytilahti Jan 9, 2024

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Generally speaking it is a good idea to avoid passing mutable objects as parameters, as they are initialized at the time of declaration. I.e., if the method body would modify the arg somehow, all future calls would default to having that modified dictionary as input.

In this case it does not make any difference, but given this function overloads the base class one, it should keep using the same signature.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@sdb9696

sdb9696 commented Jan 10, 2024

Copy link
Copy Markdown
Contributor

Current git master has separated transport from communication protocols, so this PR requires rebasing.

To help here, if I understand the SmartCameraProtocol correctly I think what you will need to do is implement a transport called XorHttpTransport which will implement send (the code you currently have in _connect and _execute_query. I think you should then be able to re-use the IotProtocol with this new transport and all the retry logic and httpx error handling will be done for you.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@rytilahti

rytilahti commented Jan 11, 2024

Copy link
Copy Markdown
Member

It is also necessary to convert this to use aiohttp instead of httpx for performance reasons, see #635.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

WingsLikeEagles
WingsLikeEagles reviewed Jan 13, 2024
View reviewed changes
Comment thread kasa/cli.py
"bulb": SmartBulb,
"dimmer": SmartDimmer,
"strip": SmartStrip,
"kasacam": SmartCamera,

@WingsLikeEagles WingsLikeEagles Jan 13, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Suggested change
"kasacam": SmartCamera,
"camera": SmartCamera,

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@WingsLikeEagles

WingsLikeEagles commented Jan 13, 2024

Copy link
Copy Markdown

I have an EC70, and I'd really like to get it working with this. @bstrdsmkr hoping you have some time to wrap this up. Excited to see it included.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@WingsLikeEagles

WingsLikeEagles commented Jan 13, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

So, not sure how much this helps, but the default user name and password for the EC70 is admin:admin. But the password must be base64 encoded when sent.

From this Reddit: https://www.reddit.com/r/homeautomation/comments/sej7vi/comment/iee9t0s/
They were able to get a stream using port 19443. They used their TP Link cloud user and password.

curl -vv -k -u [email protected]:$(echo -n "YOUR_TP_LINK_PASSWORD_HERE" | base64) "https://xxx.xxx.xxx.xxx:19443/https/stream/mixed?video=h264&audio=g711&resolution=hd" --ignore-content-length --output - | cvlc stream:///dev/stdin --sout '#rtp{access=udp,sdp=rtsp://:8554/stream}' :demux=h264

I have verified authentication with

curl -vv -k -u admin:$(echo -n "admin" | base64) "https://xxx.xxx.xxx.xxx:19443/https/stream/mixed?video=h264&audio=g711&resolution=hd"

Actually, base64('admin') is just YWRtaW4=, and the default IP address when you first turn it on is 192.168.0.10. So what I used was:

curl -vv -k -u admin:YWRtaW4= "https://192.168.0.10:19443/https/stream/mixed?video=h264&audio=g711&resolution=hd"

This returns a self signed SSL certificate.

Hope this may be of some help for the discovery portion.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@WingsLikeEagles

WingsLikeEagles commented Jan 25, 2024

Copy link
Copy Markdown

@bstrdsmkr just pinging to see if you are still interested in this. I'd like to see this implemented, but it's over my head. Plus, I'm probably going to be forced off GitHub due to their MFA requirements tomorrow.
https://github.com/WingsLikeEagles/No2FA

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sdb9696

sdb9696 commented Jan 30, 2024

Copy link
Copy Markdown
Contributor

Hi @bstrdsmkr. It's been a while since you commented on this PR and a lot of core library changes have since gone in to support a broader range of tplink devices. That may have made you think it'll be too much trouble to get this PR working but I'd be happy to help you go through it if you're still interested.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@rytilahti rytilahti mentioned this pull request Jul 17, 2024
Support for EC70 camera #1072
Closed
@rytilahti rytilahti mentioned this pull request Nov 10, 2024
IPCamera EC60 (US) not discovered #1241
Closed
@Puxtril Puxtril mentioned this pull request Nov 18, 2024
Add LinkieTransportV2 and basic IOT.IPCAMERA support #1270
Merged
@github-actions

github-actions Bot commented Mar 7, 2025

Copy link
Copy Markdown

There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want to keep it open. Also, please rebase your PR onto the latest dev branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot added the stale label Mar 7, 2025
@github-actions github-actions Bot closed this Mar 15, 2025
@mariannefeng

mariannefeng commented Mar 27, 2026

Copy link
Copy Markdown

Thanks to everybody that worked on this PR!!!

I'm sad it didn't get into the python-kasa repo, but the code in here was critical in helping me figure out how to turn on/off kasa cameras (I have a EC70) in golang. I use it in my go-home repo here (https://github.com/mariannefeng/go-home/blob/main/kasa.go). Relevant code:

const KasaCamHTTPSPort = 10443
const KasaCamHTTPSPath = "/data/LINKIE.json"

// responsible for sending commands to the Kasa camera
func sendKasaCamHTTPSCommand(ip, plaintextRequest string) error {
	encryptedCmd := kasa.Scramble(plaintextRequest)
	b64Cmd := base64.StdEncoding.EncodeToString(encryptedCmd)
	urlSafeCmd := url.QueryEscape(b64Cmd)

	form := "content=" + urlSafeCmd

	httpsURL := fmt.Sprintf("https://%s:%d%s", ip, KasaCamHTTPSPort, KasaCamHTTPSPath)

	tr := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
			CipherSuites: []uint16{
				tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
				tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
			},
		},
	}
	client := &http.Client{
		Timeout:   5 * time.Second,
		Transport: tr,
	}

	req, err := http.NewRequest(http.MethodPost, httpsURL, strings.NewReader(form))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	if user := os.Getenv("KASA_USERNAME"); user != "" {
		pass := os.Getenv("KASA_PASSWORD")
		if pass != "" {
			b64Pass := base64.StdEncoding.EncodeToString([]byte(pass))
			req.SetBasicAuth(user, b64Pass)
		}
	}

	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Drain body to allow connection reuse.
	_, _ = io.Copy(io.Discard, resp.Body)

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("camera https post failed: status=%s", resp.Status)
	}

	return nil
}

This is the command to enable Kasa cam:

const cmdSetCameraEnableOn = `{"smartlife.cam.ipcamera.switch":{"set_is_enable":{"value":"on"}}}

Usage:

if err := sendKasaCamHTTPSCommand(KASA_CAM_IP, cmdSetCameraEnableOn); err != nil {
   return err
}

If your camera is already registered with the app like mine was, don't forget to set env vars KASA_USERNAME and KASA_PASSWORD to your Kasa login creds.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@rytilahti rytilahti rytilahti left review comments
@sdb9696 sdb9696 sdb9696 left review comments
+1 more reviewer
@WingsLikeEagles WingsLikeEagles WingsLikeEagles left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

enhancement New feature or request stale

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

contributing KasaCam support

Uh oh!

There was an error while loading. Please reload this page.

5 participants

@bstrdsmkr @rytilahti @sdb9696 @WingsLikeEagles @mariannefeng
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.