diff --git a/README.md b/README.md index 585b8f47..e1d5c198 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ The following devices have been tested and confirmed as working. If your device - **Wall Switches**: S210, S220, S500, S500D, S505, S505D, TS15 - **Bulbs**: L430C, L430P, L510B, L510E, L530B, L530E, L535E, L630 - **Light Strips**: L900-10, L900-5, L920-5, L930-5 -- **Cameras**: C100, C110, C210, C220, C225, C325WB, C520WS, C720, TC65, TC70 +- **Cameras**: C100, C110, C210, C220, C225, C325WB, C460, C520WS, C720, TC65, TC70 - **Doorbells and chimes**: D100C, D130, D230 - **Vacuums**: RV20 Max Plus, RV30 Max - **Hubs**: H100, H200 diff --git a/SUPPORTED.md b/SUPPORTED.md index 66697138..b464aad1 100644 --- a/SUPPORTED.md +++ b/SUPPORTED.md @@ -314,6 +314,8 @@ All Tapo devices require authentication.
Hub-Connected Devices may work acros - Hardware: 2.0 (US) / Firmware: 1.0.11 - **C325WB** - Hardware: 1.0 (EU) / Firmware: 1.1.17 +- **C460** + - Hardware: 1.0 (CA) / Firmware: 1.2.0 - **C520WS** - Hardware: 1.0 (US) / Firmware: 1.2.8 - **C720** diff --git a/kasa/smartcam/modules/battery.py b/kasa/smartcam/modules/battery.py index d6bd97f3..b1e25f25 100644 --- a/kasa/smartcam/modules/battery.py +++ b/kasa/smartcam/modules/battery.py @@ -1,8 +1,9 @@ -"""Implementation of baby cry detection module.""" +"""Implementation of smartcam battery module.""" from __future__ import annotations import logging +from typing import Any from ...feature import Feature from ..smartcammodule import SmartCamModule @@ -44,32 +45,37 @@ class Battery(SmartCamModule): ) ) - self._add_feature( - Feature( - self._device, - "battery_temperature", - "Battery temperature", - container=self, - attribute_getter="battery_temperature", - icon="mdi:battery", - unit_getter=lambda: "celsius", - category=Feature.Category.Debug, - type=Feature.Type.Sensor, + # Optional on some battery cameras (e.g., C460). + if self._optional_float_sysinfo("battery_temperature") is not None: + self._add_feature( + Feature( + self._device, + "battery_temperature", + "Battery temperature", + container=self, + attribute_getter="battery_temperature", + icon="mdi:battery", + unit_getter=lambda: "celsius", + category=Feature.Category.Debug, + type=Feature.Type.Sensor, + ) ) - ) - self._add_feature( - Feature( - self._device, - "battery_voltage", - "Battery voltage", - container=self, - attribute_getter="battery_voltage", - icon="mdi:battery", - unit_getter=lambda: "V", - category=Feature.Category.Debug, - type=Feature.Type.Sensor, + + if self._optional_float_sysinfo("battery_voltage") is not None: + self._add_feature( + Feature( + self._device, + "battery_voltage", + "Battery voltage", + container=self, + attribute_getter="battery_voltage", + icon="mdi:battery", + unit_getter=lambda: "V", + category=Feature.Category.Debug, + type=Feature.Type.Sensor, + ) ) - ) + self._add_feature( Feature( self._device, @@ -83,6 +89,18 @@ class Battery(SmartCamModule): ) ) + def _optional_float_sysinfo(self, key: str) -> float | None: + """Return sys_info[key] as float, or None if not available or invalid.""" + v_any: Any = self._device.sys_info.get(key) + if v_any in (None, "NO"): + return None + + try: + # Accept ints/floats and numeric strings. + return float(v_any) + except (TypeError, ValueError): + return None + def query(self) -> dict: """Query to execute during the update cycle.""" return {} @@ -98,16 +116,22 @@ class Battery(SmartCamModule): return self._device.sys_info["low_battery"] @property - def battery_temperature(self) -> bool: - """Return battery voltage in C.""" - return self._device.sys_info["battery_temperature"] + def battery_temperature(self) -> float | None: + """Return battery temperature in °C (if available).""" + return self._optional_float_sysinfo("battery_temperature") @property - def battery_voltage(self) -> bool: - """Return battery voltage in V.""" - return self._device.sys_info["battery_voltage"] / 1_000 + def battery_voltage(self) -> float | None: + """Return battery voltage in V (if available).""" + v = self._optional_float_sysinfo("battery_voltage") + return None if v is None else v / 1_000 @property def battery_charging(self) -> bool: """Return True if battery is charging.""" - return self._device.sys_info["battery_voltage"] != "NO" + v = self._device.sys_info.get("battery_charging") + if isinstance(v, bool): + return v + if v is None: + return False + return str(v).strip().lower() in ("yes", "true", "1", "charging", "on") diff --git a/tests/fixtures/smartcam/C460(CA)_1.0_1.2.0.json b/tests/fixtures/smartcam/C460(CA)_1.0_1.2.0.json new file mode 100644 index 00000000..be5456b7 --- /dev/null +++ b/tests/fixtures/smartcam/C460(CA)_1.0_1.2.0.json @@ -0,0 +1,1056 @@ +{ + "discovery_result": { + "error_code": 0, + "result": { + "decrypted_data": { + "connect_ssid": "#MASKED_SSID#", + "connect_type": "wireless", + "device_id": "0000000000000000000000000000000000000000", + "http_port": 443, + "last_alarm_time": "1734967724", + "last_alarm_type": "motion", + "owner": "00000000000000000000000000000000", + "sd_status": "offline" + }, + "device_id": "00000000000000000000000000000000", + "device_model": "C460", + "device_name": "#MASKED_NAME#", + "device_type": "SMART.IPCAMERA", + "encrypt_info": { + "data": "", + "key": "", + "sym_schm": "AES" + }, + "encrypt_type": [ + "3" + ], + "factory_default": false, + "firmware_version": "1.2.0 Build 250910 Rel.70120n", + "hardware_version": "1.0", + "ip": "127.0.0.123", + "is_support_iot_cloud": true, + "mac": "40-AE-30-00-00-00", + "mgt_encrypt_schm": { + "is_support_https": true + } + } + }, + "getAlertConfig": { + "msg_alarm": { + "capability": { + "alarm_duration_support": "1", + "alarm_func": [ + "sound", + "light" + ], + "alarm_volume_support": "1", + "alert_event_type_support": "1", + "usr_def_audio_alarm_max_num": "5", + "usr_def_audio_alarm_support": "1", + "usr_def_audio_max_duration": "15", + "usr_def_audio_type": "0", + "usr_def_start_file_id": "8195" + }, + "chn1_msg_alarm_info": { + "alarm_duration": "0", + "alarm_mode": [ + "sound", + "light" + ], + "alarm_type": "0", + "alarm_volume": "high", + "enabled": "off", + "light_alarm_enabled": "on", + "light_type": "1", + "sound_alarm_enabled": "on" + }, + "usr_def_audio": [] + } + }, + "getAlertPlan": { + "msg_alarm_plan": { + "chn1_msg_alarm_plan": { + "alarm_plan_1": "0000-0000,127", + "enabled": "off" + } + } + }, + "getAlertTypeList": { + "msg_alarm": { + "alert_type": { + "alert_type_list": [ + "Siren", + "Emergency", + "Red Alert" + ] + } + } + }, + "getAppComponentList": { + "app_component": { + "app_component_list": [ + { + "name": "sdCard", + "version": 1 + }, + { + "name": "timezone", + "version": 1 + }, + { + "name": "system", + "version": 4 + }, + { + "name": "led", + "version": 1 + }, + { + "name": "playback", + "version": 6 + }, + { + "name": "detection", + "version": 3 + }, + { + "name": "alert", + "version": 2 + }, + { + "name": "firmware", + "version": 2 + }, + { + "name": "quickSetup", + "version": 1 + }, + { + "name": "video", + "version": 3 + }, + { + "name": "imageStyle", + "version": 3 + }, + { + "name": "lensMask", + "version": 2 + }, + { + "name": "lightFrequency", + "version": 3 + }, + { + "name": "dayNightMode", + "version": 2 + }, + { + "name": "osd", + "version": 2 + }, + { + "name": "record", + "version": 1 + }, + { + "name": "audio", + "version": 3 + }, + { + "name": "diagnose", + "version": 1 + }, + { + "name": "msgPush", + "version": 3 + }, + { + "name": "deviceShare", + "version": 1 + }, + { + "name": "tapoCare", + "version": 1 + }, + { + "name": "nightVisionMode", + "version": 3 + }, + { + "name": "iotCloud", + "version": 1 + }, + { + "name": "needSubscriptionServiceList", + "version": 1 + }, + { + "name": "blockZone", + "version": 1 + }, + { + "name": "personDetection", + "version": 2 + }, + { + "name": "vehicleDetection", + "version": 1 + }, + { + "name": "petDetection", + "version": 1 + }, + { + "name": "infLamp", + "version": 1 + }, + { + "name": "whiteLamp", + "version": 1 + }, + { + "name": "pir", + "version": 3 + }, + { + "name": "battery", + "version": 3 + }, + { + "name": "clips", + "version": 2 + }, + { + "name": "markerBox", + "version": 1 + }, + { + "name": "streamGrab", + "version": 1 + }, + { + "name": "antiTheft", + "version": 3 + }, + { + "name": "nvmp", + "version": 1 + }, + { + "name": "detectionRegion", + "version": 2 + }, + { + "name": "ldc", + "version": 1 + }, + { + "name": "noHubBatteryCam", + "version": 1 + }, + { + "name": "recordDownload", + "version": 1 + }, + { + "name": "smartAutoExposure", + "version": 1 + }, + { + "name": "snapshot", + "version": 2 + }, + { + "name": "timeFormat", + "version": 1 + }, + { + "name": "aov", + "version": 1 + }, + { + "name": "hubManage", + "version": 1 + }, + { + "name": "aovSupportHub", + "version": 1 + } + ] + } + }, + "getAudioConfig": { + "audio_config": { + "microphone": { + "bitrate": "64", + "channels": "1", + "echo_cancelling": "off", + "encode_type": "G711alaw", + "input_device_type": "MicIn", + "mute": "off", + "noise_cancelling": "on", + "sampling_rate": "8", + "volume": "80" + }, + "speaker": { + "mute": "off", + "output_device_type": "SpeakerOut", + "volume": "100" + } + } + }, + "getCircularRecordingConfig": { + "harddisk_manage": { + "harddisk": { + "loop": "on" + } + } + }, + "getClockStatus": { + "system": { + "clock_status": { + "local_time": "2026-01-12 15:01:13", + "seconds_from_1970": 1768248073 + } + } + }, + "getConnectStatus": { + "onboarding": { + "get_connect_status": { + "status": 0 + } + } + }, + "getConnectionType": { + "link_type": "wifi", + "rssi": "4", + "rssiValue": -26, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + "getDetectionConfig": { + "motion_detection": { + "motion_det": { + "det_sensitivity": "70", + "digital_sensitivity": "60", + "enabled": "off", + "non_vehicle_enabled": "off", + "people_enabled": "off", + "sensitivity": "medium", + "vehicle_enabled": "off" + } + } + }, + "getDeviceInfo": { + "device_info": { + "basic_info": { + "avatar": "Swimming pool", + "barcode": "", + "battery_charging": "NO", + "battery_overheated": false, + "battery_percent": 100, + "channel_plan_code": "", + "dev_id": "0000000000000000000000000000000000000000", + "device_alias": "#MASKED_NAME#", + "device_info": "C460 1.0 IPC", + "device_model": "C460", + "device_name": "#MASKED_NAME#", + "device_type": "SMART.IPCAMERA", + "features": 3, + "ffs": false, + "has_set_location_info": 1, + "hw_desc": "00000000000000000000000000000000", + "hw_id": "00000000000000000000000000000000", + "hw_version": "1.0", + "is_cal": true, + "latitude": 0, + "longitude": 0, + "low_battery": false, + "mac": "3C-78-95-00-00-00", + "manufacturer_name": "TP-Link", + "mobile_access": "0", + "oem_id": "00000000000000000000000000000000", + "power": "BATTERY", + "power_save_mode": "off", + "power_save_status": "off", + "region": "CA", + "sw_version": "1.2.0 Build 250910 Rel.70120n", + "tss": false + } + } + }, + "getFirmwareAutoUpgradeConfig": { + "auto_upgrade": { + "common": { + "enabled": "on", + "random_range": "120", + "time": "03:00" + } + } + }, + "getFirmwareUpdateStatus": { + "cloud_config": { + "upgrade_status": { + "lastUpgradingSuccess": true, + "state": "normal" + } + } + }, + "getLastAlarmInfo": { + "system": { + "last_alarm_info": { + "last_alarm_time": "0", + "last_alarm_type": "" + } + } + }, + "getLdc": { + "image": { + "common": { + "area_compensation": "default", + "auto_exp_antiflicker": "off", + "auto_exp_gain_max": "0", + "backlight": "off", + "chroma": "50", + "contrast": "50", + "dehaze": "off", + "eis": "off", + "exp_gain": "0", + "exp_level": "0", + "exp_type": "auto", + "focus_limited": "10", + "focus_type": "manual", + "high_light_compensation": "off", + "inf_delay": "1", + "inf_end_time": "21600", + "inf_sensitivity": "-1", + "inf_sensitivity_boot": "180", + "inf_sensitivity_day2night": "131", + "inf_sensitivity_night2day": "1674", + "inf_start_time": "64800", + "inf_type": "auto", + "iris_level": "160", + "light_freq_mode": "auto", + "lock_blue_colton": "0", + "lock_blue_gain": "0", + "lock_gb_gain": "0", + "lock_gr_gain": "0", + "lock_green_colton": "0", + "lock_red_colton": "0", + "lock_red_gain": "0", + "lock_source": "local", + "luma": "50", + "saturation": "50", + "sharpness": "50", + "shutter": "1/25", + "smartir": "auto_ir", + "smartir_level": "0", + "smartwtl": "auto_wtl", + "smartwtl_digital_level": "100", + "smartwtl_level": "5", + "style": "original", + "wb_B_gain": "50", + "wb_G_gain": "50", + "wb_R_gain": "50", + "wb_type": "auto", + "wd_gain": "50", + "wide_dynamic": "off", + "wtl_delay": "5", + "wtl_end_time": "21600", + "wtl_sensitivity": "4", + "wtl_sensitivity_boot": "0", + "wtl_sensitivity_day2night": "1400", + "wtl_sensitivity_night2day": "9100", + "wtl_start_time": "64800", + "wtl_type": "auto" + }, + "switch": { + "best_view_distance": "0", + "clear_licence_plate_mode": "off", + "flip_type": "off", + "full_color_min_keep_time": "5", + "full_color_people_enhance": "off", + "image_scene_mode": "normal", + "image_scene_mode_autoday": "normal", + "image_scene_mode_autonight": "normal", + "image_scene_mode_common": "normal", + "image_scene_mode_shedday": "normal", + "image_scene_mode_shednight": "normal", + "ldc": "off", + "night_vision_mode": "inf_night_vision", + "overexposure_people_suppression": "off", + "rotate_type": "off", + "schedule_end_time": "64800", + "schedule_start_time": "21600", + "switch_mode": "common", + "wtl_force_time": "300", + "wtl_intensity_level": "4" + } + } + }, + "getLedStatus": { + "led": { + "config": { + "enabled": "on" + } + } + }, + "getLensMaskConfig": { + "lens_mask": { + "lens_mask_info": { + "enabled": "off" + } + } + }, + "getLightFrequencyInfo": { + "image": { + "common": { + "area_compensation": "default", + "auto_exp_antiflicker": "off", + "auto_exp_gain_max": "0", + "backlight": "off", + "chroma": "50", + "contrast": "50", + "dehaze": "off", + "eis": "off", + "exp_gain": "0", + "exp_level": "0", + "exp_type": "auto", + "focus_limited": "10", + "focus_type": "manual", + "high_light_compensation": "off", + "inf_delay": "1", + "inf_end_time": "21600", + "inf_sensitivity": "-1", + "inf_sensitivity_boot": "180", + "inf_sensitivity_day2night": "131", + "inf_sensitivity_night2day": "1674", + "inf_start_time": "64800", + "inf_type": "auto", + "iris_level": "160", + "light_freq_mode": "auto", + "lock_blue_colton": "0", + "lock_blue_gain": "0", + "lock_gb_gain": "0", + "lock_gr_gain": "0", + "lock_green_colton": "0", + "lock_red_colton": "0", + "lock_red_gain": "0", + "lock_source": "local", + "luma": "50", + "saturation": "50", + "sharpness": "50", + "shutter": "1/25", + "smartir": "auto_ir", + "smartir_level": "0", + "smartwtl": "auto_wtl", + "smartwtl_digital_level": "100", + "smartwtl_level": "5", + "style": "original", + "wb_B_gain": "50", + "wb_G_gain": "50", + "wb_R_gain": "50", + "wb_type": "auto", + "wd_gain": "50", + "wide_dynamic": "off", + "wtl_delay": "5", + "wtl_end_time": "21600", + "wtl_sensitivity": "4", + "wtl_sensitivity_boot": "0", + "wtl_sensitivity_day2night": "1400", + "wtl_sensitivity_night2day": "9100", + "wtl_start_time": "64800", + "wtl_type": "auto" + } + } + }, + "getMediaEncrypt": { + "cet": { + "media_encrypt": { + "enabled": "off" + } + } + }, + "getMsgPushConfig": { + "msg_push": { + "chn1_msg_push_info": { + "notification_enabled": "on", + "rich_notification_enabled": "off" + } + } + }, + "getNightVisionCapability": { + "image_capability": { + "supplement_lamp": { + "night_vision_mode_range": [ + "inf_night_vision", + "wtl_night_vision" + ], + "supplement_lamp_type": [ + "infrared_lamp", + "white_lamp" + ] + } + } + }, + "getNightVisionModeConfig": { + "image": { + "switch": { + "best_view_distance": "0", + "clear_licence_plate_mode": "off", + "flip_type": "off", + "full_color_min_keep_time": "5", + "full_color_people_enhance": "off", + "image_scene_mode": "normal", + "image_scene_mode_autoday": "normal", + "image_scene_mode_autonight": "normal", + "image_scene_mode_common": "normal", + "image_scene_mode_shedday": "normal", + "image_scene_mode_shednight": "normal", + "ldc": "off", + "night_vision_mode": "inf_night_vision", + "overexposure_people_suppression": "off", + "rotate_type": "off", + "schedule_end_time": "64800", + "schedule_start_time": "21600", + "switch_mode": "common", + "wtl_force_time": "300", + "wtl_intensity_level": "4" + } + } + }, + "getPersonDetectionConfig": { + "people_detection": { + "detection": { + "enabled": "on", + "sensitivity": "60" + } + } + }, + "getPetDetectionConfig": { + "pet_detection": { + "detection": { + "enabled": "off", + "sensitivity": "60" + } + } + }, + "getRecordPlan": { + "record_plan": { + "chn1_channel": { + "enabled": "on", + "friday": "[\"0000-2400:2\"]", + "monday": "[\"0000-2400:2\"]", + "saturday": "[\"0000-2400:2\"]", + "sunday": "[\"0000-2400:2\"]", + "thursday": "[\"0000-2400:2\"]", + "tuesday": "[\"0000-2400:2\"]", + "wednesday": "[\"0000-2400:2\"]" + } + } + }, + "getRotationStatus": { + "image": { + "switch": { + "best_view_distance": "0", + "clear_licence_plate_mode": "off", + "flip_type": "off", + "full_color_min_keep_time": "5", + "full_color_people_enhance": "off", + "image_scene_mode": "normal", + "image_scene_mode_autoday": "normal", + "image_scene_mode_autonight": "normal", + "image_scene_mode_common": "normal", + "image_scene_mode_shedday": "normal", + "image_scene_mode_shednight": "normal", + "ldc": "off", + "night_vision_mode": "inf_night_vision", + "overexposure_people_suppression": "off", + "rotate_type": "off", + "schedule_end_time": "64800", + "schedule_start_time": "21600", + "switch_mode": "common", + "wtl_force_time": "300", + "wtl_intensity_level": "4" + } + } + }, + "getSdCardStatus": { + "harddisk_manage": { + "hd_info": [ + { + "hd_info_1": { + "crossline_free_space": "0B", + "crossline_free_space_accurate": "0B", + "crossline_total_space": "0B", + "crossline_total_space_accurate": "0B", + "detect_status": "normal", + "disk_name": "1", + "free_space": "56.7GB", + "free_space_accurate": "60924595724B", + "hardware_security_config": "0", + "loop_record_status": "0", + "msg_push_free_space": "0B", + "msg_push_free_space_accurate": "0B", + "msg_push_total_space": "0B", + "msg_push_total_space_accurate": "0B", + "password": "", + "percent": "100", + "picture_free_space": "0B", + "picture_free_space_accurate": "0B", + "picture_total_space": "0B", + "picture_total_space_accurate": "0B", + "record_duration": "0", + "record_free_duration": "0", + "record_start_time": "1768153168", + "rw_attr": "rw", + "security_status": "2", + "status": "normal", + "total_space": "59.2GB", + "total_space_accurate": "63580504064B", + "type": "local", + "video_free_space": "56.7GB", + "video_free_space_accurate": "60924595724B", + "video_total_space": "56.8GB", + "video_total_space_accurate": "60934848512B", + "write_protect": "0" + } + } + ] + } + }, + "getTimezone": { + "system": { + "basic": { + "timezone": "UTC-05:00", + "timing_mode": "ntp", + "zone_id": "America/New_York" + } + } + }, + "getVehicleDetectionConfig": { + "vehicle_detection": { + "detection": { + "enabled": "off", + "sensitivity": "60" + } + } + }, + "getVideoCapability": { + "video_capability": { + "main": { + "bitrate_types": [ + "vbr" + ], + "bitrates": [ + "1024", + "1800", + "2048", + "2400", + "4096", + "6144" + ], + "codec_switch_support": "0", + "encode_types": [ + "H264", + "H265" + ], + "frame_rates": [ + "65551" + ], + "minor_stream_support": "1", + "qualitys": [ + "1", + "3", + "5" + ], + "resolutions": [ + "3840*2160", + "1920*1080" + ] + } + } + }, + "getVideoQualities": { + "video": { + "main": { + "bitrate": "2400", + "bitrate_type": "vbr", + "codec_switch_trigger": "none", + "default_bitrate": [ + 4096, + 2400 + ], + "encode_type": "H265", + "frame_rate": "65551", + "name": "VideoEncoder_1", + "quality": "5", + "resolution": "3840*2160", + "smart_codec": "off" + } + } + }, + "getWhitelampConfig": { + "image": { + "switch": { + "best_view_distance": "0", + "clear_licence_plate_mode": "off", + "flip_type": "off", + "full_color_min_keep_time": "5", + "full_color_people_enhance": "off", + "image_scene_mode": "normal", + "image_scene_mode_autoday": "normal", + "image_scene_mode_autonight": "normal", + "image_scene_mode_common": "normal", + "image_scene_mode_shedday": "normal", + "image_scene_mode_shednight": "normal", + "ldc": "off", + "night_vision_mode": "inf_night_vision", + "overexposure_people_suppression": "off", + "rotate_type": "off", + "schedule_end_time": "64800", + "schedule_start_time": "21600", + "switch_mode": "common", + "wtl_force_time": "300", + "wtl_intensity_level": "4" + } + } + }, + "getWhitelampStatus": { + "rest_time": 0, + "status": 0 + }, + "get_audio_capability": { + "get": { + "audio_capability": { + "device_microphone": { + "aec": "1", + "channels": "1", + "echo_cancelling": "0", + "encode_type": [ + "G711alaw" + ], + "half_duplex": "1", + "mute": "1", + "noise_cancelling": "1", + "sampling_rate": [ + "8" + ], + "volume": "1" + }, + "device_speaker": { + "channels": "1", + "decode_type": [ + "G711alaw" + ], + "mute": "0", + "output_device_type": "0", + "sampling_rate": [ + "8" + ], + "system_volume": "100", + "volume": "1" + } + } + } + }, + "get_audio_config": { + "get": { + "audio_config": { + "microphone": { + "bitrate": "64", + "channels": "1", + "echo_cancelling": "off", + "encode_type": "G711alaw", + "input_device_type": "MicIn", + "mute": "off", + "noise_cancelling": "on", + "sampling_rate": "8", + "volume": "80" + }, + "speaker": { + "mute": "off", + "output_device_type": "SpeakerOut", + "volume": "100" + } + } + } + }, + "get_cet": { + "get": { + "cet": { + "vhttpd": { + "port": "8800" + } + } + } + }, + "get_function": { + "get": { + "function": { + "module_spec": { + "ae_weighting_table_resolution": "5*5", + "ai_enhance_capability": "1", + "ai_enhance_range": [ + "traditional_enhance" + ], + "ai_firmware_upgrade": "0", + "alarm_out_num": "0", + "app_version": "1.0.0", + "audio": [ + "speaker", + "microphone" + ], + "auth_encrypt": "1", + "auto_ip_configurable": "1", + "backlight_coexistence": "1", + "change_password": "1", + "client_info": "1", + "cloud_storage_version": "1.0", + "config_recovery": [ + "audio_config", + "OSD", + "image", + "video" + ], + "custom_area_compensation": "1", + "custom_auto_mode_exposure_level": "1", + "daynight_subdivision": "1", + "device_share": [ + "preview", + "playback", + "voice", + "cloud_storage" + ], + "download": [ + "video" + ], + "events": [ + "motion", + "tamper" + ], + "force_iframe_support": "1", + "http_system_state_audio_support": "1", + "image_capability": "1", + "image_list": [ + "supplement_lamp", + "expose" + ], + "image_style_capability": "1", + "ir_led_pwm_control": "1", + "led": "1", + "lens_mask": "1", + "linkage_capability": "1", + "local_storage": "1", + "media_encrypt": "1", + "motor": "0", + "msg_alarm_list": [ + "sound", + "light" + ], + "msg_push": "1", + "multi_user": "0", + "multicast": "0", + "network": [ + "wifi", + "ethernet" + ], + "osd_capability": "1", + "ota_upgrade": "1", + "p2p_support_versions": [ + "2.0" + ], + "personalized_audio_alarm": "0", + "playback": [ + "local", + "p2p", + "relay" + ], + "playback_scale": "1", + "preview": [ + "local", + "p2p", + "relay" + ], + "ptz": "0", + "record_max_slot_cnt": "6", + "record_type": [ + "timing", + "motion" + ], + "relay_support_versions": [ + "2.0" + ], + "remote_upgrade": "1", + "reonboarding": "0", + "smart_codec": "0", + "smart_detection": "1", + "ssl_cer_version": "1.0", + "storage_api_version": "2.2", + "storage_capability": "1", + "stream_max_sessions": "10", + "streaming_support_versions": [ + "2.0" + ], + "tapo_care_version": "1.0.0", + "target_track": "0", + "timing_reboot": "1", + "verification_change_password": "1", + "video_codec": [ + "h264", + "h265" + ], + "video_detection_digital_sensitivity": "1", + "wide_range_inf_sensitivity": "1", + "wifi_connection_info": "1", + "wireless_hotspot": "0" + } + } + } + }, + "scanApList": { + "onboarding": { + "scan": { + "ap_list": [ + { + "auth": 3, + "bssid": "000000000000", + "encryption": 3, + "rssi": 3, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 3, + "rssi": 3, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 3, + "rssi": 3, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 3, + "rssi": 2, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 3, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + } + ], + "public_key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAXfqAdngV6AVbE2CMG8f2I9OM\n/Xh/yWq4usOIqEhGW36Zq+mA2jVlH86hLqPwMeRXJO1teHYd53TVUAgk0US43GkS\n8uSFe9K5PXWt5TeDvLmBw3J85dj/sIDVxNvLrmwUD+Djqo2DLdW8HYvN83HN8Sf+\nLVWsnyRlVXjRjT5zDQIDAQAB\n-----END PUBLIC KEY-----\n", + "wpa3_supported": "true" + } + } + } +} diff --git a/tests/smartcam/modules/test_battery.py b/tests/smartcam/modules/test_battery.py index 12cab14b..723cb22b 100644 --- a/tests/smartcam/modules/test_battery.py +++ b/tests/smartcam/modules/test_battery.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from kasa import Device from kasa.smartcam.smartcammodule import SmartCamModule @@ -20,14 +22,69 @@ async def test_battery(dev: Device): battery = dev.modules.get(SmartCamModule.SmartCamBattery) assert battery - feat_ids = { - "battery_level", - "battery_low", - "battery_temperature", - "battery_voltage", - "battery_charging", - } - for feat_id in feat_ids: + required = {"battery_level", "battery_low", "battery_charging"} + optional = {"battery_temperature", "battery_voltage"} + + for feat_id in required: feat = dev.features.get(feat_id) assert feat assert feat.value is not None + + for feat_id in optional: + feat = dev.features.get(feat_id) + if feat is not None: + assert feat.value is not None + + +@battery_smartcam +@pytest.mark.parametrize( + ("raw", "expected"), + [ + (None, None), # covers: v in (None, "NO") -> return None + ("NO", None), # covers: v in (None, "NO") -> return None + ("nonsense", None), # covers: ValueError -> except -> return None + ("12.3", 12.3), # sanity: happy path + ], +) +async def test_battery_temperature_edge_cases(dev: Device, raw, expected): + battery = dev.modules.get(SmartCamModule.SmartCamBattery) + assert battery + + dev.sys_info["battery_temperature"] = raw + assert battery.battery_temperature == expected + + +@battery_smartcam +@pytest.mark.parametrize( + ("voltage_raw", "expected_v"), + [ + (None, None), # covers: battery_voltage -> return None + ("NO", None), # covers: battery_voltage -> return None + ("12000", 12.0), # sanity: parses string -> float(...) / 1000 + ], +) +async def test_battery_voltage_edge_cases(dev: Device, voltage_raw, expected_v): + battery = dev.modules.get(SmartCamModule.SmartCamBattery) + assert battery + + dev.sys_info["battery_voltage"] = voltage_raw + assert battery.battery_voltage == expected_v + + +@battery_smartcam +@pytest.mark.parametrize( + ("charging_raw", "expected"), + [ + (True, True), # covers: isinstance(v, bool) -> return v + (False, False), # covers: isinstance(v, bool) -> return v + (None, False), # covers: v is None -> return False + ("yes", True), # sanity: string normalization path + ("NO", False), # sanity: string normalization path + ], +) +async def test_battery_charging_edge_cases(dev: Device, charging_raw, expected): + battery = dev.modules.get(SmartCamModule.SmartCamBattery) + assert battery + + dev.sys_info["battery_charging"] = charging_raw + assert battery.battery_charging is expected