diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c8c145cc..0c3643b1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,9 +2,16 @@ name: CI
on:
push:
- branches: ["master", "patch"]
+ branches:
+ - master
+ - patch
pull_request:
- branches: ["master", "patch"]
+ branches:
+ - master
+ - patch
+ - 'feat/**'
+ - 'fix/**'
+ - 'janitor/**'
workflow_dispatch: # to allow manual re-runs
env:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 29d53358..9edba483 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -2,9 +2,16 @@ name: "CodeQL checks"
on:
push:
- branches: [ "master", "patch" ]
+ branches:
+ - master
+ - patch
pull_request:
- branches: [ master, "patch" ]
+ branches:
+ - master
+ - patch
+ - 'feat/**'
+ - 'fix/**'
+ - 'janitor/**'
schedule:
- cron: '44 17 * * 3'
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index adcad8e4..182ec765 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,6 +16,10 @@ repos:
- id: check-yaml
- id: debug-statements
- id: check-ast
+ - id: pretty-format-json
+ args:
+ - "--autofix"
+ - "--indent=4"
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.4
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 1d01cf18..17b68ff4 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -2,6 +2,10 @@ version: 2
formats: all
+sphinx:
+ configuration: docs/source/conf.py
+
+
build:
os: ubuntu-22.04
tools:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e64db28..fefd3fa2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,135 @@
# Changelog
+## [0.9.1](https://github.com/python-kasa/python-kasa/tree/0.9.1) (2025-01-06)
+
+[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.9.0...0.9.1)
+
+**Release summary:**
+
+- Support for hub-attached wall switches S210 and S220
+- Support for older firmware on Tapo cameras
+- Bugfixes and improvements
+
+**Implemented enhancements:**
+
+- Add support for Tapo hub-attached switch devices [\#1421](https://github.com/python-kasa/python-kasa/pull/1421) (@sdb9696)
+- Use repr\(\) for enum values in Feature.\_\_repr\_\_ [\#1414](https://github.com/python-kasa/python-kasa/pull/1414) (@rytilahti)
+- Update SslAesTransport for older firmware versions [\#1362](https://github.com/python-kasa/python-kasa/pull/1362) (@sdb9696)
+
+**Fixed bugs:**
+
+- T310 not detected with H200 Hub [\#1409](https://github.com/python-kasa/python-kasa/issues/1409)
+- Backoff after xor timeout and improve error reporting [\#1424](https://github.com/python-kasa/python-kasa/pull/1424) (@bdraco)
+- Fix incorrect obd src echo [\#1412](https://github.com/python-kasa/python-kasa/pull/1412) (@rytilahti)
+- Handle smartcam partial list responses [\#1411](https://github.com/python-kasa/python-kasa/pull/1411) (@sdb9696)
+
+**Added support for devices:**
+
+- Add S220 fixture [\#1419](https://github.com/python-kasa/python-kasa/pull/1419) (@rytilahti)
+- Add S210 fixture [\#1418](https://github.com/python-kasa/python-kasa/pull/1418) (@rytilahti)
+
+**Documentation updates:**
+
+- Improve exception messages on credential mismatches [\#1417](https://github.com/python-kasa/python-kasa/pull/1417) (@rytilahti)
+
+**Project maintenance:**
+
+- Add C210 2.0 1.3.11 fixture [\#1406](https://github.com/python-kasa/python-kasa/pull/1406) (@sdb9696)
+- Add HS210\(US\) 3.0 1.0.10 IOT Fixture [\#1405](https://github.com/python-kasa/python-kasa/pull/1405) (@ZeliardM)
+- Change smartcam detection features to category config [\#1402](https://github.com/python-kasa/python-kasa/pull/1402) (@sdb9696)
+
+## [0.9.0](https://github.com/python-kasa/python-kasa/tree/0.9.0) (2024-12-21)
+
+[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.8.1...0.9.0)
+
+**Release highlights:**
+
+- Improvements to Tapo camera support:
+ - C100, C225, C325WB, C520WS and TC70 now supported.
+ - Support for motion, person, tamper, and baby cry detection.
+- Initial support for Tapo robovacs.
+- API extended with `FeatureAttributes` for consumers to test for [supported features](https://python-kasa.readthedocs.io/en/stable/topics.html#modules-and-features).
+- Experimental support for Kasa cameras[^1]
+
+[^1]: Currently limited to devices not yet provisioned via the Tapo app - Many thanks to @puxtril!
+
+**Breaking changes:**
+
+- Use DeviceInfo consistently across devices [\#1338](https://github.com/python-kasa/python-kasa/pull/1338) (@sdb9696)
+
+**Implemented enhancements:**
+
+- Add bare-bones matter modules to smart and smartcam devices [\#1371](https://github.com/python-kasa/python-kasa/pull/1371) (@sdb9696)
+- Add bare bones homekit modules smart and smartcam devices [\#1370](https://github.com/python-kasa/python-kasa/pull/1370) (@sdb9696)
+- cli: print model, https, and lv for discover list [\#1339](https://github.com/python-kasa/python-kasa/pull/1339) (@rytilahti)
+- Add LinkieTransportV2 and basic IOT.IPCAMERA support [\#1270](https://github.com/python-kasa/python-kasa/pull/1270) (@Puxtril)
+- Add ssltransport for robovacs [\#943](https://github.com/python-kasa/python-kasa/pull/943) (@rytilahti)
+- Add rssi and signal\_level to smartcam [\#1392](https://github.com/python-kasa/python-kasa/pull/1392) (@sdb9696)
+- Add smartcam detection modules [\#1389](https://github.com/python-kasa/python-kasa/pull/1389) (@sdb9696)
+- Return raw discovery result in cli discover raw [\#1342](https://github.com/python-kasa/python-kasa/pull/1342) (@sdb9696)
+- Improve overheat reporting [\#1335](https://github.com/python-kasa/python-kasa/pull/1335) (@rytilahti)
+- Provide alternative camera urls [\#1316](https://github.com/python-kasa/python-kasa/pull/1316) (@sdb9696)
+
+**Fixed bugs:**
+
+- Tapo H200 Hub does not work with python-kasa [\#1149](https://github.com/python-kasa/python-kasa/issues/1149)
+- Fix lens mask required component and state [\#1386](https://github.com/python-kasa/python-kasa/pull/1386) (@sdb9696)
+- Add LensMask module to smartcam [\#1385](https://github.com/python-kasa/python-kasa/pull/1385) (@sdb9696)
+- Treat smartcam 500 errors after handshake as retryable [\#1395](https://github.com/python-kasa/python-kasa/pull/1395) (@sdb9696)
+- Do not error when accessing smart device\_type before update [\#1319](https://github.com/python-kasa/python-kasa/pull/1319) (@sdb9696)
+- Fallback to other module data on get\_energy\_usage errors [\#1245](https://github.com/python-kasa/python-kasa/pull/1245) (@rytilahti)
+
+**Added support for devices:**
+
+- Add P210M\(US\) 1.0 1.0.3 fixture [\#1399](https://github.com/python-kasa/python-kasa/pull/1399) (@sdb9696)
+- Add C225\(US\) 2.0 1.0.11 fixture [\#1398](https://github.com/python-kasa/python-kasa/pull/1398) (@sdb9696)
+- Add P306\(US\) 1.0 1.1.2 fixture [\#1396](https://github.com/python-kasa/python-kasa/pull/1396) (@nakanaela)
+- Add TC70 3.0 1.3.11 fixture [\#1390](https://github.com/python-kasa/python-kasa/pull/1390) (@sdb9696)
+- Add KS200 \(US\) IOT Fixture and P115 \(US\) Smart Fixture [\#1355](https://github.com/python-kasa/python-kasa/pull/1355) (@ZeliardM)
+- Add C520WS camera fixture [\#1352](https://github.com/python-kasa/python-kasa/pull/1352) (@Happy-Cadaver)
+- Add C325WB\(EU\) 1.0 1.1.17 Fixture [\#1379](https://github.com/python-kasa/python-kasa/pull/1379) (@sdb9696)
+- Add C100 4.0 1.3.14 Fixture [\#1378](https://github.com/python-kasa/python-kasa/pull/1378) (@sdb9696)
+
+**Documentation updates:**
+
+- Update docs for Tapo Lab Third-Party compatibility [\#1380](https://github.com/python-kasa/python-kasa/pull/1380) (@sdb9696)
+- Add homebridge-kasa-python link to README [\#1367](https://github.com/python-kasa/python-kasa/pull/1367) (@rytilahti)
+- Add link to related homeassistant-tapo-control [\#1333](https://github.com/python-kasa/python-kasa/pull/1333) (@rytilahti)
+- Update docs for new FeatureAttribute behaviour [\#1365](https://github.com/python-kasa/python-kasa/pull/1365) (@sdb9696)
+
+**Project maintenance:**
+
+- Add P135 1.0 1.2.0 fixture [\#1397](https://github.com/python-kasa/python-kasa/pull/1397) (@sdb9696)
+- Update C520WS fixture with new methods [\#1384](https://github.com/python-kasa/python-kasa/pull/1384) (@sdb9696)
+- Miscellaneous minor fixes to dump\_devinfo [\#1382](https://github.com/python-kasa/python-kasa/pull/1382) (@sdb9696)
+- Add timeout parameter to dump\_devinfo [\#1381](https://github.com/python-kasa/python-kasa/pull/1381) (@sdb9696)
+- Force single for some smartcam requests [\#1374](https://github.com/python-kasa/python-kasa/pull/1374) (@sdb9696)
+- Pass raw components to SmartChildDevice init [\#1363](https://github.com/python-kasa/python-kasa/pull/1363) (@sdb9696)
+- Fix line endings in device\_fixtures.py [\#1361](https://github.com/python-kasa/python-kasa/pull/1361) (@sdb9696)
+- Tweak RELEASING.md instructions for patch releases [\#1347](https://github.com/python-kasa/python-kasa/pull/1347) (@sdb9696)
+- Scrub more vacuum keys [\#1328](https://github.com/python-kasa/python-kasa/pull/1328) (@rytilahti)
+- Remove unnecessary check for python \<3.10 [\#1326](https://github.com/python-kasa/python-kasa/pull/1326) (@rytilahti)
+- Add vacuum component queries to dump\_devinfo [\#1320](https://github.com/python-kasa/python-kasa/pull/1320) (@rytilahti)
+- Handle missing mgt\_encryption\_schm in discovery [\#1318](https://github.com/python-kasa/python-kasa/pull/1318) (@sdb9696)
+- Follow main package structure for tests [\#1317](https://github.com/python-kasa/python-kasa/pull/1317) (@rytilahti)
+- Handle smartcam device blocked response [\#1393](https://github.com/python-kasa/python-kasa/pull/1393) (@sdb9696)
+- Handle KeyboardInterrupts in the cli better [\#1391](https://github.com/python-kasa/python-kasa/pull/1391) (@sdb9696)
+- Simplify get\_protocol to prevent clashes with smartcam and robovac [\#1377](https://github.com/python-kasa/python-kasa/pull/1377) (@sdb9696)
+- Add smartcam modules to package inits [\#1376](https://github.com/python-kasa/python-kasa/pull/1376) (@sdb9696)
+- Enable saving of fixture files without git clone [\#1375](https://github.com/python-kasa/python-kasa/pull/1375) (@sdb9696)
+- Add new methods to dump\_devinfo [\#1373](https://github.com/python-kasa/python-kasa/pull/1373) (@sdb9696)
+- Update cli, light modules, and docs to use FeatureAttributes [\#1364](https://github.com/python-kasa/python-kasa/pull/1364) (@sdb9696)
+- Update dump\_devinfo for raw discovery json and common redactors [\#1358](https://github.com/python-kasa/python-kasa/pull/1358) (@sdb9696)
+
+## [0.8.1](https://github.com/python-kasa/python-kasa/tree/0.8.1) (2024-12-06)
+
+[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.8.0...0.8.1)
+
+**Fixed bugs:**
+
+- Fix update errors on hubs with unsupported children [\#1344](https://github.com/python-kasa/python-kasa/pull/1344) (@sdb9696)
+- Fix smartcam missing device id [\#1343](https://github.com/python-kasa/python-kasa/pull/1343) (@sdb9696)
+
## [0.8.0](https://github.com/python-kasa/python-kasa/tree/0.8.0) (2024-11-26)
[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.7.7...0.8.0)
@@ -35,28 +165,28 @@ Special thanks to @ryenitcher and @Puxtril for their new contributions to the im
- Update cli feature command for actions not to require a value [\#1264](https://github.com/python-kasa/python-kasa/pull/1264) (@sdb9696)
- Add pan tilt camera module [\#1261](https://github.com/python-kasa/python-kasa/pull/1261) (@sdb9696)
- Add alarm module for smartcamera hubs [\#1258](https://github.com/python-kasa/python-kasa/pull/1258) (@sdb9696)
-- Move TAPO smartcamera out of experimental package [\#1255](https://github.com/python-kasa/python-kasa/pull/1255) (@sdb9696)
- Add SmartCamera Led Module [\#1249](https://github.com/python-kasa/python-kasa/pull/1249) (@sdb9696)
-- Use component queries to select smartcamera modules [\#1248](https://github.com/python-kasa/python-kasa/pull/1248) (@sdb9696)
- Print formatting for IotLightPreset [\#1216](https://github.com/python-kasa/python-kasa/pull/1216) (@Puxtril)
- Allow getting Annotated features from modules [\#1018](https://github.com/python-kasa/python-kasa/pull/1018) (@sdb9696)
+- Move TAPO smartcamera out of experimental package [\#1255](https://github.com/python-kasa/python-kasa/pull/1255) (@sdb9696)
+- Use component queries to select smartcamera modules [\#1248](https://github.com/python-kasa/python-kasa/pull/1248) (@sdb9696)
- Add common Thermostat module [\#977](https://github.com/python-kasa/python-kasa/pull/977) (@sdb9696)
**Fixed bugs:**
- TP-Link Tapo S505D cannot disable gradual on/off [\#1309](https://github.com/python-kasa/python-kasa/issues/1309)
-- Inconsistent emeter information between features and emeter cli [\#1308](https://github.com/python-kasa/python-kasa/issues/1308)
- How to dump power usage after latest updates? [\#1306](https://github.com/python-kasa/python-kasa/issues/1306)
- kasa.discover: Got unsupported connection type: 'device\_family': 'SMART.IPCAMERA' [\#1267](https://github.com/python-kasa/python-kasa/issues/1267)
- device \_\_repr\_\_ fails if no sys\_info [\#1262](https://github.com/python-kasa/python-kasa/issues/1262)
- Tapo P110M: Error processing Energy for device, module will be unavailable: get\_energy\_usage for Energy [\#1243](https://github.com/python-kasa/python-kasa/issues/1243)
- Listing light presets throws error [\#1201](https://github.com/python-kasa/python-kasa/issues/1201)
-- Include duration when disabling smooth transition on/off [\#1313](https://github.com/python-kasa/python-kasa/pull/1313) (@rytilahti)
-- Expose energy command to cli [\#1307](https://github.com/python-kasa/python-kasa/pull/1307) (@rytilahti)
+- Inconsistent emeter information between features and emeter cli [\#1308](https://github.com/python-kasa/python-kasa/issues/1308)
- Make discovery on unsupported devices less noisy [\#1291](https://github.com/python-kasa/python-kasa/pull/1291) (@rytilahti)
- Fix repr for device created with no sysinfo or discovery info" [\#1266](https://github.com/python-kasa/python-kasa/pull/1266) (@sdb9696)
- Fix discovery by alias for smart devices [\#1260](https://github.com/python-kasa/python-kasa/pull/1260) (@sdb9696)
- Make \_\_repr\_\_ work on discovery info [\#1233](https://github.com/python-kasa/python-kasa/pull/1233) (@rytilahti)
+- Include duration when disabling smooth transition on/off [\#1313](https://github.com/python-kasa/python-kasa/pull/1313) (@rytilahti)
+- Expose energy command to cli [\#1307](https://github.com/python-kasa/python-kasa/pull/1307) (@rytilahti)
**Added support for devices:**
@@ -70,13 +200,11 @@ Special thanks to @ryenitcher and @Puxtril for their new contributions to the im
**Documentation updates:**
- Use markdown footnotes in supported.md [\#1310](https://github.com/python-kasa/python-kasa/pull/1310) (@sdb9696)
-- Update docs for the new module attributes has/get feature [\#1301](https://github.com/python-kasa/python-kasa/pull/1301) (@sdb9696)
- Fixup contributing.md for running test against a real device [\#1236](https://github.com/python-kasa/python-kasa/pull/1236) (@sdb9696)
+- Update docs for the new module attributes has/get feature [\#1301](https://github.com/python-kasa/python-kasa/pull/1301) (@sdb9696)
**Project maintenance:**
-- Rename tests/smartcamera to tests/smartcam [\#1315](https://github.com/python-kasa/python-kasa/pull/1315) (@sdb9696)
-- Do not error on smartcam hub attached smartcam child devices [\#1314](https://github.com/python-kasa/python-kasa/pull/1314) (@sdb9696)
- Add P110M\(EU\) fixture [\#1305](https://github.com/python-kasa/python-kasa/pull/1305) (@sdb9696)
- Run tests with caplog in a single worker [\#1304](https://github.com/python-kasa/python-kasa/pull/1304) (@sdb9696)
- Rename smartcamera to smartcam [\#1300](https://github.com/python-kasa/python-kasa/pull/1300) (@sdb9696)
@@ -106,15 +234,17 @@ Special thanks to @ryenitcher and @Puxtril for their new contributions to the im
- Add linkcheck to readthedocs CI [\#1253](https://github.com/python-kasa/python-kasa/pull/1253) (@rytilahti)
- Update cli energy command to use energy module [\#1252](https://github.com/python-kasa/python-kasa/pull/1252) (@sdb9696)
- Consolidate warnings for fixtures missing child devices [\#1251](https://github.com/python-kasa/python-kasa/pull/1251) (@sdb9696)
-- Update smartcamera fixtures with components [\#1250](https://github.com/python-kasa/python-kasa/pull/1250) (@sdb9696)
- Move transports into their own package [\#1247](https://github.com/python-kasa/python-kasa/pull/1247) (@rytilahti)
-- Fix warnings in our test suite [\#1246](https://github.com/python-kasa/python-kasa/pull/1246) (@rytilahti)
- Move tests folder to top level of project [\#1242](https://github.com/python-kasa/python-kasa/pull/1242) (@sdb9696)
-- Fix test framework running against real devices [\#1235](https://github.com/python-kasa/python-kasa/pull/1235) (@sdb9696)
- Add Additional Firmware Test Fixures [\#1234](https://github.com/python-kasa/python-kasa/pull/1234) (@ryenitcher)
-- Update DiscoveryResult to use Mashumaro instead of pydantic [\#1231](https://github.com/python-kasa/python-kasa/pull/1231) (@sdb9696)
- Update fixture for ES20M 1.0.11 [\#1215](https://github.com/python-kasa/python-kasa/pull/1215) (@rytilahti)
- Enable ruff check for ANN [\#1139](https://github.com/python-kasa/python-kasa/pull/1139) (@rytilahti)
+- Rename tests/smartcamera to tests/smartcam [\#1315](https://github.com/python-kasa/python-kasa/pull/1315) (@sdb9696)
+- Do not error on smartcam hub attached smartcam child devices [\#1314](https://github.com/python-kasa/python-kasa/pull/1314) (@sdb9696)
+- Update smartcamera fixtures with components [\#1250](https://github.com/python-kasa/python-kasa/pull/1250) (@sdb9696)
+- Fix warnings in our test suite [\#1246](https://github.com/python-kasa/python-kasa/pull/1246) (@rytilahti)
+- Fix test framework running against real devices [\#1235](https://github.com/python-kasa/python-kasa/pull/1235) (@sdb9696)
+- Update DiscoveryResult to use Mashumaro instead of pydantic [\#1231](https://github.com/python-kasa/python-kasa/pull/1231) (@sdb9696)
**Closed issues:**
diff --git a/README.md b/README.md
index 3595dd19..b4bbf81b 100644
--- a/README.md
+++ b/README.md
@@ -178,13 +178,17 @@ The following devices have been tested and confirmed as working. If your device
> [!NOTE]
> The hub attached Tapo buttons S200B and S200D do not currently support alerting when the button is pressed.
+> [!NOTE]
+> Some firmware versions of Tapo Cameras will not authenticate unless you enable "Tapo Lab" > "Third-Party Compatibility" in the native Tapo app.
+> Alternatively, you can factory reset and then prevent the device from accessing the internet.
+
### Supported Kasa devices
- **Plugs**: EP10, EP25[^1], HS100[^2], HS103, HS105, HS110, KP100, KP105, KP115, KP125, KP125M[^1], KP401
- **Power Strips**: EP40, EP40M[^1], HS107, HS300, KP200, KP303, KP400
-- **Wall Switches**: ES20M, HS200[^2], HS210, HS220[^2], KP405, KS200M, KS205[^1], KS220, KS220M, KS225[^1], KS230, KS240[^1]
+- **Wall Switches**: ES20M, HS200[^2], HS210, HS220[^2], KP405, KS200, KS200M, KS205[^1], KS220, KS220M, KS225[^1], KS230, KS240[^1]
- **Bulbs**: KL110, KL120, KL125, KL130, KL135, KL50, KL60, LB110
- **Light Strips**: KL400L5, KL420L5, KL430
- **Hubs**: KH100[^1]
@@ -193,11 +197,13 @@ The following devices have been tested and confirmed as working. If your device
### Supported Tapo[^1] devices
- **Plugs**: P100, P110, P110M, P115, P125M, P135, TP15
-- **Power Strips**: P300, P304M, TP25
-- **Wall Switches**: S500D, S505, S505D
+- **Power Strips**: P210M, P300, P304M, P306, TP25
+- **Wall Switches**: S210, S220, S500D, S505, S505D
- **Bulbs**: L510B, L510E, L530E, L630
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
-- **Cameras**: C210, TC65
+- **Cameras**: C100, C210, C220, C225, C325WB, C520WS, C720, TC65, TC70
+- **Doorbells and chimes**: D100C, D130, D230
+- **Vacuums**: RV20 Max Plus, RV30 Max
- **Hubs**: H100, H200
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
@@ -223,6 +229,7 @@ See [supported devices in our documentation](SUPPORTED.md) for more detailed inf
* [Home Assistant](https://www.home-assistant.io/integrations/tplink/)
* [MQTT access to TP-Link devices, using python-kasa](https://github.com/flavio-fernandes/mqtt2kasa)
+* [Homebridge Kasa Python Plug-In](https://github.com/ZeliardM/homebridge-kasa-python)
### Other related projects
diff --git a/RELEASING.md b/RELEASING.md
index 032aeb0c..e3527cea 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -283,9 +283,12 @@ git rebase upstream/master
git checkout -b janitor/merge_patch
git fetch upstream patch
git merge upstream/patch --no-commit
+# If there are any merge conflicts run the following command which will simply make master win
+# Do not run it if there are no conflicts as it will end up checking out upstream/master
git diff --name-only --diff-filter=U | xargs git checkout upstream/master
+# Check the diff is as expected
git diff --staged
-# The only diff should be the version in pyproject.toml and CHANGELOG.md
+# The only diff should be the version in pyproject.toml and uv.lock, and CHANGELOG.md
# unless a change made on patch that was not part of a cherry-pick commit
# If there are any other unexpected diffs `git checkout upstream/master [thefilename]`
git commit -m "Merge patch into local master" -S
diff --git a/SUPPORTED.md b/SUPPORTED.md
index 034372b0..876566cd 100644
--- a/SUPPORTED.md
+++ b/SUPPORTED.md
@@ -5,6 +5,9 @@ The following devices have been tested and confirmed as working. If your device
> [!NOTE]
> The hub attached Tapo buttons S200B and S200D do not currently support alerting when the button is pressed.
+> [!NOTE]
+> Some firmware versions of Tapo Cameras will not authenticate unless you enable "Tapo Lab" > "Third-Party Compatibility" in the native Tapo app.
+> Alternatively, you can factory reset and then prevent the device from accessing the internet.
@@ -90,6 +93,7 @@ Some newer Kasa devices require authentication. These are marked with [^1] in th
- **HS210**
- Hardware: 1.0 (US) / Firmware: 1.5.8
- Hardware: 2.0 (US) / Firmware: 1.1.5
+ - Hardware: 3.0 (US) / Firmware: 1.0.10
- **HS220**
- Hardware: 1.0 (US) / Firmware: 1.5.7
- Hardware: 2.0 (US) / Firmware: 1.0.3
@@ -97,6 +101,8 @@ Some newer Kasa devices require authentication. These are marked with [^1] in th
- **KP405**
- Hardware: 1.0 (US) / Firmware: 1.0.5
- Hardware: 1.0 (US) / Firmware: 1.0.6
+- **KS200**
+ - Hardware: 1.0 (US) / Firmware: 1.0.8
- **KS200M**
- Hardware: 1.0 (US) / Firmware: 1.0.10
- Hardware: 1.0 (US) / Firmware: 1.0.11
@@ -114,6 +120,7 @@ Some newer Kasa devices require authentication. These are marked with [^1] in th
- Hardware: 1.0 (US) / Firmware: 1.1.0[^1]
- **KS230**
- Hardware: 1.0 (US) / Firmware: 1.0.14
+ - Hardware: 2.0 (US) / Firmware: 1.0.11
- **KS240**
- Hardware: 1.0 (US) / Firmware: 1.0.4[^1]
- Hardware: 1.0 (US) / Firmware: 1.0.5[^1]
@@ -192,26 +199,36 @@ All Tapo devices require authentication.
Hub-Connected Devices may work acros
- Hardware: 1.0 (EU) / Firmware: 1.2.3
- **P115**
- Hardware: 1.0 (EU) / Firmware: 1.2.3
+ - Hardware: 1.0 (US) / Firmware: 1.1.3
- **P125M**
- Hardware: 1.0 (US) / Firmware: 1.1.0
- **P135**
- Hardware: 1.0 (US) / Firmware: 1.0.5
+ - Hardware: 1.0 (US) / Firmware: 1.2.0
- **TP15**
- Hardware: 1.0 (US) / Firmware: 1.0.3
### Power Strips
+- **P210M**
+ - Hardware: 1.0 (US) / Firmware: 1.0.3
- **P300**
- Hardware: 1.0 (EU) / Firmware: 1.0.13
- Hardware: 1.0 (EU) / Firmware: 1.0.15
- Hardware: 1.0 (EU) / Firmware: 1.0.7
- **P304M**
- Hardware: 1.0 (UK) / Firmware: 1.0.3
+- **P306**
+ - Hardware: 1.0 (US) / Firmware: 1.1.2
- **TP25**
- Hardware: 1.0 (US) / Firmware: 1.0.2
### Wall Switches
+- **S210**
+ - Hardware: 1.0 (EU) / Firmware: 1.9.0
+- **S220**
+ - Hardware: 1.0 (EU) / Firmware: 1.9.0
- **S500D**
- Hardware: 1.0 (US) / Firmware: 1.0.5
- **S505**
@@ -252,11 +269,42 @@ All Tapo devices require authentication.
Hub-Connected Devices may work acros
### Cameras
+- **C100**
+ - Hardware: 4.0 / Firmware: 1.3.14
- **C210**
+ - Hardware: 2.0 / Firmware: 1.3.11
- Hardware: 2.0 (EU) / Firmware: 1.4.2
- Hardware: 2.0 (EU) / Firmware: 1.4.3
+- **C220**
+ - Hardware: 1.0 (EU) / Firmware: 1.2.2
+- **C225**
+ - Hardware: 2.0 (US) / Firmware: 1.0.11
+- **C325WB**
+ - Hardware: 1.0 (EU) / Firmware: 1.1.17
+- **C520WS**
+ - Hardware: 1.0 (US) / Firmware: 1.2.8
+- **C720**
+ - Hardware: 1.0 (US) / Firmware: 1.2.3
- **TC65**
- Hardware: 1.0 / Firmware: 1.3.9
+- **TC70**
+ - Hardware: 3.0 / Firmware: 1.3.11
+
+### Doorbells and chimes
+
+- **D100C**
+ - Hardware: 1.0 (US) / Firmware: 1.1.3
+- **D130**
+ - Hardware: 1.0 (US) / Firmware: 1.1.9
+- **D230**
+ - Hardware: 1.20 (EU) / Firmware: 1.1.19
+
+### Vacuums
+
+- **RV20 Max Plus**
+ - Hardware: 1.0 (EU) / Firmware: 1.0.7
+- **RV30 Max**
+ - Hardware: 1.0 (US) / Firmware: 1.2.0
### Hubs
@@ -266,6 +314,7 @@ All Tapo devices require authentication.
Hub-Connected Devices may work acros
- Hardware: 1.0 (EU) / Firmware: 1.5.5
- **H200**
- Hardware: 1.0 (EU) / Firmware: 1.3.2
+ - Hardware: 1.0 (EU) / Firmware: 1.3.6
- Hardware: 1.0 (US) / Firmware: 1.3.6
### Hub-Connected Devices
diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py
index 7760b6cb..a0fff0e5 100644
--- a/devtools/dump_devinfo.py
+++ b/devtools/dump_devinfo.py
@@ -10,8 +10,6 @@ and finally execute a query to query all of them at once.
from __future__ import annotations
-import base64
-import collections.abc
import dataclasses
import json
import logging
@@ -19,6 +17,7 @@ import re
import sys
import traceback
from collections import defaultdict, namedtuple
+from collections.abc import Callable
from pathlib import Path
from pprint import pprint
from typing import Any
@@ -39,30 +38,83 @@ from kasa import (
)
from kasa.device_factory import get_protocol
from kasa.deviceconfig import DeviceEncryptionType, DeviceFamily
-from kasa.discover import DiscoveryResult
+from kasa.discover import (
+ NEW_DISCOVERY_REDACTORS,
+ DiscoveredRaw,
+ DiscoveryResult,
+)
from kasa.exceptions import SmartErrorCode
from kasa.protocols import IotProtocol
+from kasa.protocols.iotprotocol import REDACTORS as IOT_REDACTORS
+from kasa.protocols.protocol import redact_data
from kasa.protocols.smartcamprotocol import (
SmartCamProtocol,
_ChildCameraProtocolWrapper,
)
+from kasa.protocols.smartprotocol import REDACTORS as SMART_REDACTORS
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
from kasa.smart import SmartChildDevice, SmartDevice
-from kasa.smartcam import SmartCamDevice
+from kasa.smartcam import SmartCamChild, SmartCamDevice
+from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
Call = namedtuple("Call", "module method")
-FixtureResult = namedtuple("FixtureResult", "filename, folder, data")
+FixtureResult = namedtuple("FixtureResult", "filename, folder, data, protocol_suffix")
SMART_FOLDER = "tests/fixtures/smart/"
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
SMART_CHILD_FOLDER = "tests/fixtures/smart/child/"
+SMARTCAM_CHILD_FOLDER = "tests/fixtures/smartcam/child/"
IOT_FOLDER = "tests/fixtures/iot/"
+SMART_PROTOCOL_SUFFIX = "SMART"
+SMARTCAM_SUFFIX = "SMARTCAM"
+SMART_CHILD_SUFFIX = "SMART.CHILD"
+SMARTCAM_CHILD_SUFFIX = "SMARTCAM.CHILD"
+IOT_SUFFIX = "IOT"
+
+NO_GIT_FIXTURE_FOLDER = "kasa-fixtures"
+
ENCRYPT_TYPES = [encrypt_type.value for encrypt_type in DeviceEncryptionType]
_LOGGER = logging.getLogger(__name__)
+def _wrap_redactors(redactors: dict[str, Callable[[Any], Any] | None]):
+ """Wrap the redactors for dump_devinfo.
+
+ Will replace all partial REDACT_ values with zeros.
+ If the data item is already scrubbed by dump_devinfo will leave as-is.
+ """
+
+ def _wrap(key: str) -> Any:
+ def _wrapped(redactor: Callable[[Any], Any] | None) -> Any | None:
+ if redactor is None:
+ return lambda x: "**SCRUBBED**"
+
+ def _redact_to_zeros(x: Any) -> Any:
+ if isinstance(x, str) and "REDACT" in x:
+ return re.sub(r"\w", "0", x)
+ if isinstance(x, dict):
+ for k, v in x.items():
+ x[k] = _redact_to_zeros(v)
+ return x
+
+ def _scrub(x: Any) -> Any:
+ if key in {"ip", "local_ip"}:
+ return "127.0.0.123"
+ # Already scrubbed by dump_devinfo
+ if isinstance(x, str) and "SCRUBBED" in x:
+ return x
+ default = redactor(x)
+ return _redact_to_zeros(default)
+
+ return _scrub
+
+ return _wrapped(redactors[key])
+
+ return {key: _wrap(key) for key in redactors}
+
+
@dataclasses.dataclass
class SmartCall:
"""Class for smart and smartcam calls."""
@@ -74,115 +126,6 @@ class SmartCall:
supports_multiple: bool = True
-def scrub(res):
- """Remove identifiers from the given dict."""
- keys_to_scrub = [
- "deviceId",
- "fwId",
- "hwId",
- "oemId",
- "mac",
- "mic_mac",
- "latitude_i",
- "longitude_i",
- "latitude",
- "longitude",
- "la", # lat on ks240
- "lo", # lon on ks240
- "owner",
- "device_id",
- "ip",
- "ssid",
- "hw_id",
- "fw_id",
- "oem_id",
- "nickname",
- "alias",
- "bssid",
- "channel",
- "original_device_id", # for child devices on strips
- "parent_device_id", # for hub children
- "setup_code", # matter
- "setup_payload", # matter
- "mfi_setup_code", # mfi_ for homekit
- "mfi_setup_id",
- "mfi_token_token",
- "mfi_token_uuid",
- "dev_id",
- "device_name",
- "device_alias",
- "connect_ssid",
- "encrypt_info",
- "local_ip",
- "username",
- # vacuum
- "board_sn",
- "custom_sn",
- "location",
- ]
-
- for k, v in res.items():
- if isinstance(v, collections.abc.Mapping):
- if k == "encrypt_info":
- if "data" in v:
- v["data"] = ""
- if "key" in v:
- v["key"] = ""
- else:
- res[k] = scrub(res.get(k))
- elif (
- isinstance(v, list)
- and len(v) > 0
- and isinstance(v[0], collections.abc.Mapping)
- ):
- res[k] = [scrub(vi) for vi in v]
- else:
- if k in keys_to_scrub:
- if k in ["mac", "mic_mac"]:
- # Some macs have : or - as a separator and others do not
- if len(v) == 12:
- v = f"{v[:6]}000000"
- else:
- delim = ":" if ":" in v else "-"
- rest = delim.join(
- format(s, "02x") for s in bytes.fromhex("000000")
- )
- v = f"{v[:8]}{delim}{rest}"
- elif k in ["latitude", "latitude_i", "longitude", "longitude_i"]:
- v = 0
- elif k in ["ip", "local_ip"]:
- v = "127.0.0.123"
- elif k in ["ssid"]:
- # Need a valid base64 value here
- v = base64.b64encode(b"#MASKED_SSID#").decode()
- elif k in ["nickname"]:
- v = base64.b64encode(b"#MASKED_NAME#").decode()
- elif k in [
- "alias",
- "device_alias",
- "device_name",
- "username",
- "location",
- ]:
- v = "#MASKED_NAME#"
- elif isinstance(res[k], int):
- v = 0
- elif k in ["map_data"]: #
- v = "#SCRUBBED_MAPDATA#"
- elif k in ["device_id", "dev_id"] and "SCRUBBED" in v:
- pass # already scrubbed
- elif k == ["device_id", "dev_id"] and len(v) > 40:
- # retain the last two chars when scrubbing child ids
- end = v[-2:]
- v = re.sub(r"\w", "0", v)
- v = v[:40] + end
- else:
- v = re.sub(r"\w", "0", v)
-
- res[k] = v
- return res
-
-
def default_to_regular(d):
"""Convert nested defaultdicts to regular ones.
@@ -207,9 +150,19 @@ async def handle_device(
]
for fixture_result in fixture_results:
- save_filename = Path(basedir) / fixture_result.folder / fixture_result.filename
+ save_folder = Path(basedir) / fixture_result.folder
+ if save_folder.exists():
+ save_filename = save_folder / f"{fixture_result.filename}.json"
+ else:
+ # If being run without git clone
+ save_folder = Path(basedir) / NO_GIT_FIXTURE_FOLDER
+ save_folder.mkdir(exist_ok=True)
+ save_filename = (
+ save_folder
+ / f"{fixture_result.filename}-{fixture_result.protocol_suffix}.json"
+ )
- pprint(scrub(fixture_result.data))
+ pprint(fixture_result.data)
if autosave:
save = "y"
else:
@@ -300,6 +253,12 @@ async def handle_device(
type=bool,
help="Set flag if the device encryption uses https.",
)
+@click.option(
+ "--timeout",
+ required=False,
+ default=15,
+ help="Timeout for queries.",
+)
@click.option("--port", help="Port override", type=int)
async def cli(
host,
@@ -317,6 +276,7 @@ async def cli(
device_family,
login_version,
port,
+ timeout,
):
"""Generate devinfo files for devices.
@@ -325,6 +285,11 @@ async def cli(
if debug:
logging.basicConfig(level=logging.DEBUG)
+ raw_discovery = {}
+
+ def capture_raw(discovered: DiscoveredRaw):
+ raw_discovery[discovered["meta"]["ip"]] = discovered["discovery_response"]
+
credentials = Credentials(username=username, password=password)
if host is not None:
if discovery_info:
@@ -335,13 +300,16 @@ async def cli(
connection_type = DeviceConnectionParameters.from_values(
dr.device_type,
dr.mgt_encrypt_schm.encrypt_type,
- dr.mgt_encrypt_schm.lv,
+ login_version=dr.mgt_encrypt_schm.lv,
+ https=dr.mgt_encrypt_schm.is_support_https,
+ http_port=dr.mgt_encrypt_schm.http_port,
)
dc = DeviceConfig(
host=host,
connection_type=connection_type,
port_override=port,
credentials=credentials,
+ timeout=timeout,
)
device = await Device.connect(config=dc)
await handle_device(
@@ -363,6 +331,7 @@ async def cli(
port_override=port,
credentials=credentials,
connection_type=ctype,
+ timeout=timeout,
)
if protocol := get_protocol(config):
await handle_device(basedir, autosave, protocol, batch_size=batch_size)
@@ -377,12 +346,17 @@ async def cli(
credentials=credentials,
port=port,
discovery_timeout=discovery_timeout,
+ timeout=timeout,
+ on_discovered_raw=capture_raw,
)
+ discovery_info = raw_discovery[device.host]
+ if decrypted_data := device._discovery_info.get("decrypted_data"):
+ discovery_info["result"]["decrypted_data"] = decrypted_data
await handle_device(
basedir,
autosave,
device.protocol,
- discovery_info=device._discovery_info,
+ discovery_info=discovery_info,
batch_size=batch_size,
)
else:
@@ -391,21 +365,29 @@ async def cli(
f" {target}. Use --target to override."
)
devices = await Discover.discover(
- target=target, credentials=credentials, discovery_timeout=discovery_timeout
+ target=target,
+ credentials=credentials,
+ discovery_timeout=discovery_timeout,
+ timeout=timeout,
+ on_discovered_raw=capture_raw,
)
click.echo(f"Detected {len(devices)} devices")
for dev in devices.values():
+ discovery_info = raw_discovery[dev.host]
+ if decrypted_data := dev._discovery_info.get("decrypted_data"):
+ discovery_info["result"]["decrypted_data"] = decrypted_data
+
await handle_device(
basedir,
autosave,
dev.protocol,
- discovery_info=dev._discovery_info,
+ discovery_info=discovery_info,
batch_size=batch_size,
)
async def get_legacy_fixture(
- protocol: IotProtocol, *, discovery_info: dict[str, Any] | None
+ protocol: IotProtocol, *, discovery_info: dict[str, dict[str, Any]] | None
) -> FixtureResult:
"""Get fixture for legacy IOT style protocol."""
items = [
@@ -475,11 +457,21 @@ async def get_legacy_fixture(
_echo_error(f"Unable to query all successes at once: {ex}")
finally:
await protocol.close()
+
+ final = redact_data(final, _wrap_redactors(IOT_REDACTORS))
+
+ # Scrub the child device ids
+ if children := final.get("system", {}).get("get_sysinfo", {}).get("children"):
+ for index, child in enumerate(children):
+ if "id" not in child:
+ _LOGGER.error("Could not find a device for the child device: %s", child)
+ else:
+ child["id"] = f"SCRUBBED_CHILD_DEVICE_ID_{index + 1}"
+
if discovery_info and not discovery_info.get("system"):
- # Need to recreate a DiscoverResult here because we don't want the aliases
- # in the fixture, we want the actual field names as returned by the device.
- dr = DiscoveryResult.from_dict(discovery_info)
- final["discovery_result"] = dr.to_dict()
+ final["discovery_result"] = redact_data(
+ discovery_info, _wrap_redactors(NEW_DISCOVERY_REDACTORS)
+ )
click.echo(f"Got {len(successes)} successes")
click.echo(click.style("## device info file ##", bold=True))
@@ -489,9 +481,14 @@ async def get_legacy_fixture(
hw_version = sysinfo["hw_ver"]
sw_version = sysinfo["sw_ver"]
sw_version = sw_version.split(" ", maxsplit=1)[0]
- save_filename = f"{model}_{hw_version}_{sw_version}.json"
+ save_filename = f"{model}_{hw_version}_{sw_version}"
copy_folder = IOT_FOLDER
- return FixtureResult(filename=save_filename, folder=copy_folder, data=final)
+ return FixtureResult(
+ filename=save_filename,
+ folder=copy_folder,
+ data=final,
+ protocol_suffix=IOT_SUFFIX,
+ )
def _echo_error(msg: str):
@@ -852,22 +849,83 @@ async def get_smart_test_calls(protocol: SmartProtocol):
return test_calls, successes
-def get_smart_child_fixture(response):
+def get_smart_child_fixture(response, model_info, folder, suffix):
"""Get a seperate fixture for the child device."""
- model_info = SmartDevice._get_device_info(response, None)
hw_version = model_info.hardware_version
fw_version = model_info.firmware_version
model = model_info.long_name
if model_info.region is not None:
model = f"{model}({model_info.region})"
- save_filename = f"{model}_{hw_version}_{fw_version}.json"
+ save_filename = f"{model}_{hw_version}_{fw_version}"
return FixtureResult(
- filename=save_filename, folder=SMART_CHILD_FOLDER, data=response
+ filename=save_filename,
+ folder=folder,
+ data=response,
+ protocol_suffix=suffix,
)
+def scrub_child_device_ids(
+ main_response: dict, child_responses: dict
+) -> dict[str, str]:
+ """Scrub all the child device ids in the responses."""
+ # Make the scrubbed id map
+ scrubbed_child_id_map = {
+ device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index + 1}"
+ for index, device_id in enumerate(child_responses.keys())
+ if device_id != ""
+ }
+
+ for child_id, response in child_responses.items():
+ scrubbed_child_id = scrubbed_child_id_map[child_id]
+ # scrub the device id in the child's get info response
+ # The checks for the device_id will ensure we can get a fixture
+ # even if the data is unexpectedly not available although it should
+ # always be there
+ if "get_device_info" in response and "device_id" in response["get_device_info"]:
+ response["get_device_info"]["device_id"] = scrubbed_child_id
+ elif (
+ basic_info := response.get("getDeviceInfo", {})
+ .get("device_info", {})
+ .get("basic_info")
+ ) and "dev_id" in basic_info:
+ basic_info["dev_id"] = scrubbed_child_id
+ else:
+ _LOGGER.error(
+ "Cannot find device id in child get device info: %s", child_id
+ )
+
+ # Scrub the device ids in the parent for smart protocol
+ if gc := main_response.get("get_child_device_component_list"):
+ for child in gc["child_component_list"]:
+ device_id = child["device_id"]
+ child["device_id"] = scrubbed_child_id_map[device_id]
+ for child in main_response["get_child_device_list"]["child_device_list"]:
+ device_id = child["device_id"]
+ child["device_id"] = scrubbed_child_id_map[device_id]
+
+ # Scrub the device ids in the parent for the smart camera protocol
+ if gc := main_response.get("getChildDeviceComponentList"):
+ for child in gc["child_component_list"]:
+ device_id = child["device_id"]
+ child["device_id"] = scrubbed_child_id_map[device_id]
+ for child in main_response["getChildDeviceList"]["child_device_list"]:
+ if device_id := child.get("device_id"):
+ child["device_id"] = scrubbed_child_id_map[device_id]
+ continue
+ elif dev_id := child.get("dev_id"):
+ child["dev_id"] = scrubbed_child_id_map[dev_id]
+ continue
+ _LOGGER.error("Could not find a device id for the child device: %s", child)
+
+ return scrubbed_child_id_map
+
+
async def get_smart_fixtures(
- protocol: SmartProtocol, *, discovery_info: dict[str, Any] | None, batch_size: int
+ protocol: SmartProtocol,
+ *,
+ discovery_info: dict[str, dict[str, Any]] | None,
+ batch_size: int,
) -> list[FixtureResult]:
"""Get fixture for new TAPO style protocol."""
if isinstance(protocol, SmartCamProtocol):
@@ -919,21 +977,19 @@ async def get_smart_fixtures(
finally:
await protocol.close()
+ # Put all the successes into a dict[child_device_id or "", successes[]]
device_requests: dict[str, list[SmartCall]] = {}
for success in successes:
device_request = device_requests.setdefault(success.child_device_id, [])
device_request.append(success)
- scrubbed_device_ids = {
- device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index}"
- for index, device_id in enumerate(device_requests.keys())
- if device_id != ""
- }
-
final = await _make_final_calls(
protocol, device_requests[""], "All successes", batch_size, child_device_id=""
)
fixture_results = []
+
+ # Make the final child calls
+ child_responses = {}
for child_device_id, requests in device_requests.items():
if child_device_id == "":
continue
@@ -944,77 +1000,118 @@ async def get_smart_fixtures(
batch_size,
child_device_id=child_device_id,
)
+ child_responses[child_device_id] = response
- scrubbed = scrubbed_device_ids[child_device_id]
- if "get_device_info" in response and "device_id" in response["get_device_info"]:
- response["get_device_info"]["device_id"] = scrubbed
- # If the child is a different model to the parent create a seperate fixture
- if "get_device_info" in final:
- parent_model = final["get_device_info"]["model"]
- elif "getDeviceInfo" in final:
- parent_model = final["getDeviceInfo"]["device_info"]["basic_info"][
- "device_model"
- ]
+ # scrub the child ids
+ scrubbed_child_id_map = scrub_child_device_ids(final, child_responses)
+
+ # Redact data from the main device response. _wrap_redactors ensure we do
+ # not redact the scrubbed child device ids and replaces REDACTED_partial_id
+ # with zeros
+ final = redact_data(final, _wrap_redactors(SMART_REDACTORS))
+
+ # smart cam child devices provide more information in getChildDeviceList on the
+ # parent than they return when queried directly for getDeviceInfo so we will store
+ # it in the child fixture.
+ if smart_cam_child_list := final.get("getChildDeviceList"):
+ child_infos_on_parent = {
+ info["device_id"]: info
+ for info in smart_cam_child_list["child_device_list"]
+ }
+
+ for child_id, response in child_responses.items():
+ scrubbed_child_id = scrubbed_child_id_map[child_id]
+
+ # Get the parent model for checking whether to create a seperate child fixture
+ if model := final.get("get_device_info", {}).get("model"):
+ parent_model = model
+ elif (
+ device_model := final.get("getDeviceInfo", {})
+ .get("device_info", {})
+ .get("basic_info", {})
+ .get("device_model")
+ ):
+ parent_model = device_model
else:
- raise KasaException("Cannot determine parent device model.")
+ parent_model = None
+ _LOGGER.error("Cannot determine parent device model.")
+
+ # different model smart child device
if (
- "component_nego" in response
- and "get_device_info" in response
- and (child_model := response["get_device_info"].get("model"))
+ (child_model := response.get("get_device_info", {}).get("model"))
+ and parent_model
and child_model != parent_model
):
- fixture_results.append(get_smart_child_fixture(response))
+ response = redact_data(response, _wrap_redactors(SMART_REDACTORS))
+ model_info = SmartDevice._get_device_info(response, None)
+ fixture_results.append(
+ get_smart_child_fixture(
+ response, model_info, SMART_CHILD_FOLDER, SMART_CHILD_SUFFIX
+ )
+ )
+ # different model smartcam child device
+ elif (
+ (
+ child_model := response.get("getDeviceInfo", {})
+ .get("device_info", {})
+ .get("basic_info", {})
+ .get("device_model")
+ )
+ and parent_model
+ and child_model != parent_model
+ ):
+ response = redact_data(response, _wrap_redactors(SMART_REDACTORS))
+ # There is more info in the childDeviceList on the parent
+ # particularly the region is needed here.
+ child_info_from_parent = child_infos_on_parent[scrubbed_child_id]
+ response[CHILD_INFO_FROM_PARENT] = child_info_from_parent
+ model_info = SmartCamChild._get_device_info(response, None)
+ fixture_results.append(
+ get_smart_child_fixture(
+ response, model_info, SMARTCAM_CHILD_FOLDER, SMARTCAM_CHILD_SUFFIX
+ )
+ )
+ # same model child device
else:
cd = final.setdefault("child_devices", {})
- cd[scrubbed] = response
+ cd[scrubbed_child_id] = response
- # Scrub the device ids in the parent for smart protocol
- if gc := final.get("get_child_device_component_list"):
- for child in gc["child_component_list"]:
- device_id = child["device_id"]
- child["device_id"] = scrubbed_device_ids[device_id]
- for child in final["get_child_device_list"]["child_device_list"]:
- device_id = child["device_id"]
- child["device_id"] = scrubbed_device_ids[device_id]
-
- # Scrub the device ids in the parent for the smart camera protocol
- if gc := final.get("getChildDeviceList"):
- for child in gc["child_device_list"]:
- if device_id := child.get("device_id"):
- child["device_id"] = scrubbed_device_ids[device_id]
- continue
- if device_id := child.get("dev_id"):
- child["dev_id"] = scrubbed_device_ids[device_id]
- continue
- _LOGGER.error("Could not find a device for the child device: %s", child)
-
- # Need to recreate a DiscoverResult here because we don't want the aliases
- # in the fixture, we want the actual field names as returned by the device.
+ discovery_result = None
if discovery_info:
- dr = DiscoveryResult.from_dict(discovery_info) # type: ignore
- final["discovery_result"] = dr.to_dict()
+ final["discovery_result"] = redact_data(
+ discovery_info, _wrap_redactors(NEW_DISCOVERY_REDACTORS)
+ )
+ discovery_result = discovery_info["result"]
click.echo(f"Got {len(successes)} successes")
click.echo(click.style("## device info file ##", bold=True))
if "get_device_info" in final:
# smart protocol
- model_info = SmartDevice._get_device_info(final, discovery_info)
+ model_info = SmartDevice._get_device_info(final, discovery_result)
copy_folder = SMART_FOLDER
+ protocol_suffix = SMART_PROTOCOL_SUFFIX
else:
# smart camera protocol
- model_info = SmartCamDevice._get_device_info(final, discovery_info)
+ model_info = SmartCamDevice._get_device_info(final, discovery_result)
copy_folder = SMARTCAM_FOLDER
+ protocol_suffix = SMARTCAM_SUFFIX
hw_version = model_info.hardware_version
sw_version = model_info.firmware_version
model = model_info.long_name
if model_info.region is not None:
model = f"{model}({model_info.region})"
- save_filename = f"{model}_{hw_version}_{sw_version}.json"
+ save_filename = f"{model}_{hw_version}_{sw_version}"
fixture_results.insert(
- 0, FixtureResult(filename=save_filename, folder=copy_folder, data=final)
+ 0,
+ FixtureResult(
+ filename=save_filename,
+ folder=copy_folder,
+ data=final,
+ protocol_suffix=protocol_suffix,
+ ),
)
return fixture_results
diff --git a/devtools/generate_supported.py b/devtools/generate_supported.py
index 532c7e6a..669a2de2 100755
--- a/devtools/generate_supported.py
+++ b/devtools/generate_supported.py
@@ -13,7 +13,7 @@ from typing import Any, NamedTuple
from kasa.device_type import DeviceType
from kasa.iot import IotDevice
from kasa.smart import SmartDevice
-from kasa.smartcam import SmartCamDevice
+from kasa.smartcam import SmartCamChild, SmartCamDevice
class SupportedVersion(NamedTuple):
@@ -36,6 +36,9 @@ DEVICE_TYPE_TO_PRODUCT_GROUP = {
DeviceType.Bulb: "Bulbs",
DeviceType.LightStrip: "Light Strips",
DeviceType.Camera: "Cameras",
+ DeviceType.Doorbell: "Doorbells and chimes",
+ DeviceType.Chime: "Doorbells and chimes",
+ DeviceType.Vacuum: "Vacuums",
DeviceType.Hub: "Hubs",
DeviceType.Sensor: "Hub-Connected Devices",
DeviceType.Thermostat: "Hub-Connected Devices",
@@ -49,6 +52,7 @@ IOT_FOLDER = "tests/fixtures/iot/"
SMART_FOLDER = "tests/fixtures/smart/"
SMART_CHILD_FOLDER = "tests/fixtures/smart/child"
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
+SMARTCAM_CHILD_FOLDER = "tests/fixtures/smartcam/child"
def generate_supported(args):
@@ -66,6 +70,7 @@ def generate_supported(args):
_get_supported_devices(supported, SMART_FOLDER, SmartDevice)
_get_supported_devices(supported, SMART_CHILD_FOLDER, SmartDevice)
_get_supported_devices(supported, SMARTCAM_FOLDER, SmartCamDevice)
+ _get_supported_devices(supported, SMARTCAM_CHILD_FOLDER, SmartCamChild)
readme_updated = _update_supported_file(
README_FILENAME, _supported_summary(supported), print_diffs
@@ -205,7 +210,7 @@ def _get_supported_devices(
fixture_data = json.load(f)
model_info = device_cls._get_device_info(
- fixture_data, fixture_data.get("discovery_result")
+ fixture_data, fixture_data.get("discovery_result", {}).get("result")
)
supported_type = DEVICE_TYPE_TO_PRODUCT_GROUP[model_info.device_type]
@@ -214,7 +219,7 @@ def _get_supported_devices(
smodel = stype.setdefault(model_info.long_name, [])
smodel.append(
SupportedVersion(
- region=model_info.region,
+ region=model_info.region if model_info.region else "",
hw=model_info.hardware_version,
fw=model_info.firmware_version,
auth=model_info.requires_auth,
diff --git a/devtools/helpers/smartcamrequests.py b/devtools/helpers/smartcamrequests.py
index 074b5774..5759a44b 100644
--- a/devtools/helpers/smartcamrequests.py
+++ b/devtools/helpers/smartcamrequests.py
@@ -60,4 +60,7 @@ SMARTCAM_REQUESTS: list[dict] = [
{"get": {"motor": {"name": ["capability"]}}},
{"get": {"audio_capability": {"name": ["device_speaker", "device_microphone"]}}},
{"get": {"audio_config": {"name": ["speaker", "microphone"]}}},
+ {"getMatterSetupInfo": {"matter": {}}},
+ {"getConnectStatus": {"onboarding": {"get_connect_status": {}}}},
+ {"scanApList": {"onboarding": {"scan": {}}}},
]
diff --git a/devtools/helpers/smartrequests.py b/devtools/helpers/smartrequests.py
index 6ab53937..3756cb95 100644
--- a/devtools/helpers/smartrequests.py
+++ b/devtools/helpers/smartrequests.py
@@ -118,6 +118,16 @@ class SmartRequest:
enable: bool
id: str | None = None
+ @dataclass
+ class GetCleanAttrParams(SmartRequestParams):
+ """CleanAttr params.
+
+ Decides which cleaning settings are requested
+ """
+
+ #: type can be global or pose
+ type: str = "global"
+
@staticmethod
def get_raw_request(
method: str, params: SmartRequestParams | None = None
@@ -427,23 +437,32 @@ COMPONENT_REQUESTS = {
"overheat_protection": [],
# Vacuum components
"clean": [
+ SmartRequest.get_raw_request("getCarpetClean"),
SmartRequest.get_raw_request("getCleanRecords"),
SmartRequest.get_raw_request("getVacStatus"),
+ SmartRequest.get_raw_request("getAreaUnit"),
+ SmartRequest.get_raw_request("getCleanInfo"),
+ SmartRequest.get_raw_request("getCleanStatus"),
+ SmartRequest("getCleanAttr", SmartRequest.GetCleanAttrParams()),
],
"battery": [SmartRequest.get_raw_request("getBatteryInfo")],
"consumables": [SmartRequest.get_raw_request("getConsumablesInfo")],
"direction_control": [],
- "button_and_led": [],
+ "button_and_led": [SmartRequest.get_raw_request("getChildLockInfo")],
"speaker": [
SmartRequest.get_raw_request("getSupportVoiceLanguage"),
SmartRequest.get_raw_request("getCurrentVoiceLanguage"),
+ SmartRequest.get_raw_request("getVolume"),
],
"map": [
SmartRequest.get_raw_request("getMapInfo"),
SmartRequest.get_raw_request("getMapData"),
],
"auto_change_map": [SmartRequest.get_raw_request("getAutoChangeMap")],
- "dust_bucket": [SmartRequest.get_raw_request("getAutoDustCollection")],
+ "dust_bucket": [
+ SmartRequest.get_raw_request("getAutoDustCollection"),
+ SmartRequest.get_raw_request("getDustCollectionInfo"),
+ ],
"mop": [SmartRequest.get_raw_request("getMopState")],
"do_not_disturb": [SmartRequest.get_raw_request("getDoNotDisturb")],
"charge_pose_clean": [],
diff --git a/devtools/update_fixtures.py b/devtools/update_fixtures.py
new file mode 100644
index 00000000..13b9996e
--- /dev/null
+++ b/devtools/update_fixtures.py
@@ -0,0 +1,128 @@
+"""Module to mass update fixture files."""
+
+import json
+import logging
+from collections.abc import Callable
+from pathlib import Path
+
+import asyncclick as click
+
+from devtools.dump_devinfo import _wrap_redactors
+from kasa.discover import NEW_DISCOVERY_REDACTORS, redact_data
+from kasa.protocols.iotprotocol import REDACTORS as IOT_REDACTORS
+from kasa.protocols.smartprotocol import REDACTORS as SMART_REDACTORS
+
+FIXTURE_FOLDER = "tests/fixtures/"
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def update_fixtures(update_func: Callable[[dict], bool], *, dry_run: bool) -> None:
+ """Run the update function against the fixtures."""
+ for file in Path(FIXTURE_FOLDER).glob("**/*.json"):
+ with file.open("r") as f:
+ fixture_data = json.load(f)
+
+ if file.parent.name == "serialization":
+ continue
+ changed = update_func(fixture_data)
+ if changed:
+ click.echo(f"Will update {file.name}\n")
+ if changed and not dry_run:
+ with file.open("w") as f:
+ json.dump(fixture_data, f, sort_keys=True, indent=4)
+ f.write("\n")
+
+
+def _discovery_result_update(info) -> bool:
+ """Update discovery_result to be the raw result and error_code."""
+ if (disco_result := info.get("discovery_result")) and "result" not in disco_result:
+ info["discovery_result"] = {
+ "result": disco_result,
+ "error_code": 0,
+ }
+ return True
+ return False
+
+
+def _child_device_id_update(info) -> bool:
+ """Update child device ids to be the scrubbed ids from dump_devinfo."""
+ changed = False
+ if get_child_device_list := info.get("get_child_device_list"):
+ child_device_list = get_child_device_list["child_device_list"]
+ child_component_list = info["get_child_device_component_list"][
+ "child_component_list"
+ ]
+ for index, child_device in enumerate(child_device_list):
+ child_component = child_component_list[index]
+ if "SCRUBBED" not in child_device["device_id"]:
+ dev_id = f"SCRUBBED_CHILD_DEVICE_ID_{index + 1}"
+ click.echo(
+ f"child_device_id{index}: {child_device['device_id']} -> {dev_id}"
+ )
+ child_device["device_id"] = dev_id
+ child_component["device_id"] = dev_id
+ changed = True
+
+ if children := info.get("system", {}).get("get_sysinfo", {}).get("children"):
+ for index, child_device in enumerate(children):
+ if "SCRUBBED" not in child_device["id"]:
+ dev_id = f"SCRUBBED_CHILD_DEVICE_ID_{index + 1}"
+ click.echo(f"child_device_id{index}: {child_device['id']} -> {dev_id}")
+ child_device["id"] = dev_id
+ changed = True
+
+ return changed
+
+
+def _diff_data(fullkey, data1, data2, diffs):
+ if isinstance(data1, dict):
+ for k, v in data1.items():
+ _diff_data(fullkey + "/" + k, v, data2[k], diffs)
+ elif isinstance(data1, list):
+ for index, item in enumerate(data1):
+ _diff_data(fullkey + "/" + str(index), item, data2[index], diffs)
+ elif data1 != data2:
+ diffs[fullkey] = (data1, data2)
+
+
+def _redactor_result_update(info) -> bool:
+ """Update fixtures with the output using the common redactors."""
+ changed = False
+
+ redactors = IOT_REDACTORS if "system" in info else SMART_REDACTORS
+
+ for key, val in info.items():
+ if not isinstance(val, dict):
+ continue
+ if key == "discovery_result":
+ info[key] = redact_data(val, _wrap_redactors(NEW_DISCOVERY_REDACTORS))
+ else:
+ info[key] = redact_data(val, _wrap_redactors(redactors))
+ diffs: dict[str, tuple[str, str]] = {}
+ _diff_data(key, val, info[key], diffs)
+ if diffs:
+ for k, v in diffs.items():
+ click.echo(f"{k}: {v[0]} -> {v[1]}")
+ changed = True
+
+ return changed
+
+
+@click.option(
+ "--dry-run/--no-dry-run",
+ default=False,
+ is_flag=True,
+ type=bool,
+ help="Perform a dry run without saving.",
+)
+@click.command()
+async def cli(dry_run: bool) -> None:
+ """Cli method fo rupdating fixtures."""
+ update_fixtures(_discovery_result_update, dry_run=dry_run)
+ update_fixtures(_child_device_id_update, dry_run=dry_run)
+ update_fixtures(_redactor_result_update, dry_run=dry_run)
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/docs/source/featureattributes.md b/docs/source/featureattributes.md
new file mode 100644
index 00000000..69285ad4
--- /dev/null
+++ b/docs/source/featureattributes.md
@@ -0,0 +1,13 @@
+Some modules have attributes that may not be supported by the device.
+These attributes will be annotated with a `FeatureAttribute` return type.
+For example:
+
+```py
+ @property
+ def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
+ """Return the current HSV state of the bulb."""
+```
+
+You can test whether a `FeatureAttribute` is supported by the device with {meth}`kasa.Module.has_feature`
+or {meth}`kasa.Module.get_feature` which will return `None` if not supported.
+Calling these methods on attributes not annotated with a `FeatureAttribute` return type will return an error.
diff --git a/docs/source/reference.md b/docs/source/reference.md
index f4771ac5..90493c9c 100644
--- a/docs/source/reference.md
+++ b/docs/source/reference.md
@@ -13,11 +13,13 @@
## Device
+% N.B. Credentials clashes with autodoc
```{eval-rst}
.. autoclass:: Device
:members:
:undoc-members:
+ :exclude-members: Credentials
```
@@ -28,7 +30,6 @@
.. autoclass:: Credentials
:members:
:undoc-members:
- :noindex:
```
@@ -61,15 +62,11 @@
```{eval-rst}
.. autoclass:: Module
- :noindex:
:members:
- :inherited-members:
- :undoc-members:
```
```{eval-rst}
.. autoclass:: Feature
- :noindex:
:members:
:inherited-members:
:undoc-members:
@@ -77,7 +74,6 @@
```{eval-rst}
.. automodule:: kasa.interfaces
- :noindex:
:members:
:inherited-members:
:undoc-members:
@@ -85,64 +81,29 @@
## Protocols and transports
+
```{eval-rst}
-.. autoclass:: kasa.protocols.BaseProtocol
+.. automodule:: kasa.protocols
:members:
- :inherited-members:
+ :imported-members:
:undoc-members:
+ :exclude-members: SmartErrorCode
+ :no-index:
```
```{eval-rst}
-.. autoclass:: kasa.protocols.IotProtocol
+.. automodule:: kasa.transports
:members:
- :inherited-members:
+ :imported-members:
:undoc-members:
+ :no-index:
```
-```{eval-rst}
-.. autoclass:: kasa.protocols.SmartProtocol
- :members:
- :inherited-members:
- :undoc-members:
-```
-
-```{eval-rst}
-.. autoclass:: kasa.transports.BaseTransport
- :members:
- :inherited-members:
- :undoc-members:
-```
-
-```{eval-rst}
-.. autoclass:: kasa.transports.XorTransport
- :members:
- :inherited-members:
- :undoc-members:
-```
-
-```{eval-rst}
-.. autoclass:: kasa.transports.KlapTransport
- :members:
- :inherited-members:
- :undoc-members:
-```
-
-```{eval-rst}
-.. autoclass:: kasa.transports.KlapTransportV2
- :members:
- :inherited-members:
- :undoc-members:
-```
-
-```{eval-rst}
-.. autoclass:: kasa.transports.AesTransport
- :members:
- :inherited-members:
- :undoc-members:
-```
## Errors and exceptions
+
+
```{eval-rst}
.. autoclass:: kasa.exceptions.KasaException
:members:
@@ -171,3 +132,4 @@
.. autoclass:: kasa.exceptions.TimeoutError
:members:
:undoc-members:
+```
diff --git a/docs/source/topics.md b/docs/source/topics.md
index 0dcc60d1..f7d0cdd5 100644
--- a/docs/source/topics.md
+++ b/docs/source/topics.md
@@ -80,14 +80,17 @@ This can be done using the {attr}`~kasa.Device.internal_state` property.
## Modules and Features
The functionality provided by all {class}`~kasa.Device` instances is (mostly) done inside separate modules.
-While the individual device-type specific classes provide an easy access for the most import features,
-you can also access individual modules through {attr}`kasa.Device.modules`.
-You can get the list of supported modules for a given device instance using {attr}`~kasa.Device.supported_modules`.
+While the device class provides easy access for most device related attributes,
+for components like `light` and `camera` you can access the module through {attr}`kasa.Device.modules`.
+The module names are handily available as constants on {class}`~kasa.Module` and will return type aware values from the collection.
-```{note}
-If you only need some module-specific information,
-you can call the wanted method on the module to avoid using {meth}`~kasa.Device.update`.
-```
+Features represent individual pieces of functionality within a module like brightness, hsv and temperature within a light module.
+They allow for instrospection and can be accessed through {attr}`kasa.Device.features`.
+Attributes can be accessed via a `Feature` or a module attribute depending on the use case.
+Modules tend to provide richer functionality but using the features does not require an understanding of the module api.
+
+:::{include} featureattributes.md
+:::
(topics-protocols-and-transports)=
## Protocols and Transports
@@ -137,96 +140,3 @@ The base exception for all library errors is {class}`KasaException `.
- If the device fails to respond within a timeout the library raises a {class}`TimeoutError `.
- All other failures will raise the base {class}`KasaException ` class.
-
-
diff --git a/docs/tutorial.py b/docs/tutorial.py
index 8d0a1435..fddcc79a 100644
--- a/docs/tutorial.py
+++ b/docs/tutorial.py
@@ -40,7 +40,7 @@ Different groups of functionality are supported by modules which you can access
key from :class:`~kasa.Module`.
Modules will only be available on the device if they are supported but some individual features of a module may not be available for your device.
-You can check the availability using ``is_``-prefixed properties like `is_color`.
+You can check the availability using ``has_feature()`` method.
>>> from kasa import Module
>>> Module.Light in dev.modules
@@ -52,9 +52,9 @@ True
>>> await dev.update()
>>> light.brightness
50
->>> light.is_color
+>>> light.has_feature("hsv")
True
->>> if light.is_color:
+>>> if light.has_feature("hsv"):
>>> print(light.hsv)
HSV(hue=0, saturation=100, value=50)
@@ -91,5 +91,5 @@ False
True
>>> for feat in dev.features.values():
>>> print(f"{feat.name}: {feat.value}")
-Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nReboot: \nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: None\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: None\nCheck latest firmware: \nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nDevice time: 2024-02-23 02:40:15+01:00
+Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nReboot: \nDevice time: 2024-02-23 02:40:15+01:00\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: None\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: None\nCheck latest firmware: \nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nOverheated: False
"""
diff --git a/kasa/__init__.py b/kasa/__init__.py
index d4a5022e..b8871f99 100755
--- a/kasa/__init__.py
+++ b/kasa/__init__.py
@@ -38,8 +38,9 @@ from kasa.feature import Feature
from kasa.interfaces.light import HSV, ColorTempRange, Light, LightState
from kasa.interfaces.thermostat import Thermostat, ThermostatState
from kasa.module import Module
-from kasa.protocols import BaseProtocol, IotProtocol, SmartProtocol
+from kasa.protocols import BaseProtocol, IotProtocol, SmartCamProtocol, SmartProtocol
from kasa.protocols.iotprotocol import _deprecated_TPLinkSmartHomeProtocol # noqa: F401
+from kasa.smartcam.modules.camera import StreamResolution
from kasa.transports import BaseTransport
__version__ = version("python-kasa")
@@ -51,6 +52,7 @@ __all__ = [
"BaseTransport",
"IotProtocol",
"SmartProtocol",
+ "SmartCamProtocol",
"LightState",
"TurnOnBehaviors",
"TurnOnBehavior",
@@ -75,6 +77,7 @@ __all__ = [
"DeviceFamily",
"ThermostatState",
"Thermostat",
+ "StreamResolution",
]
from . import iot
diff --git a/kasa/cli/common.py b/kasa/cli/common.py
index 649df065..d0ef9dc3 100644
--- a/kasa/cli/common.py
+++ b/kasa/cli/common.py
@@ -2,13 +2,15 @@
from __future__ import annotations
+import asyncio
import json
import re
import sys
from collections.abc import Callable
from contextlib import contextmanager
from functools import singledispatch, update_wrapper, wraps
-from typing import TYPE_CHECKING, Any, Final
+from gettext import gettext
+from typing import TYPE_CHECKING, Any, Final, NoReturn
import asyncclick as click
@@ -55,7 +57,7 @@ def echo(*args, **kwargs) -> None:
_echo(*args, **kwargs)
-def error(msg: str) -> None:
+def error(msg: str) -> NoReturn:
"""Print an error and exit."""
echo(f"[bold red]{msg}[/bold red]")
sys.exit(1)
@@ -66,6 +68,16 @@ def json_formatter_cb(result: Any, **kwargs) -> None:
if not kwargs.get("json"):
return
+ # Calling the discover command directly always returns a DeviceDict so if host
+ # was specified just format the device json
+ if (
+ (host := kwargs.get("host"))
+ and isinstance(result, dict)
+ and (dev := result.get(host))
+ and isinstance(dev, Device)
+ ):
+ result = dev
+
@singledispatch
def to_serializable(val):
"""Regular obj-to-string for json serialization.
@@ -83,6 +95,25 @@ def json_formatter_cb(result: Any, **kwargs) -> None:
print(json_content)
+async def invoke_subcommand(
+ command: click.BaseCommand,
+ ctx: click.Context,
+ args: list[str] | None = None,
+ **extra: Any,
+) -> Any:
+ """Invoke a click subcommand.
+
+ Calling ctx.Invoke() treats the command like a simple callback and doesn't
+ process any result_callbacks so we use this pattern from the click docs
+ https://click.palletsprojects.com/en/stable/exceptions/#what-if-i-don-t-want-that.
+ """
+ if args is None:
+ args = []
+ sub_ctx = await command.make_context(command.name, args, parent=ctx, **extra)
+ async with sub_ctx:
+ return await command.invoke(sub_ctx)
+
+
def pass_dev_or_child(wrapped_function: Callable) -> Callable:
"""Pass the device or child to the click command based on the child options."""
child_help = (
@@ -238,4 +269,19 @@ def CatchAllExceptions(cls):
except Exception as exc:
_handle_exception(self._debug, exc)
+ def __call__(self, *args, **kwargs):
+ """Run the coroutine in the event loop and print any exceptions.
+
+ python click catches KeyboardInterrupt in main, raises Abort()
+ and does sys.exit. asyncclick doesn't properly handle a coroutine
+ receiving CancelledError on a KeyboardInterrupt, so we catch the
+ KeyboardInterrupt here once asyncio.run has re-raised it. This
+ avoids large stacktraces when a user presses Ctrl-C.
+ """
+ try:
+ asyncio.run(self.main(*args, **kwargs))
+ except KeyboardInterrupt:
+ click.echo(gettext("\nAborted!"), file=sys.stderr)
+ sys.exit(1)
+
return _CommandCls
diff --git a/kasa/cli/device.py b/kasa/cli/device.py
index 2e621368..a10f485d 100644
--- a/kasa/cli/device.py
+++ b/kasa/cli/device.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from pprint import pformat as pf
+from typing import TYPE_CHECKING
import asyncclick as click
@@ -41,8 +42,14 @@ async def state(ctx, dev: Device):
echo(f"Device state: {dev.is_on}")
echo(f"Time: {dev.time} (tz: {dev.timezone})")
- echo(f"Hardware: {dev.hw_info['hw_ver']}")
- echo(f"Software: {dev.hw_info['sw_ver']}")
+ echo(
+ f"Hardware: {dev.device_info.hardware_version}"
+ f"{' (' + dev.region + ')' if dev.region else ''}"
+ )
+ echo(
+ f"Firmware: {dev.device_info.firmware_version}"
+ f" {dev.device_info.firmware_build}"
+ )
echo(f"MAC (rssi): {dev.mac} ({dev.rssi})")
if verbose:
echo(f"Location: {dev.location}")
@@ -76,6 +83,8 @@ async def state(ctx, dev: Device):
echo()
from .discover import _echo_discovery_info
+ if TYPE_CHECKING:
+ assert dev._discovery_info
_echo_discovery_info(dev._discovery_info)
return dev.internal_state
diff --git a/kasa/cli/discover.py b/kasa/cli/discover.py
index 377d75e8..af367e32 100644
--- a/kasa/cli/discover.py
+++ b/kasa/cli/discover.py
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
from pprint import pformat as pf
+from typing import TYPE_CHECKING, cast
import asyncclick as click
@@ -14,22 +15,53 @@ from kasa import (
Discover,
UnsupportedDeviceError,
)
-from kasa.discover import ConnectAttempt, DiscoveryResult
+from kasa.discover import (
+ NEW_DISCOVERY_REDACTORS,
+ ConnectAttempt,
+ DeviceDict,
+ DiscoveredRaw,
+ DiscoveryResult,
+ OnDiscoveredCallable,
+ OnDiscoveredRawCallable,
+ OnUnsupportedCallable,
+)
+from kasa.iot.iotdevice import _extract_sys_info
+from kasa.protocols.iotprotocol import REDACTORS as IOT_REDACTORS
+from kasa.protocols.protocol import redact_data
+from ..json import dumps as json_dumps
from .common import echo, error
@click.group(invoke_without_command=True)
@click.pass_context
-async def discover(ctx):
+async def discover(ctx: click.Context):
"""Discover devices in the network."""
if ctx.invoked_subcommand is None:
return await ctx.invoke(detail)
+@discover.result_callback()
+@click.pass_context
+async def _close_protocols(ctx: click.Context, discovered: DeviceDict):
+ """Close all the device protocols if discover was invoked directly by the user."""
+ if _discover_is_root_cmd(ctx):
+ for dev in discovered.values():
+ await dev.disconnect()
+ return discovered
+
+
+def _discover_is_root_cmd(ctx: click.Context) -> bool:
+ """Will return true if discover was invoked directly by the user."""
+ root_ctx = ctx.find_root()
+ return (
+ root_ctx.invoked_subcommand is None or root_ctx.invoked_subcommand == "discover"
+ )
+
+
@discover.command()
@click.pass_context
-async def detail(ctx):
+async def detail(ctx: click.Context) -> DeviceDict:
"""Discover devices in the network using udp broadcasts."""
unsupported = []
auth_failed = []
@@ -50,10 +82,14 @@ async def detail(ctx):
from .device import state
async def print_discovered(dev: Device) -> None:
+ if TYPE_CHECKING:
+ assert ctx.parent
async with sem:
try:
await dev.update()
except AuthenticationError:
+ if TYPE_CHECKING:
+ assert dev._discovery_info
auth_failed.append(dev._discovery_info)
echo("== Authentication failed for device ==")
_echo_discovery_info(dev._discovery_info)
@@ -63,8 +99,12 @@ async def detail(ctx):
await ctx.parent.invoke(state)
echo()
- discovered = await _discover(ctx, print_discovered, print_unsupported)
- if ctx.parent.parent.params["host"]:
+ discovered = await _discover(
+ ctx,
+ print_discovered=print_discovered if _discover_is_root_cmd(ctx) else None,
+ print_unsupported=print_unsupported,
+ )
+ if ctx.find_root().params["host"]:
return discovered
echo(f"Found {len(discovered)} devices")
@@ -77,22 +117,54 @@ async def detail(ctx):
@discover.command()
+@click.option(
+ "--redact/--no-redact",
+ default=False,
+ is_flag=True,
+ type=bool,
+ help="Set flag to redact sensitive data from raw output.",
+)
@click.pass_context
-async def list(ctx):
+async def raw(ctx: click.Context, redact: bool) -> DeviceDict:
+ """Return raw discovery data returned from devices."""
+
+ def print_raw(discovered: DiscoveredRaw):
+ if redact:
+ redactors = (
+ NEW_DISCOVERY_REDACTORS
+ if discovered["meta"]["port"] == Discover.DISCOVERY_PORT_2
+ else IOT_REDACTORS
+ )
+ discovered["discovery_response"] = redact_data(
+ discovered["discovery_response"], redactors
+ )
+ echo(json_dumps(discovered, indent=True))
+
+ return await _discover(ctx, print_raw=print_raw, do_echo=False)
+
+
+@discover.command()
+@click.pass_context
+async def list(ctx: click.Context) -> DeviceDict:
"""List devices in the network in a table using udp broadcasts."""
sem = asyncio.Semaphore()
async def print_discovered(dev: Device):
cparams = dev.config.connection_type
infostr = (
- f"{dev.host:<15} {cparams.device_family.value:<20} "
- f"{cparams.encryption_type.value:<7}"
+ f"{dev.host:<15} {dev.model:<9} {cparams.device_family.value:<20} "
+ f"{cparams.encryption_type.value:<7} {cparams.https:<5} "
+ f"{cparams.login_version or '-':<3}"
)
async with sem:
try:
await dev.update()
except AuthenticationError:
echo(f"{infostr} - Authentication failed")
+ except TimeoutError:
+ echo(f"{infostr} - Timed out")
+ except Exception as ex:
+ echo(f"{infostr} - Error: {ex}")
else:
echo(f"{infostr} {dev.alias}")
@@ -100,12 +172,28 @@ async def list(ctx):
if host := unsupported_exception.host:
echo(f"{host:<15} UNSUPPORTED DEVICE")
- echo(f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}")
- return await _discover(ctx, print_discovered, print_unsupported, do_echo=False)
+ echo(
+ f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
+ f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
+ )
+ discovered = await _discover(
+ ctx,
+ print_discovered=print_discovered,
+ print_unsupported=print_unsupported,
+ do_echo=False,
+ )
+ return discovered
-async def _discover(ctx, print_discovered, print_unsupported, *, do_echo=True):
- params = ctx.parent.parent.params
+async def _discover(
+ ctx: click.Context,
+ *,
+ print_discovered: OnDiscoveredCallable | None = None,
+ print_unsupported: OnUnsupportedCallable | None = None,
+ print_raw: OnDiscoveredRawCallable | None = None,
+ do_echo=True,
+) -> DeviceDict:
+ params = ctx.find_root().params
target = params["target"]
username = params["username"]
password = params["password"]
@@ -117,15 +205,23 @@ async def _discover(ctx, print_discovered, print_unsupported, *, do_echo=True):
credentials = Credentials(username, password) if username and password else None
if host:
+ host = cast(str, host)
echo(f"Discovering device {host} for {discovery_timeout} seconds")
- return await Discover.discover_single(
+ dev = await Discover.discover_single(
host,
port=port,
credentials=credentials,
timeout=timeout,
discovery_timeout=discovery_timeout,
on_unsupported=print_unsupported,
+ on_discovered_raw=print_raw,
)
+ if dev:
+ if print_discovered:
+ await print_discovered(dev)
+ return {host: dev}
+ else:
+ return {}
if do_echo:
echo(f"Discovering devices on {target} for {discovery_timeout} seconds")
discovered_devices = await Discover.discover(
@@ -136,23 +232,21 @@ async def _discover(ctx, print_discovered, print_unsupported, *, do_echo=True):
port=port,
timeout=timeout,
credentials=credentials,
+ on_discovered_raw=print_raw,
)
- for device in discovered_devices.values():
- await device.protocol.close()
-
return discovered_devices
@discover.command()
@click.pass_context
-async def config(ctx):
+async def config(ctx: click.Context) -> DeviceDict:
"""Bypass udp discovery and try to show connection config for a device.
Bypasses udp discovery and shows the parameters required to connect
directly to the device.
"""
- params = ctx.parent.parent.params
+ params = ctx.find_root().params
username = params["username"]
password = params["password"]
timeout = params["timeout"]
@@ -167,8 +261,11 @@ async def config(ctx):
host_port = host + (f":{port}" if port else "")
def on_attempt(connect_attempt: ConnectAttempt, success: bool) -> None:
- prot, tran, dev = connect_attempt
- key_str = f"{prot.__name__} + {tran.__name__} + {dev.__name__}"
+ prot, tran, dev, https = connect_attempt
+ key_str = (
+ f"{prot.__name__} + {tran.__name__} + {dev.__name__}"
+ f" + {'https' if https else 'http'}"
+ )
result = "succeeded" if success else "failed"
msg = f"Attempt to connect to {host_port} with {key_str} {result}"
echo(msg)
@@ -184,6 +281,7 @@ async def config(ctx):
f"--encrypt-type {cparams.encryption_type.value} "
f"{'--https' if cparams.https else '--no-https'}"
)
+ return {host: dev}
else:
error(f"Unable to connect to {host}")
@@ -196,13 +294,13 @@ def _echo_dictionary(discovery_info: dict) -> None:
echo(f"\t{key_name_and_spaces}{value}")
-def _echo_discovery_info(discovery_info) -> None:
+def _echo_discovery_info(discovery_info: dict) -> None:
# We don't have discovery info when all connection params are passed manually
if discovery_info is None:
return
- if "system" in discovery_info and "get_sysinfo" in discovery_info["system"]:
- _echo_dictionary(discovery_info["system"]["get_sysinfo"])
+ if sysinfo := _extract_sys_info(discovery_info):
+ _echo_dictionary(sysinfo)
return
try:
@@ -228,7 +326,7 @@ def _echo_discovery_info(discovery_info) -> None:
_conditional_echo("HW Ver", dr.hw_ver)
_conditional_echo("HW Ver", dr.hardware_version)
_conditional_echo("Supports IOT Cloud", dr.is_support_iot_cloud)
- _conditional_echo("OBD Src", dr.owner)
+ _conditional_echo("OBD Src", dr.obd_src)
_conditional_echo("Factory Default", dr.factory_default)
_conditional_echo("Encrypt Type", dr.encrypt_type)
if mgt_encrypt_schm := dr.mgt_encrypt_schm:
diff --git a/kasa/cli/hub.py b/kasa/cli/hub.py
new file mode 100644
index 00000000..44478132
--- /dev/null
+++ b/kasa/cli/hub.py
@@ -0,0 +1,96 @@
+"""Hub-specific commands."""
+
+import asyncio
+
+import asyncclick as click
+
+from kasa import DeviceType, Module, SmartDevice
+from kasa.smart import SmartChildDevice
+
+from .common import (
+ echo,
+ error,
+ pass_dev,
+)
+
+
+def pretty_category(cat: str):
+ """Return pretty category for paired devices."""
+ return SmartChildDevice.CHILD_DEVICE_TYPE_MAP.get(cat)
+
+
+@click.group()
+@pass_dev
+async def hub(dev: SmartDevice):
+ """Commands controlling hub child device pairing."""
+ if dev.device_type is not DeviceType.Hub:
+ error(f"{dev} is not a hub.")
+
+ if dev.modules.get(Module.ChildSetup) is None:
+ error(f"{dev} does not have child setup module.")
+
+
+@hub.command(name="list")
+@pass_dev
+async def hub_list(dev: SmartDevice):
+ """List hub paired child devices."""
+ for c in dev.children:
+ echo(f"{c.device_id}: {c}")
+
+
+@hub.command(name="supported")
+@pass_dev
+async def hub_supported(dev: SmartDevice):
+ """List supported hub child device categories."""
+ cs = dev.modules[Module.ChildSetup]
+
+ cats = [cat["category"] for cat in await cs.get_supported_device_categories()]
+ for cat in cats:
+ echo(f"Supports: {cat}")
+
+
+@hub.command(name="pair")
+@click.option("--timeout", default=10)
+@pass_dev
+async def hub_pair(dev: SmartDevice, timeout: int):
+ """Pair all pairable device.
+
+ This will pair any child devices currently in pairing mode.
+ """
+ cs = dev.modules[Module.ChildSetup]
+
+ echo(f"Finding new devices for {timeout} seconds...")
+
+ pair_res = await cs.pair(timeout=timeout)
+ if not pair_res:
+ echo("No devices found.")
+
+ for child in pair_res:
+ echo(
+ f'Paired {child["name"]} ({child["device_model"]}, '
+ f'{pretty_category(child["category"])}) with id {child["device_id"]}'
+ )
+
+
+@hub.command(name="unpair")
+@click.argument("device_id")
+@pass_dev
+async def hub_unpair(dev, device_id: str):
+ """Unpair given device."""
+ cs = dev.modules[Module.ChildSetup]
+
+ # Accessing private here, as the property exposes only values
+ if device_id not in dev._children:
+ error(f"{dev} does not have children with identifier {device_id}")
+
+ res = await cs.unpair(device_id=device_id)
+ # Give the device some time to update its internal state, just in case.
+ await asyncio.sleep(1)
+ await dev.update()
+
+ if device_id not in dev._children:
+ echo(f"Unpaired {device_id}")
+ else:
+ error(f"Failed to unpair {device_id}")
+
+ return res
diff --git a/kasa/cli/light.py b/kasa/cli/light.py
index b2909c59..a7785563 100644
--- a/kasa/cli/light.py
+++ b/kasa/cli/light.py
@@ -25,7 +25,9 @@ def light(dev) -> None:
@pass_dev_or_child
async def brightness(dev: Device, brightness: int, transition: int):
"""Get or set brightness."""
- if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
+ if not (light := dev.modules.get(Module.Light)) or not light.has_feature(
+ "brightness"
+ ):
error("This device does not support brightness.")
return
@@ -45,13 +47,15 @@ async def brightness(dev: Device, brightness: int, transition: int):
@pass_dev_or_child
async def temperature(dev: Device, temperature: int, transition: int):
"""Get or set color temperature."""
- if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
+ if not (light := dev.modules.get(Module.Light)) or not (
+ color_temp_feat := light.get_feature("color_temp")
+ ):
error("Device does not support color temperature")
return
if temperature is None:
echo(f"Color temperature: {light.color_temp}")
- valid_temperature_range = light.valid_temperature_range
+ valid_temperature_range = color_temp_feat.range
if valid_temperature_range != (0, 0):
echo("(min: {}, max: {})".format(*valid_temperature_range))
else:
@@ -59,7 +63,7 @@ async def temperature(dev: Device, temperature: int, transition: int):
"Temperature range unknown, please open a github issue"
f" or a pull request for model '{dev.model}'"
)
- return light.valid_temperature_range
+ return color_temp_feat.range
else:
echo(f"Setting color temperature to {temperature}")
return await light.set_color_temp(temperature, transition=transition)
@@ -99,7 +103,7 @@ async def effect(dev: Device, ctx, effect):
@pass_dev_or_child
async def hsv(dev: Device, ctx, h, s, v, transition):
"""Get or set color in HSV."""
- if not (light := dev.modules.get(Module.Light)) or not light.is_color:
+ if not (light := dev.modules.get(Module.Light)) or not light.has_feature("hsv"):
error("Device does not support colors")
return
diff --git a/kasa/cli/main.py b/kasa/cli/main.py
index fbcdf391..4f1eccda 100755
--- a/kasa/cli/main.py
+++ b/kasa/cli/main.py
@@ -22,6 +22,7 @@ from .common import (
CatchAllExceptions,
echo,
error,
+ invoke_subcommand,
json_formatter_cb,
pass_dev_or_child,
)
@@ -92,6 +93,8 @@ def _legacy_type_to_class(_type: str) -> Any:
"hsv": "light",
"temperature": "light",
"effect": "light",
+ "vacuum": "vacuum",
+ "hub": "hub",
},
result_callback=json_formatter_cb,
)
@@ -295,9 +298,10 @@ async def cli(
echo("No host name given, trying discovery..")
from .discover import discover
- return await ctx.invoke(discover)
+ return await invoke_subcommand(discover, ctx)
device_updated = False
+ device_discovered = False
if type is not None and type not in {"smart", "camera"}:
from kasa.deviceconfig import DeviceConfig
@@ -351,12 +355,14 @@ async def cli(
return
echo(f"Found hostname by alias: {dev.host}")
device_updated = True
- else:
+ else: # host will be set
from .discover import discover
- dev = await ctx.invoke(discover)
- if not dev:
+ discovered = await invoke_subcommand(discover, ctx)
+ if not discovered:
error(f"Unable to create device for {host}")
+ dev = discovered[host]
+ device_discovered = True
# Skip update on specific commands, or if device factory,
# that performs an update was used for the device.
@@ -372,11 +378,14 @@ async def cli(
ctx.obj = await ctx.with_async_resource(async_wrapped_device(dev))
- if ctx.invoked_subcommand is None:
+ # discover command has already invoked state
+ if ctx.invoked_subcommand is None and not device_discovered:
from .device import state
return await ctx.invoke(state)
+ return dev
+
@cli.command()
@pass_dev_or_child
diff --git a/kasa/cli/vacuum.py b/kasa/cli/vacuum.py
new file mode 100644
index 00000000..d0ccc55a
--- /dev/null
+++ b/kasa/cli/vacuum.py
@@ -0,0 +1,84 @@
+"""Module for cli vacuum commands.."""
+
+from __future__ import annotations
+
+import asyncclick as click
+
+from kasa import (
+ Device,
+ Module,
+)
+
+from .common import (
+ error,
+ pass_dev_or_child,
+)
+
+
+@click.group(invoke_without_command=False)
+@click.pass_context
+async def vacuum(ctx: click.Context) -> None:
+ """Vacuum commands."""
+
+
+@vacuum.group(invoke_without_command=True, name="records")
+@pass_dev_or_child
+async def records_group(dev: Device) -> None:
+ """Access cleaning records."""
+ if not (rec := dev.modules.get(Module.CleanRecords)):
+ error("This device does not support records.")
+
+ data = rec.parsed_data
+ latest = data.last_clean
+ click.echo(
+ f"Totals: {rec.total_clean_area} {rec.area_unit} in {rec.total_clean_time} "
+ f"(cleaned {rec.total_clean_count} times)"
+ )
+ click.echo(f"Last clean: {latest.clean_area} {rec.area_unit} @ {latest.clean_time}")
+ click.echo("Execute `kasa vacuum records list` to list all records.")
+
+
+@records_group.command(name="list")
+@pass_dev_or_child
+async def records_list(dev: Device) -> None:
+ """List all cleaning records."""
+ if not (rec := dev.modules.get(Module.CleanRecords)):
+ error("This device does not support records.")
+
+ data = rec.parsed_data
+ for record in data.records:
+ click.echo(
+ f"* {record.timestamp}: cleaned {record.clean_area} {rec.area_unit}"
+ f" in {record.clean_time}"
+ )
+
+
+@vacuum.group(invoke_without_command=True, name="consumables")
+@pass_dev_or_child
+@click.pass_context
+async def consumables(ctx: click.Context, dev: Device) -> None:
+ """List device consumables."""
+ if not (cons := dev.modules.get(Module.Consumables)):
+ error("This device does not support consumables.")
+
+ if not ctx.invoked_subcommand:
+ for c in cons.consumables.values():
+ click.echo(f"{c.name} ({c.id}): {c.used} used, {c.remaining} remaining")
+
+
+@consumables.command(name="reset")
+@click.argument("consumable_id", required=True)
+@pass_dev_or_child
+async def reset_consumable(dev: Device, consumable_id: str) -> None:
+ """Reset the consumable used/remaining time."""
+ cons = dev.modules[Module.Consumables]
+
+ if consumable_id not in cons.consumables:
+ error(
+ f"Consumable {consumable_id} not found in "
+ f"device consumables: {', '.join(cons.consumables.keys())}."
+ )
+
+ await cons.reset_consumable(consumable_id)
+
+ click.echo(f"Consumable {consumable_id} reset")
diff --git a/kasa/credentials.py b/kasa/credentials.py
index 2d669999..66dd1174 100644
--- a/kasa/credentials.py
+++ b/kasa/credentials.py
@@ -25,6 +25,7 @@ def get_default_credentials(tuple: tuple[str, str]) -> Credentials:
DEFAULT_CREDENTIALS = {
"KASA": ("a2FzYUB0cC1saW5rLm5ldA==", "a2FzYVNldHVw"),
+ "KASACAMERA": ("YWRtaW4=", "MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM="),
"TAPO": ("dGVzdEB0cC1saW5rLm5ldA==", "dGVzdA=="),
"TAPOCAMERA": ("YWRtaW4=", "YWRtaW4="),
}
diff --git a/kasa/device.py b/kasa/device.py
index 76d7a7c5..d86a565e 100644
--- a/kasa/device.py
+++ b/kasa/device.py
@@ -29,7 +29,7 @@ All devices provide several informational properties:
>>> dev.alias
Bedroom Lamp Plug
>>> dev.model
-HS110(EU)
+HS110
>>> dev.rssi
-71
>>> dev.mac
@@ -107,7 +107,7 @@ from __future__ import annotations
import logging
from abc import ABC, abstractmethod
-from collections.abc import Mapping, Sequence
+from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime, tzinfo
from typing import TYPE_CHECKING, Any, TypeAlias
@@ -151,7 +151,7 @@ _LOGGER = logging.getLogger(__name__)
@dataclass
-class _DeviceInfo:
+class DeviceInfo:
"""Device Model Information."""
short_name: str
@@ -208,7 +208,7 @@ class Device(ABC):
self.protocol: BaseProtocol = protocol or IotProtocol(
transport=XorTransport(config=config or DeviceConfig(host=host)),
)
- self._last_update: Any = None
+ self._last_update: dict[str, Any] = {}
_LOGGER.debug("Initializing %s of type %s", host, type(self))
self._device_type = DeviceType.Unknown
# TODO: typing Any is just as using dict | None would require separate
@@ -334,9 +334,21 @@ class Device(ABC):
"""Returns the device model."""
@property
+ def region(self) -> str | None:
+ """Returns the device region."""
+ return self.device_info.region
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device info."""
+ return self._get_device_info(self._last_update, self._discovery_info)
+
+ @staticmethod
@abstractmethod
- def _model_region(self) -> str:
- """Return device full model name and region."""
+ def _get_device_info(
+ info: dict[str, Any], discovery_info: dict[str, Any] | None
+ ) -> DeviceInfo:
+ """Get device info."""
@property
@abstractmethod
@@ -525,19 +537,52 @@ class Device(ABC):
return None
+ def _get_deprecated_callable_attribute(self, name: str) -> Any | None:
+ vals: dict[str, tuple[ModuleName, Callable[[Any], Any], str]] = {
+ "is_dimmable": (
+ Module.Light,
+ lambda c: c.has_feature("brightness"),
+ 'light_module.has_feature("brightness")',
+ ),
+ "is_color": (
+ Module.Light,
+ lambda c: c.has_feature("hsv"),
+ 'light_module.has_feature("hsv")',
+ ),
+ "is_variable_color_temp": (
+ Module.Light,
+ lambda c: c.has_feature("color_temp"),
+ 'light_module.has_feature("color_temp")',
+ ),
+ "valid_temperature_range": (
+ Module.Light,
+ lambda c: c._deprecated_valid_temperature_range(),
+ 'minimum and maximum value of get_feature("color_temp")',
+ ),
+ "has_effects": (
+ Module.Light,
+ lambda c: Module.LightEffect in c._device.modules,
+ "Module.LightEffect in device.modules",
+ ),
+ }
+ if mod_call_msg := vals.get(name):
+ mod, call, msg = mod_call_msg
+ msg = f"{name} is deprecated, use: {msg} instead"
+ warn(msg, DeprecationWarning, stacklevel=2)
+ if (module := self.modules.get(mod)) is None:
+ raise AttributeError(f"Device has no attribute {name!r}")
+ return call(module)
+
+ return None
+
_deprecated_other_attributes = {
# light attributes
- "is_color": (Module.Light, ["is_color"]),
- "is_dimmable": (Module.Light, ["is_dimmable"]),
- "is_variable_color_temp": (Module.Light, ["is_variable_color_temp"]),
"brightness": (Module.Light, ["brightness"]),
"set_brightness": (Module.Light, ["set_brightness"]),
"hsv": (Module.Light, ["hsv"]),
"set_hsv": (Module.Light, ["set_hsv"]),
"color_temp": (Module.Light, ["color_temp"]),
"set_color_temp": (Module.Light, ["set_color_temp"]),
- "valid_temperature_range": (Module.Light, ["valid_temperature_range"]),
- "has_effects": (Module.Light, ["has_effects"]),
"_deprecated_set_light_state": (Module.Light, ["has_effects"]),
# led attributes
"led": (Module.Led, ["led"]),
@@ -576,6 +621,9 @@ class Device(ABC):
msg = f"{name} is deprecated, use device_type property instead"
warn(msg, DeprecationWarning, stacklevel=2)
return self.device_type == dep_device_type_attr[1]
+ # callable
+ if (result := self._get_deprecated_callable_attribute(name)) is not None:
+ return result
# Other deprecated attributes
if (dep_attr := self._deprecated_other_attributes.get(name)) and (
(replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))
diff --git a/kasa/device_factory.py b/kasa/device_factory.py
old mode 100755
new mode 100644
index be3c6ca0..53ceba17
--- a/kasa/device_factory.py
+++ b/kasa/device_factory.py
@@ -8,10 +8,11 @@ from typing import Any
from .device import Device
from .device_type import DeviceType
-from .deviceconfig import DeviceConfig
+from .deviceconfig import DeviceConfig, DeviceEncryptionType, DeviceFamily
from .exceptions import KasaException, UnsupportedDeviceError
from .iot import (
IotBulb,
+ IotCamera,
IotDevice,
IotDimmer,
IotLightStrip,
@@ -32,6 +33,7 @@ from .transports import (
BaseTransport,
KlapTransport,
KlapTransportV2,
+ LinkieTransportV2,
SslTransport,
XorTransport,
)
@@ -138,6 +140,7 @@ def get_device_class_from_sys_info(sysinfo: dict[str, Any]) -> type[IotDevice]:
DeviceType.Strip: IotStrip,
DeviceType.WallSwitch: IotWallSwitch,
DeviceType.LightStrip: IotLightStrip,
+ DeviceType.Camera: IotCamera,
}
return TYPE_TO_CLASS[IotDevice._get_device_type_from_sys_info(sysinfo)]
@@ -156,9 +159,11 @@ def get_device_class_from_family(
"SMART.KASAHUB": SmartDevice,
"SMART.KASASWITCH": SmartDevice,
"SMART.IPCAMERA.HTTPS": SmartCamDevice,
- "SMART.TAPOROBOVAC": SmartDevice,
+ "SMART.TAPODOORBELL.HTTPS": SmartCamDevice,
+ "SMART.TAPOROBOVAC.HTTPS": SmartDevice,
"IOT.SMARTPLUGSWITCH": IotPlug,
"IOT.SMARTBULB": IotBulb,
+ "IOT.IPCAMERA": IotCamera,
}
lookup_key = f"{device_type}{'.HTTPS' if https else ''}"
if (
@@ -169,26 +174,52 @@ def get_device_class_from_family(
_LOGGER.debug("Unknown SMART device with %s, using SmartDevice", device_type)
cls = SmartDevice
+ if cls is not None:
+ _LOGGER.debug("Using %s for %s", cls.__name__, device_type)
+
return cls
-def get_protocol(
- config: DeviceConfig,
-) -> BaseProtocol | None:
- """Return the protocol from the connection name."""
- protocol_name = config.connection_type.device_family.value.split(".")[0]
+def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol | None:
+ """Return the protocol from the device config.
+
+ For cameras and vacuums the device family is a simple mapping to
+ the protocol/transport. For other device types the transport varies
+ based on the discovery information.
+
+ :param config: Device config to derive protocol
+ :param strict: Require exact match on encrypt type
+ """
+ _LOGGER.debug("Finding protocol for %s", config.host)
ctype = config.connection_type
+ protocol_name = ctype.device_family.value.split(".")[0]
+ _LOGGER.debug("Finding protocol for %s", ctype.device_family)
+
+ if ctype.device_family in {
+ DeviceFamily.SmartIpCamera,
+ DeviceFamily.SmartTapoDoorbell,
+ }:
+ if strict and ctype.encryption_type is not DeviceEncryptionType.Aes:
+ return None
+ return SmartCamProtocol(transport=SslAesTransport(config=config))
+
+ if ctype.device_family is DeviceFamily.IotIpCamera:
+ if strict and ctype.encryption_type is not DeviceEncryptionType.Xor:
+ return None
+ return IotProtocol(transport=LinkieTransportV2(config=config))
+
+ # Older FW used a different transport
+ if (
+ ctype.device_family is DeviceFamily.SmartTapoRobovac
+ and ctype.encryption_type is DeviceEncryptionType.Aes
+ ):
+ return SmartProtocol(transport=SslTransport(config=config))
protocol_transport_key = (
protocol_name
+ "."
+ ctype.encryption_type.value
+ (".HTTPS" if ctype.https else "")
- + (
- f".{ctype.login_version}"
- if ctype.login_version and ctype.login_version > 1
- else ""
- )
)
_LOGGER.debug("Finding transport for %s", protocol_transport_key)
@@ -198,10 +229,11 @@ def get_protocol(
"IOT.XOR": (IotProtocol, XorTransport),
"IOT.KLAP": (IotProtocol, KlapTransport),
"SMART.AES": (SmartProtocol, AesTransport),
- "SMART.AES.2": (SmartProtocol, AesTransport),
- "SMART.KLAP.2": (SmartProtocol, KlapTransportV2),
- "SMART.AES.HTTPS.2": (SmartCamProtocol, SslAesTransport),
- "SMART.AES.HTTPS": (SmartProtocol, SslTransport),
+ "SMART.KLAP": (SmartProtocol, KlapTransportV2),
+ "SMART.KLAP.HTTPS": (SmartProtocol, KlapTransportV2),
+ # H200 is device family SMART.TAPOHUB and uses SmartCamProtocol so use
+ # https to distuingish from SmartProtocol devices
+ "SMART.AES.HTTPS": (SmartCamProtocol, SslAesTransport),
}
if not (prot_tran_cls := supported_device_protocols.get(protocol_transport_key)):
return None
diff --git a/kasa/device_type.py b/kasa/device_type.py
index 7fe485d3..d3996217 100755
--- a/kasa/device_type.py
+++ b/kasa/device_type.py
@@ -22,6 +22,8 @@ class DeviceType(Enum):
Fan = "fan"
Thermostat = "thermostat"
Vacuum = "vacuum"
+ Chime = "chime"
+ Doorbell = "doorbell"
Unknown = "unknown"
@staticmethod
diff --git a/kasa/deviceconfig.py b/kasa/deviceconfig.py
index 6f9176f5..2b669f80 100644
--- a/kasa/deviceconfig.py
+++ b/kasa/deviceconfig.py
@@ -20,7 +20,7 @@ None
{'host': '127.0.0.3', 'timeout': 5, 'credentials': {'username': 'user@example.com', \
'password': 'great_password'}, 'connection_type'\
: {'device_family': 'SMART.TAPOBULB', 'encryption_type': 'KLAP', 'login_version': 2, \
-'https': False}, 'uses_http': True}
+'https': False, 'http_port': 80}}
>>> later_device = await Device.connect(config=Device.Config.from_dict(config_dict))
>>> print(later_device.alias) # Alias is available as connect() calls update()
@@ -69,6 +69,7 @@ class DeviceFamily(Enum):
IotSmartPlugSwitch = "IOT.SMARTPLUGSWITCH"
IotSmartBulb = "IOT.SMARTBULB"
+ IotIpCamera = "IOT.IPCAMERA"
SmartKasaPlug = "SMART.KASAPLUG"
SmartKasaSwitch = "SMART.KASASWITCH"
SmartTapoPlug = "SMART.TAPOPLUG"
@@ -78,6 +79,8 @@ class DeviceFamily(Enum):
SmartKasaHub = "SMART.KASAHUB"
SmartIpCamera = "SMART.IPCAMERA"
SmartTapoRobovac = "SMART.TAPOROBOVAC"
+ SmartTapoChime = "SMART.TAPOCHIME"
+ SmartTapoDoorbell = "SMART.TAPODOORBELL"
class _DeviceConfigBaseMixin(DataClassJSONMixin):
@@ -97,13 +100,16 @@ class DeviceConnectionParameters(_DeviceConfigBaseMixin):
encryption_type: DeviceEncryptionType
login_version: int | None = None
https: bool = False
+ http_port: int | None = None
@staticmethod
def from_values(
device_family: str,
encryption_type: str,
+ *,
login_version: int | None = None,
https: bool | None = None,
+ http_port: int | None = None,
) -> DeviceConnectionParameters:
"""Return connection parameters from string values."""
try:
@@ -114,6 +120,7 @@ class DeviceConnectionParameters(_DeviceConfigBaseMixin):
DeviceEncryptionType(encryption_type),
login_version,
https,
+ http_port=http_port,
)
except (ValueError, TypeError) as ex:
raise KasaException(
@@ -147,9 +154,12 @@ class DeviceConfig(_DeviceConfigBaseMixin):
DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Xor
)
)
- #: True if the device uses http. Consumers should retrieve rather than set this
- #: in order to determine whether they should pass a custom http client if desired.
- uses_http: bool = False
+
+ @property
+ def uses_http(self) -> bool:
+ """True if the device uses http."""
+ ctype = self.connection_type
+ return ctype.encryption_type is not DeviceEncryptionType.Xor or ctype.https
#: Set a custom http_client for the device to use.
http_client: ClientSession | None = field(
diff --git a/kasa/discover.py b/kasa/discover.py
index 771c3f5c..a943ddd4 100755
--- a/kasa/discover.py
+++ b/kasa/discover.py
@@ -22,7 +22,7 @@ Discovery returns a dict of {ip: discovered devices}:
>>>
>>> found_devices = await Discover.discover()
>>> [dev.model for dev in found_devices.values()]
-['KP303(UK)', 'HS110(EU)', 'L530E', 'KL430(US)', 'HS220(US)']
+['KP303', 'HS110', 'L530E', 'KL430', 'HS220']
You can pass username and password for devices requiring authentication
@@ -65,17 +65,17 @@ It is also possible to pass a coroutine to be executed for each found device:
>>> print(f"Discovered {dev.alias} (model: {dev.model})")
>>>
>>> devices = await Discover.discover(on_discovered=print_dev_info, credentials=creds)
-Discovered Bedroom Power Strip (model: KP303(UK))
-Discovered Bedroom Lamp Plug (model: HS110(EU))
+Discovered Bedroom Power Strip (model: KP303)
+Discovered Bedroom Lamp Plug (model: HS110)
Discovered Living Room Bulb (model: L530)
-Discovered Bedroom Lightstrip (model: KL430(US))
-Discovered Living Room Dimmer Switch (model: HS220(US))
+Discovered Bedroom Lightstrip (model: KL430)
+Discovered Living Room Dimmer Switch (model: HS220)
Discovering a single device returns a kasa.Device object.
>>> device = await Discover.discover_single("127.0.0.1", credentials=creds)
>>> device.model
-'KP303(UK)'
+'KP303'
"""
@@ -99,6 +99,7 @@ from typing import (
Annotated,
Any,
NamedTuple,
+ TypedDict,
cast,
)
@@ -123,7 +124,7 @@ from kasa.exceptions import (
TimeoutError,
UnsupportedDeviceError,
)
-from kasa.iot.iotdevice import IotDevice
+from kasa.iot.iotdevice import IotDevice, _extract_sys_info
from kasa.json import DataClassJSONMixin
from kasa.json import dumps as json_dumps
from kasa.json import loads as json_loads
@@ -145,20 +146,46 @@ class ConnectAttempt(NamedTuple):
protocol: type
transport: type
device: type
+ https: bool
+
+
+class DiscoveredMeta(TypedDict):
+ """Meta info about discovery response."""
+
+ ip: str
+ port: int
+
+
+class DiscoveredRaw(TypedDict):
+ """Try to connect attempt."""
+
+ meta: DiscoveredMeta
+ discovery_response: dict
OnDiscoveredCallable = Callable[[Device], Coroutine]
+OnDiscoveredRawCallable = Callable[[DiscoveredRaw], None]
OnUnsupportedCallable = Callable[[UnsupportedDeviceError], Coroutine]
OnConnectAttemptCallable = Callable[[ConnectAttempt, bool], None]
DeviceDict = dict[str, Device]
+DECRYPTED_REDACTORS: dict[str, Callable[[Any], Any] | None] = {
+ "connect_ssid": lambda x: "#MASKED_SSID#" if x else "",
+ "device_id": lambda x: "REDACTED_" + x[9::],
+ "owner": lambda x: "REDACTED_" + x[9::],
+}
+
NEW_DISCOVERY_REDACTORS: dict[str, Callable[[Any], Any] | None] = {
"device_id": lambda x: "REDACTED_" + x[9::],
+ "device_name": lambda x: "#MASKED_NAME#" if x else "",
"owner": lambda x: "REDACTED_" + x[9::],
"mac": mask_mac,
"master_device_id": lambda x: "REDACTED_" + x[9::],
"group_id": lambda x: "REDACTED_" + x[9::],
"group_name": lambda x: "I01BU0tFRF9TU0lEIw==",
+ "encrypt_info": lambda x: {**x, "key": "", "data": ""},
+ "ip": lambda x: x, # don't redact but keep listed here for dump_devinfo
+ "decrypted_data": lambda x: redact_data(x, DECRYPTED_REDACTORS),
}
@@ -216,6 +243,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
self,
*,
on_discovered: OnDiscoveredCallable | None = None,
+ on_discovered_raw: OnDiscoveredRawCallable | None = None,
target: str = "255.255.255.255",
discovery_packets: int = 3,
discovery_timeout: int = 5,
@@ -240,6 +268,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
self.unsupported_device_exceptions: dict = {}
self.invalid_device_exceptions: dict = {}
self.on_unsupported = on_unsupported
+ self.on_discovered_raw = on_discovered_raw
self.credentials = credentials
self.timeout = timeout
self.discovery_timeout = discovery_timeout
@@ -329,12 +358,22 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
config.timeout = self.timeout
try:
if port == self.discovery_port:
- device = Discover._get_device_instance_legacy(data, config)
+ json_func = Discover._get_discovery_json_legacy
+ device_func = Discover._get_device_instance_legacy
elif port == Discover.DISCOVERY_PORT_2:
- config.uses_http = True
- device = Discover._get_device_instance(data, config)
+ json_func = Discover._get_discovery_json
+ device_func = Discover._get_device_instance
else:
return
+ info = json_func(data, ip)
+ if self.on_discovered_raw is not None:
+ self.on_discovered_raw(
+ {
+ "discovery_response": info,
+ "meta": {"ip": ip, "port": port},
+ }
+ )
+ device = device_func(info, config)
except UnsupportedDeviceError as udex:
_LOGGER.debug("Unsupported device found at %s << %s", ip, udex)
self.unsupported_device_exceptions[ip] = udex
@@ -391,6 +430,7 @@ class Discover:
*,
target: str = "255.255.255.255",
on_discovered: OnDiscoveredCallable | None = None,
+ on_discovered_raw: OnDiscoveredRawCallable | None = None,
discovery_timeout: int = 5,
discovery_packets: int = 3,
interface: str | None = None,
@@ -421,6 +461,8 @@ class Discover:
:param target: The target address where to send the broadcast discovery
queries if multi-homing (e.g. 192.168.xxx.255).
:param on_discovered: coroutine to execute on discovery
+ :param on_discovered_raw: Optional callback once discovered json is loaded
+ before any attempt to deserialize it and create devices
:param discovery_timeout: Seconds to wait for responses, defaults to 5
:param discovery_packets: Number of discovery packets to broadcast
:param interface: Bind to specific interface
@@ -443,6 +485,7 @@ class Discover:
discovery_packets=discovery_packets,
interface=interface,
on_unsupported=on_unsupported,
+ on_discovered_raw=on_discovered_raw,
credentials=credentials,
timeout=timeout,
discovery_timeout=discovery_timeout,
@@ -455,7 +498,7 @@ class Discover:
try:
_LOGGER.debug("Waiting %s seconds for responses...", discovery_timeout)
await protocol.wait_for_discovery_to_complete()
- except KasaException as ex:
+ except (KasaException, asyncio.CancelledError) as ex:
for device in protocol.discovered_devices.values():
await device.protocol.close()
raise ex
@@ -476,6 +519,7 @@ class Discover:
credentials: Credentials | None = None,
username: str | None = None,
password: str | None = None,
+ on_discovered_raw: OnDiscoveredRawCallable | None = None,
on_unsupported: OnUnsupportedCallable | None = None,
) -> Device | None:
"""Discover a single device by the given IP address.
@@ -493,6 +537,9 @@ class Discover:
username and password are ignored if provided.
:param username: Username for devices that require authentication
:param password: Password for devices that require authentication
+ :param on_discovered_raw: Optional callback once discovered json is loaded
+ before any attempt to deserialize it and create devices
+ :param on_unsupported: Optional callback when unsupported devices are discovered
:rtype: SmartDevice
:return: Object for querying/controlling found device.
"""
@@ -529,6 +576,7 @@ class Discover:
credentials=credentials,
timeout=timeout,
discovery_timeout=discovery_timeout,
+ on_discovered_raw=on_discovered_raw,
),
local_addr=("0.0.0.0", 0), # noqa: S104
)
@@ -586,12 +634,14 @@ class Discover:
Device.Family.SmartTapoPlug,
Device.Family.IotSmartPlugSwitch,
Device.Family.SmartIpCamera,
+ Device.Family.SmartTapoRobovac,
+ Device.Family.IotIpCamera,
}
candidates: dict[
- tuple[type[BaseProtocol], type[BaseTransport], type[Device]],
+ tuple[type[BaseProtocol], type[BaseTransport], type[Device], bool],
tuple[BaseProtocol, DeviceConfig],
] = {
- (type(protocol), type(protocol._transport), device_class): (
+ (type(protocol), type(protocol._transport), device_class, https): (
protocol,
config,
)
@@ -615,10 +665,9 @@ class Discover:
port_override=port,
credentials=credentials,
http_client=http_client,
- uses_http=encrypt is not Device.EncryptionType.Xor,
)
)
- and (protocol := get_protocol(config))
+ and (protocol := get_protocol(config, strict=True))
and (
device_class := get_device_class_from_family(
device_family.value, https=https, require_exact=True
@@ -628,9 +677,14 @@ class Discover:
for key, val in candidates.items():
try:
prot, config = val
+ _LOGGER.debug("Trying to connect with %s", prot.__class__.__name__)
dev = await _connect(config, prot)
- except Exception:
- _LOGGER.debug("Unable to connect with %s", prot)
+ except Exception as ex:
+ _LOGGER.debug(
+ "Unable to connect with %s: %s",
+ prot.__class__.__name__,
+ ex,
+ )
if on_attempt:
ca = tuple.__new__(ConnectAttempt, key)
on_attempt(ca, False)
@@ -638,6 +692,7 @@ class Discover:
if on_attempt:
ca = tuple.__new__(ConnectAttempt, key)
on_attempt(ca, True)
+ _LOGGER.debug("Found working protocol %s", prot.__class__.__name__)
return dev
finally:
await prot.close()
@@ -666,33 +721,43 @@ class Discover:
return get_device_class_from_sys_info(info)
@staticmethod
- def _get_device_instance_legacy(data: bytes, config: DeviceConfig) -> IotDevice:
- """Get SmartDevice from legacy 9999 response."""
+ def _get_discovery_json_legacy(data: bytes, ip: str) -> dict:
+ """Get discovery json from legacy 9999 response."""
try:
info = json_loads(XorEncryption.decrypt(data))
except Exception as ex:
raise KasaException(
- f"Unable to read response from device: {config.host}: {ex}"
+ f"Unable to read response from device: {ip}: {ex}"
) from ex
+ return info
+ @staticmethod
+ def _get_device_instance_legacy(info: dict, config: DeviceConfig) -> Device:
+ """Get IotDevice from legacy 9999 response."""
if _LOGGER.isEnabledFor(logging.DEBUG):
data = redact_data(info, IOT_REDACTORS) if Discover._redact_data else info
_LOGGER.debug("[DISCOVERY] %s << %s", config.host, pf(data))
device_class = cast(type[IotDevice], Discover._get_device_class(info))
device = device_class(config.host, config=config)
- sys_info = info["system"]["get_sysinfo"]
- if device_type := sys_info.get("mic_type", sys_info.get("type")):
- config.connection_type = DeviceConnectionParameters.from_values(
- device_family=device_type,
- encryption_type=DeviceEncryptionType.Xor.value,
- )
+ sys_info = _extract_sys_info(info)
+ device_type = sys_info.get("mic_type", sys_info.get("type"))
+ login_version = (
+ sys_info.get("stream_version") if device_type == "IOT.IPCAMERA" else None
+ )
+ config.connection_type = DeviceConnectionParameters.from_values(
+ device_family=device_type,
+ encryption_type=DeviceEncryptionType.Xor.value,
+ https=device_type == "IOT.IPCAMERA",
+ login_version=login_version,
+ )
device.protocol = get_protocol(config) # type: ignore[assignment]
device.update_from_discover_info(info)
return device
@staticmethod
def _decrypt_discovery_data(discovery_result: DiscoveryResult) -> None:
+ debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
if TYPE_CHECKING:
assert discovery_result.encrypt_info
assert _AesDiscoveryQuery.keypair
@@ -708,22 +773,80 @@ class Discover:
session = AesEncyptionSession(key, iv)
decrypted_data = session.decrypt(encrypted_data)
- discovery_result.decrypted_data = json_loads(decrypted_data)
+ result = json_loads(decrypted_data)
+ if debug_enabled:
+ data = (
+ redact_data(result, DECRYPTED_REDACTORS)
+ if Discover._redact_data
+ else result
+ )
+ _LOGGER.debug(
+ "Decrypted encrypt_info for %s: %s",
+ discovery_result.ip,
+ pf(data),
+ )
+ discovery_result.decrypted_data = result
+
+ @staticmethod
+ def _get_discovery_json(data: bytes, ip: str) -> dict:
+ """Get discovery json from the new 20002 response."""
+ try:
+ info = json_loads(data[16:])
+ except Exception as ex:
+ _LOGGER.debug("Got invalid response from device %s: %s", ip, data)
+ raise KasaException(
+ f"Unable to read response from device: {ip}: {ex}"
+ ) from ex
+ return info
+
+ @staticmethod
+ def _get_connection_parameters(
+ discovery_result: DiscoveryResult,
+ ) -> DeviceConnectionParameters:
+ """Get connection parameters from the discovery result."""
+ type_ = discovery_result.device_type
+ if (encrypt_schm := discovery_result.mgt_encrypt_schm) is None:
+ raise UnsupportedDeviceError(
+ f"Unsupported device {discovery_result.ip} of type {type_} "
+ "with no mgt_encrypt_schm",
+ discovery_result=discovery_result.to_dict(),
+ host=discovery_result.ip,
+ )
+
+ if not (encrypt_type := encrypt_schm.encrypt_type) and (
+ encrypt_info := discovery_result.encrypt_info
+ ):
+ encrypt_type = encrypt_info.sym_schm
+
+ if not (login_version := encrypt_schm.lv) and (
+ et := discovery_result.encrypt_type
+ ):
+ # Known encrypt types are ["1","2"] and ["3"]
+ # Reuse the login_version attribute to pass the max to transport
+ login_version = max([int(i) for i in et])
+
+ if not encrypt_type:
+ raise UnsupportedDeviceError(
+ f"Unsupported device {discovery_result.ip} of type {type_} "
+ + "with no encryption type",
+ discovery_result=discovery_result.to_dict(),
+ host=discovery_result.ip,
+ )
+ return DeviceConnectionParameters.from_values(
+ type_,
+ encrypt_type,
+ login_version=login_version,
+ https=encrypt_schm.is_support_https,
+ http_port=encrypt_schm.http_port,
+ )
@staticmethod
def _get_device_instance(
- data: bytes,
+ info: dict,
config: DeviceConfig,
) -> Device:
"""Get SmartDevice from the new 20002 response."""
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
- try:
- info = json_loads(data[16:])
- except Exception as ex:
- _LOGGER.debug("Got invalid response from device %s: %s", config.host, data)
- raise KasaException(
- f"Unable to read response from device: {config.host}: {ex}"
- ) from ex
try:
discovery_result = DiscoveryResult.from_dict(info["result"])
@@ -752,56 +875,26 @@ class Discover:
Discover._decrypt_discovery_data(discovery_result)
except Exception:
_LOGGER.exception(
- "Unable to decrypt discovery data %s: %s", config.host, data
+ "Unable to decrypt discovery data %s: %s",
+ config.host,
+ redact_data(info, NEW_DISCOVERY_REDACTORS),
)
-
type_ = discovery_result.device_type
- if (encrypt_schm := discovery_result.mgt_encrypt_schm) is None:
- raise UnsupportedDeviceError(
- f"Unsupported device {config.host} of type {type_} "
- "with no mgt_encrypt_schm",
- discovery_result=discovery_result.to_dict(),
- host=config.host,
- )
-
try:
- if not (encrypt_type := encrypt_schm.encrypt_type) and (
- encrypt_info := discovery_result.encrypt_info
- ):
- encrypt_type = encrypt_info.sym_schm
-
- if (
- not (login_version := encrypt_schm.lv)
- and (et := discovery_result.encrypt_type)
- and et == ["3"]
- ):
- login_version = 2
-
- if not encrypt_type:
- raise UnsupportedDeviceError(
- f"Unsupported device {config.host} of type {type_} "
- + "with no encryption type",
- discovery_result=discovery_result.to_dict(),
- host=config.host,
- )
- config.connection_type = DeviceConnectionParameters.from_values(
- type_,
- encrypt_type,
- login_version,
- encrypt_schm.is_support_https,
- )
+ conn_params = Discover._get_connection_parameters(discovery_result)
+ config.connection_type = conn_params
except KasaException as ex:
+ if isinstance(ex, UnsupportedDeviceError):
+ raise
raise UnsupportedDeviceError(
f"Unsupported device {config.host} of type {type_} "
- + f"with encrypt_type {encrypt_schm.encrypt_type}",
+ + f"with encrypt_scheme {discovery_result.mgt_encrypt_schm}",
discovery_result=discovery_result.to_dict(),
host=config.host,
) from ex
if (
- device_class := get_device_class_from_family(
- type_, https=encrypt_schm.is_support_https
- )
+ device_class := get_device_class_from_family(type_, https=conn_params.https)
) is None:
_LOGGER.debug("Got unsupported device type: %s", type_)
raise UnsupportedDeviceError(
diff --git a/kasa/exceptions.py b/kasa/exceptions.py
index a0ecbf8f..1c764ad7 100644
--- a/kasa/exceptions.py
+++ b/kasa/exceptions.py
@@ -127,11 +127,14 @@ class SmartErrorCode(IntEnum):
DST_ERROR = -2301
DST_SAVE_ERROR = -2302
+ VACUUM_BATTERY_LOW = -3001
+
SYSTEM_ERROR = -40101
INVALID_ARGUMENTS = -40209
# Camera error codes
SESSION_EXPIRED = -40401
+ BAD_USERNAME = -40411 # determined from testing
HOMEKIT_LOGIN_FAIL = -40412
DEVICE_BLOCKED = -40404
DEVICE_FACTORY = -40405
diff --git a/kasa/feature.py b/kasa/feature.py
index d747338d..3c6beb0d 100644
--- a/kasa/feature.py
+++ b/kasa/feature.py
@@ -24,8 +24,8 @@ State (state): True
Signal Level (signal_level): 2
RSSI (rssi): -52
SSID (ssid): #MASKED_SSID#
-Overheated (overheated): False
Reboot (reboot):
+Device time (device_time): 2024-02-23 02:40:15+01:00
Brightness (brightness): 100
Cloud connection (cloud_connection): True
HSV (hsv): HSV(hue=0, saturation=100, value=100)
@@ -39,7 +39,7 @@ Light effect (light_effect): Off
Light preset (light_preset): Not set
Smooth transition on (smooth_transition_on): 2
Smooth transition off (smooth_transition_off): 2
-Device time (device_time): 2024-02-23 02:40:15+01:00
+Overheated (overheated): False
To see whether a device supports a feature, check for the existence of it:
@@ -76,6 +76,7 @@ from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from .device import Device
+ from .module import Module
_LOGGER = logging.getLogger(__name__)
@@ -142,7 +143,7 @@ class Feature:
#: Callable coroutine or name of the method that allows changing the value
attribute_setter: str | Callable[..., Coroutine[Any, Any, Any]] | None = None
#: Container storing the data, this overrides 'device' for getters
- container: Any = None
+ container: Device | Module | None = None
#: Icon suggestion
icon: str | None = None
#: Attribute containing the name of the unit getter property.
@@ -295,9 +296,13 @@ class Feature:
if self.precision_hint is not None and isinstance(value, float):
value = round(value, self.precision_hint)
+ if isinstance(value, Enum):
+ value = repr(value)
s = f"{self.name} ({self.id}): {value}"
- if self.unit is not None:
- s += f" {self.unit}"
+ if (unit := self.unit) is not None:
+ if isinstance(unit, Enum):
+ unit = repr(unit)
+ s += f" {unit}"
if self.type == Feature.Type.Number:
s += f" (range: {self.minimum_value}-{self.maximum_value})"
diff --git a/kasa/httpclient.py b/kasa/httpclient.py
index 87e3626a..31d8dfbb 100644
--- a/kasa/httpclient.py
+++ b/kasa/httpclient.py
@@ -113,10 +113,23 @@ class HttpClient:
ssl=ssl,
)
async with resp:
- if resp.status == 200:
- response_data = await resp.read()
- if return_json:
+ response_data = await resp.read()
+
+ if resp.status == 200:
+ if return_json:
+ response_data = json_loads(response_data.decode())
+ else:
+ _LOGGER.debug(
+ "Device %s received status code %s with response %s",
+ self._config.host,
+ resp.status,
+ str(response_data),
+ )
+ if response_data and return_json:
+ try:
response_data = json_loads(response_data.decode())
+ except Exception:
+ _LOGGER.debug("Device %s response could not be parsed as json")
except (aiohttp.ServerDisconnectedError, aiohttp.ClientOSError) as ex:
if not self._wait_between_requests:
diff --git a/kasa/interfaces/light.py b/kasa/interfaces/light.py
index 1d99f846..fdcfe46d 100644
--- a/kasa/interfaces/light.py
+++ b/kasa/interfaces/light.py
@@ -23,13 +23,13 @@ Get the light module to interact:
>>> light = dev.modules[Module.Light]
-You can use the ``is_``-prefixed properties to check for supported features:
+You can use the ``has_feature()`` method to check for supported features:
->>> light.is_dimmable
+>>> light.has_feature("brightness")
True
->>> light.is_color
+>>> light.has_feature("hsv")
True
->>> light.is_variable_color_temp
+>>> light.has_feature("color_temp")
True
All known bulbs support changing the brightness:
@@ -43,8 +43,9 @@ All known bulbs support changing the brightness:
Bulbs supporting color temperature can be queried for the supported range:
->>> light.valid_temperature_range
-ColorTempRange(min=2500, max=6500)
+>>> if color_temp_feature := light.get_feature("color_temp"):
+>>> print(f"{color_temp_feature.minimum_value}, {color_temp_feature.maximum_value}")
+2500, 6500
>>> await light.set_color_temp(3000)
>>> await dev.update()
>>> light.color_temp
@@ -64,8 +65,10 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
-from typing import Annotated, NamedTuple
+from typing import TYPE_CHECKING, Annotated, Any, NamedTuple
+from warnings import warn
+from ..exceptions import KasaException
from ..module import FeatureAttribute, Module
@@ -99,34 +102,6 @@ class HSV(NamedTuple):
class Light(Module, ABC):
"""Base class for TP-Link Light."""
- @property
- @abstractmethod
- def is_dimmable(self) -> bool:
- """Whether the light supports brightness changes."""
-
- @property
- @abstractmethod
- def is_color(self) -> bool:
- """Whether the bulb supports color changes."""
-
- @property
- @abstractmethod
- def is_variable_color_temp(self) -> bool:
- """Whether the bulb supports color temperature changes."""
-
- @property
- @abstractmethod
- def valid_temperature_range(self) -> ColorTempRange:
- """Return the device-specific white temperature range (in Kelvin).
-
- :return: White temperature range in Kelvin (minimum, maximum)
- """
-
- @property
- @abstractmethod
- def has_effects(self) -> bool:
- """Return True if the device supports effects."""
-
@property
@abstractmethod
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
@@ -196,3 +171,44 @@ class Light(Module, ABC):
@abstractmethod
async def set_state(self, state: LightState) -> dict:
"""Set the light state."""
+
+ def _deprecated_valid_temperature_range(self) -> ColorTempRange:
+ if not (temp := self.get_feature("color_temp")):
+ raise KasaException("Color temperature not supported")
+ return ColorTempRange(temp.minimum_value, temp.maximum_value)
+
+ def _deprecated_attributes(self, dep_name: str) -> str | None:
+ map: dict[str, str] = {
+ "is_color": "hsv",
+ "is_dimmable": "brightness",
+ "is_variable_color_temp": "color_temp",
+ }
+ return map.get(dep_name)
+
+ if not TYPE_CHECKING:
+
+ def __getattr__(self, name: str) -> Any:
+ if name == "valid_temperature_range":
+ msg = (
+ "valid_temperature_range is deprecated, use "
+ 'get_feature("color_temp") minimum_value '
+ " and maximum_value instead"
+ )
+ warn(msg, DeprecationWarning, stacklevel=2)
+ res = self._deprecated_valid_temperature_range()
+ return res
+
+ if name == "has_effects":
+ msg = (
+ "has_effects is deprecated, check `Module.LightEffect "
+ "in device.modules` instead"
+ )
+ warn(msg, DeprecationWarning, stacklevel=2)
+ return Module.LightEffect in self._device.modules
+
+ if attr := self._deprecated_attributes(name):
+ msg = f'{name} is deprecated, use has_feature("{attr}") instead'
+ warn(msg, DeprecationWarning, stacklevel=2)
+ return self.has_feature(attr)
+
+ raise AttributeError(f"Energy module has no attribute {name!r}")
diff --git a/kasa/interfaces/lighteffect.py b/kasa/interfaces/lighteffect.py
index 9a69f2d0..fa50dd3e 100644
--- a/kasa/interfaces/lighteffect.py
+++ b/kasa/interfaces/lighteffect.py
@@ -13,8 +13,7 @@ Living Room Bulb
Light effects are accessed via the LightPreset module. To list available presets
->>> if dev.modules[Module.Light].has_effects:
->>> light_effect = dev.modules[Module.LightEffect]
+>>> light_effect = dev.modules[Module.LightEffect]
>>> light_effect.effect_list
['Off', 'Party', 'Relax']
diff --git a/kasa/iot/__init__.py b/kasa/iot/__init__.py
index 536679ca..3b5b01c6 100644
--- a/kasa/iot/__init__.py
+++ b/kasa/iot/__init__.py
@@ -1,6 +1,7 @@
"""Package for supporting legacy kasa devices."""
from .iotbulb import IotBulb
+from .iotcamera import IotCamera
from .iotdevice import IotDevice
from .iotdimmer import IotDimmer
from .iotlightstrip import IotLightStrip
@@ -15,4 +16,5 @@ __all__ = [
"IotDimmer",
"IotLightStrip",
"IotWallSwitch",
+ "IotCamera",
]
diff --git a/kasa/iot/iotcamera.py b/kasa/iot/iotcamera.py
new file mode 100644
index 00000000..8965948c
--- /dev/null
+++ b/kasa/iot/iotcamera.py
@@ -0,0 +1,42 @@
+"""Module for cameras."""
+
+from __future__ import annotations
+
+import logging
+from datetime import datetime, tzinfo
+
+from ..device_type import DeviceType
+from ..deviceconfig import DeviceConfig
+from ..protocols import BaseProtocol
+from .iotdevice import IotDevice
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class IotCamera(IotDevice):
+ """Representation of a TP-Link Camera."""
+
+ def __init__(
+ self,
+ host: str,
+ *,
+ config: DeviceConfig | None = None,
+ protocol: BaseProtocol | None = None,
+ ) -> None:
+ super().__init__(host=host, config=config, protocol=protocol)
+ self._device_type = DeviceType.Camera
+
+ @property
+ def time(self) -> datetime:
+ """Get the camera's time."""
+ return datetime.fromtimestamp(self.sys_info["system_time"])
+
+ @property
+ def timezone(self) -> tzinfo:
+ """Get the camera's timezone."""
+ return None # type: ignore
+
+ @property # type: ignore
+ def is_on(self) -> bool:
+ """Return whether device is on."""
+ return True
diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py
index f23ebc8b..851f21cc 100755
--- a/kasa/iot/iotdevice.py
+++ b/kasa/iot/iotdevice.py
@@ -22,7 +22,7 @@ from datetime import datetime, timedelta, tzinfo
from typing import TYPE_CHECKING, Any, cast
from warnings import warn
-from ..device import Device, WifiNetwork, _DeviceInfo
+from ..device import Device, DeviceInfo, WifiNetwork
from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..exceptions import KasaException
@@ -43,7 +43,7 @@ def requires_update(f: Callable) -> Any:
@functools.wraps(f)
async def wrapped(*args: Any, **kwargs: Any) -> Any:
self = args[0]
- if self._last_update is None and (
+ if not self._last_update and (
self._sys_info is None or f.__name__ not in self._sys_info
):
raise KasaException("You need to await update() to access the data")
@@ -54,7 +54,7 @@ def requires_update(f: Callable) -> Any:
@functools.wraps(f)
def wrapped(*args: Any, **kwargs: Any) -> Any:
self = args[0]
- if self._last_update is None and (
+ if not self._last_update and (
self._sys_info is None or f.__name__ not in self._sys_info
):
raise KasaException("You need to await update() to access the data")
@@ -70,6 +70,16 @@ def _parse_features(features: str) -> set[str]:
return set(features.split(":"))
+def _extract_sys_info(info: dict[str, Any]) -> dict[str, Any]:
+ """Return the system info structure."""
+ sysinfo_default = info.get("system", {}).get("get_sysinfo", {})
+ sysinfo_nest = sysinfo_default.get("system", {})
+
+ if len(sysinfo_nest) > len(sysinfo_default) and isinstance(sysinfo_nest, dict):
+ return sysinfo_nest
+ return sysinfo_default
+
+
class IotDevice(Device):
"""Base class for all supported device types.
@@ -102,7 +112,7 @@ class IotDevice(Device):
>>> dev.alias
Bedroom Lamp Plug
>>> dev.model
- HS110(EU)
+ HS110
>>> dev.rssi
-71
>>> dev.mac
@@ -300,18 +310,18 @@ class IotDevice(Device):
# If this is the initial update, check only for the sysinfo
# This is necessary as some devices crash on unexpected modules
# See #105, #120, #161
- if self._last_update is None:
+ if not self._last_update:
_LOGGER.debug("Performing the initial update to obtain sysinfo")
response = await self.protocol.query(req)
self._last_update = response
- self._set_sys_info(response["system"]["get_sysinfo"])
+ self._set_sys_info(_extract_sys_info(response))
if not self._modules:
await self._initialize_modules()
await self._modular_update(req)
- self._set_sys_info(self._last_update["system"]["get_sysinfo"])
+ self._set_sys_info(_extract_sys_info(self._last_update))
for module in self._modules.values():
await module._post_update_hook()
@@ -442,7 +452,9 @@ class IotDevice(Device):
# This allows setting of some info properties directly
# from partial discovery info that will then be found
# by the requires_update decorator
- self._set_sys_info(info)
+ discovery_model = info["device_model"]
+ no_region_model, _, _ = discovery_model.partition("(")
+ self._set_sys_info({**info, "model": no_region_model})
def _set_sys_info(self, sys_info: dict[str, Any]) -> None:
"""Set sys_info."""
@@ -461,18 +473,13 @@ class IotDevice(Device):
"""
return self._sys_info # type: ignore
- @property # type: ignore
- @requires_update
- def model(self) -> str:
- """Return device model."""
- sys_info = self._sys_info
- return str(sys_info["model"])
-
@property
@requires_update
- def _model_region(self) -> str:
- """Return device full model name and region."""
- return self.model
+ def model(self) -> str:
+ """Returns the device model."""
+ if self._last_update:
+ return self.device_info.short_name
+ return self._sys_info["model"]
@property # type: ignore
def alias(self) -> str | None:
@@ -705,10 +712,13 @@ class IotDevice(Device):
@staticmethod
def _get_device_type_from_sys_info(info: dict[str, Any]) -> DeviceType:
"""Find SmartDevice subclass for device described by passed data."""
+ if "system" in info.get("system", {}).get("get_sysinfo", {}):
+ return DeviceType.Camera
+
if "system" not in info or "get_sysinfo" not in info["system"]:
raise KasaException("No 'system' or 'get_sysinfo' in response")
- sysinfo: dict[str, Any] = info["system"]["get_sysinfo"]
+ sysinfo: dict[str, Any] = _extract_sys_info(info)
type_: str | None = sysinfo.get("type", sysinfo.get("mic_type"))
if type_ is None:
raise KasaException("Unable to find the device type field!")
@@ -728,15 +738,16 @@ class IotDevice(Device):
return DeviceType.LightStrip
return DeviceType.Bulb
+
_LOGGER.warning("Unknown device type %s, falling back to plug", type_)
return DeviceType.Plug
@staticmethod
def _get_device_info(
info: dict[str, Any], discovery_info: dict[str, Any] | None
- ) -> _DeviceInfo:
+ ) -> DeviceInfo:
"""Get model information for a device."""
- sys_info = info["system"]["get_sysinfo"]
+ sys_info = _extract_sys_info(info)
# Get model and region info
region = None
@@ -752,7 +763,7 @@ class IotDevice(Device):
firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
auth = bool(discovery_info and ("mgt_encrypt_schm" in discovery_info))
- return _DeviceInfo(
+ return DeviceInfo(
short_name=long_name,
long_name=long_name,
brand="kasa",
diff --git a/kasa/iot/modules/light.py b/kasa/iot/modules/light.py
index 5fdbf014..fa953590 100644
--- a/kasa/iot/modules/light.py
+++ b/kasa/iot/modules/light.py
@@ -3,13 +3,14 @@
from __future__ import annotations
from dataclasses import asdict
-from typing import TYPE_CHECKING, cast
+from typing import TYPE_CHECKING, Annotated, cast
from ...device_type import DeviceType
from ...exceptions import KasaException
from ...feature import Feature
-from ...interfaces.light import HSV, ColorTempRange, LightState
+from ...interfaces.light import HSV, LightState
from ...interfaces.light import Light as LightInterface
+from ...module import FeatureAttribute
from ..iotmodule import IotModule
if TYPE_CHECKING:
@@ -32,7 +33,7 @@ class Light(IotModule, LightInterface):
super()._initialize_features()
device = self._device
- if self._device._is_dimmable:
+ if device._is_dimmable:
self._add_feature(
Feature(
device,
@@ -46,7 +47,9 @@ class Light(IotModule, LightInterface):
category=Feature.Category.Primary,
)
)
- if self._device._is_variable_color_temp:
+ if device._is_variable_color_temp:
+ if TYPE_CHECKING:
+ assert isinstance(device, IotBulb)
self._add_feature(
Feature(
device=device,
@@ -55,12 +58,12 @@ class Light(IotModule, LightInterface):
container=self,
attribute_getter="color_temp",
attribute_setter="set_color_temp",
- range_getter="valid_temperature_range",
+ range_getter=lambda: device._valid_temperature_range,
category=Feature.Category.Primary,
type=Feature.Type.Number,
)
)
- if self._device._is_color:
+ if device._is_color:
self._add_feature(
Feature(
device=device,
@@ -90,18 +93,13 @@ class Light(IotModule, LightInterface):
return None
@property # type: ignore
- def is_dimmable(self) -> int:
- """Whether the bulb supports brightness changes."""
- return self._device._is_dimmable
-
- @property # type: ignore
- def brightness(self) -> int:
+ def brightness(self) -> Annotated[int, FeatureAttribute()]:
"""Return the current brightness in percentage."""
return self._device._brightness
async def set_brightness(
self, brightness: int, *, transition: int | None = None
- ) -> dict:
+ ) -> Annotated[dict, FeatureAttribute()]:
"""Set the brightness in percentage. A value of 0 will turn off the light.
:param int brightness: brightness in percent
@@ -112,28 +110,7 @@ class Light(IotModule, LightInterface):
)
@property
- def is_color(self) -> bool:
- """Whether the light supports color changes."""
- if (bulb := self._get_bulb_device()) is None:
- return False
- return bulb._is_color
-
- @property
- def is_variable_color_temp(self) -> bool:
- """Whether the bulb supports color temperature changes."""
- if (bulb := self._get_bulb_device()) is None:
- return False
- return bulb._is_variable_color_temp
-
- @property
- def has_effects(self) -> bool:
- """Return True if the device supports effects."""
- if (bulb := self._get_bulb_device()) is None:
- return False
- return bulb._has_effects
-
- @property
- def hsv(self) -> HSV:
+ def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
"""Return the current HSV state of the bulb.
:return: hue, saturation and value (degrees, %, %)
@@ -149,7 +126,7 @@ class Light(IotModule, LightInterface):
value: int | None = None,
*,
transition: int | None = None,
- ) -> dict:
+ ) -> Annotated[dict, FeatureAttribute()]:
"""Set new HSV.
Note, transition is not supported and will be ignored.
@@ -164,19 +141,7 @@ class Light(IotModule, LightInterface):
return await bulb._set_hsv(hue, saturation, value, transition=transition)
@property
- def valid_temperature_range(self) -> ColorTempRange:
- """Return the device-specific white temperature range (in Kelvin).
-
- :return: White temperature range in Kelvin (minimum, maximum)
- """
- if (
- bulb := self._get_bulb_device()
- ) is None or not bulb._is_variable_color_temp:
- raise KasaException("Light does not support colortemp.")
- return bulb._valid_temperature_range
-
- @property
- def color_temp(self) -> int:
+ def color_temp(self) -> Annotated[int, FeatureAttribute()]:
"""Whether the bulb supports color temperature changes."""
if (
bulb := self._get_bulb_device()
@@ -186,7 +151,7 @@ class Light(IotModule, LightInterface):
async def set_color_temp(
self, temp: int, *, brightness: int | None = None, transition: int | None = None
- ) -> dict:
+ ) -> Annotated[dict, FeatureAttribute()]:
"""Set the color temperature of the device in kelvin.
Note, transition is not supported and will be ignored.
@@ -242,17 +207,18 @@ class Light(IotModule, LightInterface):
return self._light_state
async def _post_update_hook(self) -> None:
- if self._device.is_on is False:
+ device = self._device
+ if device.is_on is False:
state = LightState(light_on=False)
else:
state = LightState(light_on=True)
- if self.is_dimmable:
+ if device._is_dimmable:
state.brightness = self.brightness
- if self.is_color:
+ if device._is_color:
hsv = self.hsv
state.hue = hsv.hue
state.saturation = hsv.saturation
- if self.is_variable_color_temp:
+ if device._is_variable_color_temp:
state.color_temp = self.color_temp
self._light_state = state
diff --git a/kasa/iot/modules/lightpreset.py b/kasa/iot/modules/lightpreset.py
index d97bfc4a..76d39860 100644
--- a/kasa/iot/modules/lightpreset.py
+++ b/kasa/iot/modules/lightpreset.py
@@ -85,17 +85,19 @@ class LightPreset(IotModule, LightPresetInterface):
def preset(self) -> str:
"""Return current preset name."""
light = self._device.modules[Module.Light]
+ is_color = light.has_feature("hsv")
+ is_variable_color_temp = light.has_feature("color_temp")
+
brightness = light.brightness
- color_temp = light.color_temp if light.is_variable_color_temp else None
- h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
+ color_temp = light.color_temp if is_variable_color_temp else None
+
+ h, s = (light.hsv.hue, light.hsv.saturation) if is_color else (None, None)
for preset_name, preset in self._presets.items():
if (
preset.brightness == brightness
- and (
- preset.color_temp == color_temp or not light.is_variable_color_temp
- )
- and (preset.hue == h or not light.is_color)
- and (preset.saturation == s or not light.is_color)
+ and (preset.color_temp == color_temp or not is_variable_color_temp)
+ and (preset.hue == h or not is_color)
+ and (preset.saturation == s or not is_color)
):
return preset_name
return self.PRESET_NOT_SET
@@ -107,7 +109,7 @@ class LightPreset(IotModule, LightPresetInterface):
"""Set a light preset for the device."""
light = self._device.modules[Module.Light]
if preset_name == self.PRESET_NOT_SET:
- if light.is_color:
+ if light.has_feature("hsv"):
preset = LightState(hue=0, saturation=0, brightness=100)
else:
preset = LightState(brightness=100)
diff --git a/kasa/json.py b/kasa/json.py
index 21c6fa00..8a0eab7b 100755
--- a/kasa/json.py
+++ b/kasa/json.py
@@ -8,18 +8,24 @@ from typing import Any
try:
import orjson
- def dumps(obj: Any, *, default: Callable | None = None) -> str:
+ def dumps(
+ obj: Any, *, default: Callable | None = None, indent: bool = False
+ ) -> str:
"""Dump JSON."""
- return orjson.dumps(obj).decode()
+ return orjson.dumps(
+ obj, option=orjson.OPT_INDENT_2 if indent else None
+ ).decode()
loads = orjson.loads
except ImportError:
import json
- def dumps(obj: Any, *, default: Callable | None = None) -> str:
+ def dumps(
+ obj: Any, *, default: Callable | None = None, indent: bool = False
+ ) -> str:
"""Dump JSON."""
# Separators specified for consistency with orjson
- return json.dumps(obj, separators=(",", ":"))
+ return json.dumps(obj, separators=(",", ":"), indent=2 if indent else None)
loads = json.loads
diff --git a/kasa/module.py b/kasa/module.py
index 2b2e65f9..6f188b30 100644
--- a/kasa/module.py
+++ b/kasa/module.py
@@ -21,6 +21,9 @@ check for the existence of the module:
>>> print(light.brightness)
100
+.. include:: ../featureattributes.md
+ :parser: myst_parser.sphinx_
+
To see whether a device supports specific functionality, you can check whether the
module has that feature:
@@ -149,10 +152,24 @@ class Module(ABC):
ChildProtection: Final[ModuleName[smart.ChildProtection]] = ModuleName(
"ChildProtection"
)
+ ChildLock: Final[ModuleName[smart.ChildLock]] = ModuleName("ChildLock")
TriggerLogs: Final[ModuleName[smart.TriggerLogs]] = ModuleName("TriggerLogs")
+ ChildSetup: Final[ModuleName[smart.ChildSetup]] = ModuleName("ChildSetup")
+
+ HomeKit: Final[ModuleName[smart.HomeKit]] = ModuleName("HomeKit")
+ Matter: Final[ModuleName[smart.Matter]] = ModuleName("Matter")
# SMARTCAM only modules
Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera")
+ LensMask: Final[ModuleName[smartcam.LensMask]] = ModuleName("LensMask")
+
+ # Vacuum modules
+ Clean: Final[ModuleName[smart.Clean]] = ModuleName("Clean")
+ Consumables: Final[ModuleName[smart.Consumables]] = ModuleName("Consumables")
+ Dustbin: Final[ModuleName[smart.Dustbin]] = ModuleName("Dustbin")
+ Speaker: Final[ModuleName[smart.Speaker]] = ModuleName("Speaker")
+ Mop: Final[ModuleName[smart.Mop]] = ModuleName("Mop")
+ CleanRecords: Final[ModuleName[smart.CleanRecords]] = ModuleName("CleanRecords")
def __init__(self, device: Device, module: str) -> None:
self._device = device
diff --git a/kasa/protocols/__init__.py b/kasa/protocols/__init__.py
index 44130d7f..b994d732 100644
--- a/kasa/protocols/__init__.py
+++ b/kasa/protocols/__init__.py
@@ -2,6 +2,7 @@
from .iotprotocol import IotProtocol
from .protocol import BaseProtocol
+from .smartcamprotocol import SmartCamProtocol
from .smartprotocol import SmartErrorCode, SmartProtocol
__all__ = [
@@ -9,4 +10,5 @@ __all__ = [
"IotProtocol",
"SmartErrorCode",
"SmartProtocol",
+ "SmartCamProtocol",
]
diff --git a/kasa/protocols/iotprotocol.py b/kasa/protocols/iotprotocol.py
index 3bc6c454..1af4ae59 100755
--- a/kasa/protocols/iotprotocol.py
+++ b/kasa/protocols/iotprotocol.py
@@ -25,19 +25,35 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
+
+def _mask_children(children: list[dict[str, Any]]) -> list[dict[str, Any]]:
+ def mask_child(child: dict[str, Any], index: int) -> dict[str, Any]:
+ result = {
+ **child,
+ "id": f"SCRUBBED_CHILD_DEVICE_ID_{index+1}",
+ }
+ # Will leave empty aliases as blank
+ if child.get("alias"):
+ result["alias"] = f"#MASKED_NAME# {index + 1}"
+ return result
+
+ return [mask_child(child, index) for index, child in enumerate(children)]
+
+
REDACTORS: dict[str, Callable[[Any], Any] | None] = {
"latitude": lambda x: 0,
"longitude": lambda x: 0,
"latitude_i": lambda x: 0,
"longitude_i": lambda x: 0,
"deviceId": lambda x: "REDACTED_" + x[9::],
- "id": lambda x: "REDACTED_" + x[9::],
+ "children": _mask_children,
"alias": lambda x: "#MASKED_NAME#" if x else "",
"mac": mask_mac,
"mic_mac": mask_mac,
"ssid": lambda x: "#MASKED_SSID#" if x else "",
"oemId": lambda x: "REDACTED_" + x[9::],
"username": lambda _: "user@example.com", # cnCloud
+ "hwId": lambda x: "REDACTED_" + x[9::],
}
@@ -82,12 +98,26 @@ class IotProtocol(BaseProtocol):
)
raise auex
except _RetryableError as ex:
+ if retry == 0:
+ _LOGGER.debug(
+ "Device %s got a retryable error, will retry %s times: %s",
+ self._host,
+ retry_count,
+ ex,
+ )
await self._transport.reset()
if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self._host, retry)
raise ex
continue
except TimeoutError as ex:
+ if retry == 0:
+ _LOGGER.debug(
+ "Device %s got a timeout error, will retry %s times: %s",
+ self._host,
+ retry_count,
+ ex,
+ )
await self._transport.reset()
if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self._host, retry)
diff --git a/kasa/protocols/protocol.py b/kasa/protocols/protocol.py
index 211a7b5a..fb09b882 100755
--- a/kasa/protocols/protocol.py
+++ b/kasa/protocols/protocol.py
@@ -66,6 +66,8 @@ def redact_data(data: _T, redactors: dict[str, Callable[[Any], Any] | None]) ->
def mask_mac(mac: str) -> str:
"""Return mac address with last two octects blanked."""
+ if len(mac) == 12:
+ return f"{mac[:6]}000000"
delim = ":" if ":" in mac else "-"
rest = delim.join(format(s, "02x") for s in bytes.fromhex("000000"))
return f"{mac[:8]}{delim}{rest}"
diff --git a/kasa/protocols/smartcamprotocol.py b/kasa/protocols/smartcamprotocol.py
index 12caa207..9bf40f7d 100644
--- a/kasa/protocols/smartcamprotocol.py
+++ b/kasa/protocols/smartcamprotocol.py
@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from dataclasses import dataclass
from pprint import pformat as pf
-from typing import Any
+from typing import Any, cast
from ..exceptions import (
AuthenticationError,
@@ -19,7 +19,7 @@ from ..transports.sslaestransport import (
SMART_RETRYABLE_ERRORS,
SmartErrorCode,
)
-from . import SmartProtocol
+from .smartprotocol import SmartProtocol
_LOGGER = logging.getLogger(__name__)
@@ -49,10 +49,13 @@ class SingleRequest:
class SmartCamProtocol(SmartProtocol):
"""Class for SmartCam Protocol."""
- async def _handle_response_lists(
- self, response_result: dict[str, Any], method: str, retry_count: int
- ) -> None:
- pass
+ def _get_list_request(
+ self, method: str, params: dict | None, start_index: int
+ ) -> dict:
+ # All smartcam requests have params
+ params = cast(dict, params)
+ module_name = next(iter(params))
+ return {method: {module_name: {"start_index": start_index}}}
def _handle_response_error_code(
self, resp_dict: dict, method: str, raise_on_error: bool = True
@@ -147,7 +150,9 @@ class SmartCamProtocol(SmartProtocol):
if len(request) == 1 and method in {"get", "set", "do", "multipleRequest"}:
single_request = self._get_smart_camera_single_request(request)
else:
- return await self._execute_multiple_query(request, retry_count)
+ return await self._execute_multiple_query(
+ request, retry_count, iterate_list_pages
+ )
else:
single_request = self._make_smart_camera_single_request(request)
@@ -239,11 +244,15 @@ class _ChildCameraProtocolWrapper(SmartProtocol):
responses = response["multipleRequest"]["responses"]
response_dict = {}
+
+ # Raise errors for single calls
+ raise_on_error = len(requests) == 1
+
for index_id, response in enumerate(responses):
response_data = response["result"]["response_data"]
method = methods[index_id]
self._handle_response_error_code(
- response_data, method, raise_on_error=False
+ response_data, method, raise_on_error=raise_on_error
)
response_dict[method] = response_data.get("result")
diff --git a/kasa/protocols/smartprotocol.py b/kasa/protocols/smartprotocol.py
index 80e76ca6..6b3b03be 100644
--- a/kasa/protocols/smartprotocol.py
+++ b/kasa/protocols/smartprotocol.py
@@ -9,6 +9,7 @@ from __future__ import annotations
import asyncio
import base64
import logging
+import re
import time
import uuid
from collections.abc import Callable
@@ -35,6 +36,18 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
+
+def _mask_area_list(area_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
+ def mask_area(area: dict[str, Any]) -> dict[str, Any]:
+ result = {**area}
+ # Will leave empty names as blank
+ if area.get("name"):
+ result["name"] = "I01BU0tFRF9OQU1FIw==" # #MASKED_NAME#
+ return result
+
+ return [mask_area(area) for area in area_list]
+
+
REDACTORS: dict[str, Callable[[Any], Any] | None] = {
"latitude": lambda x: 0,
"longitude": lambda x: 0,
@@ -45,15 +58,42 @@ REDACTORS: dict[str, Callable[[Any], Any] | None] = {
"original_device_id": lambda x: "REDACTED_" + x[9::], # Strip children
"nickname": lambda x: "I01BU0tFRF9OQU1FIw==" if x else "",
"mac": mask_mac,
- "ssid": lambda x: "I01BU0tFRF9TU0lEIw=" if x else "",
+ "ssid": lambda x: "I01BU0tFRF9TU0lEIw==" if x else "",
"bssid": lambda _: "000000000000",
+ "channel": lambda _: 0,
"oem_id": lambda x: "REDACTED_" + x[9::],
- "setup_code": None, # matter
- "setup_payload": None, # matter
- "mfi_setup_code": None, # mfi_ for homekit
- "mfi_setup_id": None,
- "mfi_token_token": None,
- "mfi_token_uuid": None,
+ "hw_id": lambda x: "REDACTED_" + x[9::],
+ "fw_id": lambda x: "REDACTED_" + x[9::],
+ "setup_code": lambda x: re.sub(r"\w", "0", x), # matter
+ "setup_payload": lambda x: re.sub(r"\w", "0", x), # matter
+ "mfi_setup_code": lambda x: re.sub(r"\w", "0", x), # mfi_ for homekit
+ "mfi_setup_id": lambda x: re.sub(r"\w", "0", x),
+ "mfi_token_token": lambda x: re.sub(r"\w", "0", x),
+ "mfi_token_uuid": lambda x: re.sub(r"\w", "0", x),
+ "ip": lambda x: x, # don't redact but keep listed here for dump_devinfo
+ # smartcam
+ "dev_id": lambda x: "REDACTED_" + x[9::],
+ "ext_addr": lambda x: "REDACTED_" + x[9::],
+ "device_name": lambda x: "#MASKED_NAME#" if x else "",
+ "device_alias": lambda x: "#MASKED_NAME#" if x else "",
+ "alias": lambda x: "#MASKED_NAME#" if x else "", # child info on parent uses alias
+ "local_ip": lambda x: x, # don't redact but keep listed here for dump_devinfo
+ # robovac
+ "board_sn": lambda _: "000000000000",
+ "custom_sn": lambda _: "000000000000",
+ "location": lambda x: "#MASKED_NAME#" if x else "",
+ "map_data": lambda x: "#SCRUBBED_MAPDATA#" if x else "",
+ "map_name": lambda x: "I01BU0tFRF9OQU1FIw==", # #MASKED_NAME#
+ "area_list": _mask_area_list,
+ # unknown robovac binary blob in get_device_info
+ "cd": lambda x: "I01BU0tFRF9CSU5BUlkj", # #MASKED_BINARY#
+}
+
+# Queries that are known not to work properly when sent as a
+# multiRequest. They will not return the `method` key.
+FORCE_SINGLE_REQUEST = {
+ "getConnectStatus",
+ "scanApList",
}
@@ -76,6 +116,7 @@ class SmartProtocol(BaseProtocol):
self._transport._config.batch_size or self.DEFAULT_MULTI_REQUEST_BATCH_SIZE
)
self._redact_data = True
+ self._method_missing_logged = False
def get_smart_request(self, method: str, params: dict | None = None) -> str:
"""Get a request message as a string."""
@@ -157,22 +198,25 @@ class SmartProtocol(BaseProtocol):
# make mypy happy, this should never be reached..
raise KasaException("Query reached somehow to unreachable")
- async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dict:
+ async def _execute_multiple_query(
+ self, requests: dict, retry_count: int, iterate_list_pages: bool
+ ) -> dict:
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
multi_result: dict[str, Any] = {}
smart_method = "multipleRequest"
- multi_requests = [
- {"method": method, "params": params} if params else {"method": method}
- for method, params in requests.items()
- ]
-
- end = len(multi_requests)
+ end = len(requests)
# The SmartCamProtocol sends requests with a length 1 as a
# multipleRequest. The SmartProtocol doesn't so will never
# raise_on_error
raise_on_error = end == 1
+ multi_requests = [
+ {"method": method, "params": params} if params else {"method": method}
+ for method, params in requests.items()
+ if method not in FORCE_SINGLE_REQUEST
+ ]
+
# Break the requests down as there can be a size limit
step = self._multi_request_batch_size
if step == 1:
@@ -233,22 +277,41 @@ class SmartProtocol(BaseProtocol):
responses = response_step["result"]["responses"]
for response in responses:
- method = response["method"]
+ # some smartcam devices calls do not populate the method key
+ # these should be defined in DO_NOT_SEND_AS_MULTI_REQUEST.
+ if not (method := response.get("method")):
+ if not self._method_missing_logged:
+ # Avoid spamming the logs
+ self._method_missing_logged = True
+ _LOGGER.error(
+ "No method key in response for %s, skipping: %s",
+ self._host,
+ response_step,
+ )
+ # These will end up being queried individually
+ continue
+
self._handle_response_error_code(
response, method, raise_on_error=raise_on_error
)
result = response.get("result", None)
- await self._handle_response_lists(
- result, method, retry_count=retry_count
- )
+ request_params = rp if (rp := requests.get(method)) else None
+ if iterate_list_pages and result:
+ await self._handle_response_lists(
+ result, method, request_params, retry_count=retry_count
+ )
multi_result[method] = result
- # Multi requests don't continue after errors so requery any missing
+
+ # Multi requests don't continue after errors so requery any missing.
+ # Will also query individually any DO_NOT_SEND_AS_MULTI_REQUEST.
for method, params in requests.items():
if method not in multi_result:
resp = await self._transport.send(
self.get_smart_request(method, params)
)
- self._handle_response_error_code(resp, method, raise_on_error=False)
+ self._handle_response_error_code(
+ resp, method, raise_on_error=raise_on_error
+ )
multi_result[method] = resp.get("result")
return multi_result
@@ -262,7 +325,9 @@ class SmartProtocol(BaseProtocol):
smart_method = next(iter(request))
smart_params = request[smart_method]
else:
- return await self._execute_multiple_query(request, retry_count)
+ return await self._execute_multiple_query(
+ request, retry_count, iterate_list_pages
+ )
else:
smart_method = request
smart_params = None
@@ -289,12 +354,21 @@ class SmartProtocol(BaseProtocol):
result = response_data.get("result")
if iterate_list_pages and result:
await self._handle_response_lists(
- result, smart_method, retry_count=retry_count
+ result, smart_method, smart_params, retry_count=retry_count
)
return {smart_method: result}
+ def _get_list_request(
+ self, method: str, params: dict | None, start_index: int
+ ) -> dict:
+ return {method: {"start_index": start_index}}
+
async def _handle_response_lists(
- self, response_result: dict[str, Any], method: str, retry_count: int
+ self,
+ response_result: dict[str, Any],
+ method: str,
+ params: dict | None,
+ retry_count: int,
) -> None:
if (
response_result is None
@@ -314,8 +388,9 @@ class SmartProtocol(BaseProtocol):
)
)
while (list_length := len(response_result[response_list_name])) < list_sum:
+ request = self._get_list_request(method, params, list_length)
response = await self._execute_query(
- {method: {"start_index": list_length}},
+ request,
retry_count=retry_count,
iterate_list_pages=False,
)
diff --git a/kasa/smart/modules/__init__.py b/kasa/smart/modules/__init__.py
index 99820cfa..9215277e 100644
--- a/kasa/smart/modules/__init__.py
+++ b/kasa/smart/modules/__init__.py
@@ -6,16 +6,23 @@ from .autooff import AutoOff
from .batterysensor import BatterySensor
from .brightness import Brightness
from .childdevice import ChildDevice
+from .childlock import ChildLock
from .childprotection import ChildProtection
+from .childsetup import ChildSetup
+from .clean import Clean
+from .cleanrecords import CleanRecords
from .cloud import Cloud
from .color import Color
from .colortemperature import ColorTemperature
+from .consumables import Consumables
from .contactsensor import ContactSensor
from .devicemodule import DeviceModule
+from .dustbin import Dustbin
from .energy import Energy
from .fan import Fan
from .firmware import Firmware
from .frostprotection import FrostProtection
+from .homekit import HomeKit
from .humiditysensor import HumiditySensor
from .led import Led
from .light import Light
@@ -23,8 +30,12 @@ from .lighteffect import LightEffect
from .lightpreset import LightPreset
from .lightstripeffect import LightStripEffect
from .lighttransition import LightTransition
+from .matter import Matter
+from .mop import Mop
from .motionsensor import MotionSensor
+from .overheatprotection import OverheatProtection
from .reportmode import ReportMode
+from .speaker import Speaker
from .temperaturecontrol import TemperatureControl
from .temperaturesensor import TemperatureSensor
from .thermostat import Thermostat
@@ -38,6 +49,8 @@ __all__ = [
"Energy",
"DeviceModule",
"ChildDevice",
+ "ChildLock",
+ "ChildSetup",
"BatterySensor",
"HumiditySensor",
"TemperatureSensor",
@@ -63,5 +76,14 @@ __all__ = [
"TriggerLogs",
"FrostProtection",
"Thermostat",
+ "Clean",
+ "Consumables",
+ "CleanRecords",
"SmartLightEffect",
+ "OverheatProtection",
+ "Speaker",
+ "HomeKit",
+ "Matter",
+ "Dustbin",
+ "Mop",
]
diff --git a/kasa/smart/modules/batterysensor.py b/kasa/smart/modules/batterysensor.py
index 87072b10..aef100fc 100644
--- a/kasa/smart/modules/batterysensor.py
+++ b/kasa/smart/modules/batterysensor.py
@@ -2,7 +2,11 @@
from __future__ import annotations
+from typing import Annotated
+
+from ...exceptions import KasaException
from ...feature import Feature
+from ...module import FeatureAttribute
from ..smartmodule import SmartModule
@@ -14,18 +18,22 @@ class BatterySensor(SmartModule):
def _initialize_features(self) -> None:
"""Initialize features."""
- self._add_feature(
- Feature(
- self._device,
- "battery_low",
- "Battery low",
- container=self,
- attribute_getter="battery_low",
- icon="mdi:alert",
- type=Feature.Type.BinarySensor,
- category=Feature.Category.Debug,
+ if (
+ "at_low_battery" in self._device.sys_info
+ or "is_low" in self._device.sys_info
+ ):
+ self._add_feature(
+ Feature(
+ self._device,
+ "battery_low",
+ "Battery low",
+ container=self,
+ attribute_getter="battery_low",
+ icon="mdi:alert",
+ type=Feature.Type.BinarySensor,
+ category=Feature.Category.Debug,
+ )
)
- )
# Some devices, like T110 contact sensor do not report the battery percentage
if "battery_percentage" in self._device.sys_info:
@@ -48,11 +56,17 @@ class BatterySensor(SmartModule):
return {}
@property
- def battery(self) -> int:
+ def battery(self) -> Annotated[int, FeatureAttribute()]:
"""Return battery level."""
return self._device.sys_info["battery_percentage"]
@property
- def battery_low(self) -> bool:
+ def battery_low(self) -> Annotated[bool, FeatureAttribute()]:
"""Return True if battery is low."""
- return self._device.sys_info["at_low_battery"]
+ is_low = self._device.sys_info.get(
+ "at_low_battery", self._device.sys_info.get("is_low")
+ )
+ if is_low is None:
+ raise KasaException("Device does not report battery low status")
+
+ return is_low
diff --git a/kasa/smart/modules/childdevice.py b/kasa/smart/modules/childdevice.py
index 4c3b99de..e816e3f1 100644
--- a/kasa/smart/modules/childdevice.py
+++ b/kasa/smart/modules/childdevice.py
@@ -38,6 +38,7 @@ Plug 3: False
True
"""
+from ...device_type import DeviceType
from ..smartmodule import SmartModule
@@ -46,3 +47,10 @@ class ChildDevice(SmartModule):
REQUIRED_COMPONENT = "child_device"
QUERY_GETTER_NAME = "get_child_device_list"
+
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ q = super().query()
+ if self._device.device_type is DeviceType.Hub:
+ q["get_child_device_component_list"] = None
+ return q
diff --git a/kasa/smart/modules/childlock.py b/kasa/smart/modules/childlock.py
new file mode 100644
index 00000000..1c5e72d9
--- /dev/null
+++ b/kasa/smart/modules/childlock.py
@@ -0,0 +1,37 @@
+"""Child lock module."""
+
+from __future__ import annotations
+
+from ...feature import Feature
+from ..smartmodule import SmartModule
+
+
+class ChildLock(SmartModule):
+ """Implementation for child lock."""
+
+ REQUIRED_COMPONENT = "button_and_led"
+ QUERY_GETTER_NAME = "getChildLockInfo"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ device=self._device,
+ id="child_lock",
+ name="Child lock",
+ container=self,
+ attribute_getter="enabled",
+ attribute_setter="set_enabled",
+ type=Feature.Type.Switch,
+ category=Feature.Category.Config,
+ )
+ )
+
+ @property
+ def enabled(self) -> bool:
+ """Return True if child lock is enabled."""
+ return self.data["child_lock_status"]
+
+ async def set_enabled(self, enabled: bool) -> dict:
+ """Set child lock."""
+ return await self.call("setChildLockInfo", {"child_lock_status": enabled})
diff --git a/kasa/smart/modules/childsetup.py b/kasa/smart/modules/childsetup.py
new file mode 100644
index 00000000..b1a17102
--- /dev/null
+++ b/kasa/smart/modules/childsetup.py
@@ -0,0 +1,87 @@
+"""Implementation for child device setup.
+
+This module allows pairing and disconnecting child devices.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+
+from ...feature import Feature
+from ..smartmodule import SmartModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ChildSetup(SmartModule):
+ """Implementation for child device setup."""
+
+ REQUIRED_COMPONENT = "child_quick_setup"
+ QUERY_GETTER_NAME = "get_support_child_device_category"
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="pair",
+ name="Pair",
+ container=self,
+ attribute_setter="pair",
+ category=Feature.Category.Config,
+ type=Feature.Type.Action,
+ )
+ )
+
+ async def get_supported_device_categories(self) -> list[dict]:
+ """Get supported device categories."""
+ categories = await self.call("get_support_child_device_category")
+ return categories["get_support_child_device_category"]["device_category_list"]
+
+ async def pair(self, *, timeout: int = 10) -> list[dict]:
+ """Scan for new devices and pair after discovering first new device."""
+ await self.call("begin_scanning_child_device")
+
+ _LOGGER.info("Waiting %s seconds for discovering new devices", timeout)
+ await asyncio.sleep(timeout)
+ detected = await self._get_detected_devices()
+
+ if not detected["child_device_list"]:
+ _LOGGER.warning(
+ "No devices found, make sure to activate pairing "
+ "mode on the devices to be added."
+ )
+ return []
+
+ _LOGGER.info(
+ "Discovery done, found %s devices: %s",
+ len(detected["child_device_list"]),
+ detected,
+ )
+
+ await self._add_devices(detected)
+
+ return detected["child_device_list"]
+
+ async def unpair(self, device_id: str) -> dict:
+ """Remove device from the hub."""
+ _LOGGER.info("Going to unpair %s from %s", device_id, self)
+
+ payload = {"child_device_list": [{"device_id": device_id}]}
+ return await self.call("remove_child_device_list", payload)
+
+ async def _add_devices(self, devices: dict) -> dict:
+ """Add devices based on get_detected_device response.
+
+ Pass the output from :ref:_get_detected_devices: as a parameter.
+ """
+ res = await self.call("add_child_device_list", devices)
+ return res
+
+ async def _get_detected_devices(self) -> dict:
+ """Return list of devices detected during scanning."""
+ param = {"scan_list": await self.get_supported_device_categories()}
+ res = await self.call("get_scan_child_device_list", param)
+ _LOGGER.debug("Scan status: %s", res)
+ return res["get_scan_child_device_list"]
diff --git a/kasa/smart/modules/clean.py b/kasa/smart/modules/clean.py
new file mode 100644
index 00000000..393a4f29
--- /dev/null
+++ b/kasa/smart/modules/clean.py
@@ -0,0 +1,427 @@
+"""Implementation of vacuum clean module."""
+
+from __future__ import annotations
+
+import logging
+from datetime import timedelta
+from enum import IntEnum, StrEnum
+from typing import Annotated, Literal
+
+from ...feature import Feature
+from ...module import FeatureAttribute
+from ..smartmodule import SmartModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Status(IntEnum):
+ """Status of vacuum."""
+
+ Idle = 0
+ Cleaning = 1
+ Mapping = 2
+ GoingHome = 4
+ Charging = 5
+ Charged = 6
+ Paused = 7
+ Undocked = 8
+ Error = 100
+
+ UnknownInternal = -1000
+
+
+class ErrorCode(IntEnum):
+ """Error codes for vacuum."""
+
+ Ok = 0
+ SideBrushStuck = 2
+ MainBrushStuck = 3
+ WheelBlocked = 4
+ Trapped = 6
+ TrappedCliff = 7
+ DustBinRemoved = 14
+ UnableToMove = 15
+ LidarBlocked = 16
+ UnableToFindDock = 21
+ BatteryLow = 22
+
+ UnknownInternal = -1000
+
+
+class FanSpeed(IntEnum):
+ """Fan speed level."""
+
+ Quiet = 1
+ Standard = 2
+ Turbo = 3
+ Max = 4
+ Ultra = 5
+
+
+class CarpetCleanMode(StrEnum):
+ """Carpet clean mode."""
+
+ Normal = "normal"
+ Boost = "boost"
+
+
+class AreaUnit(IntEnum):
+ """Area unit."""
+
+ #: Square meter
+ Sqm = 0
+ #: Square feet
+ Sqft = 1
+ #: Taiwanese unit: https://en.wikipedia.org/wiki/Taiwanese_units_of_measurement#Area
+ Ping = 2
+
+
+class Clean(SmartModule):
+ """Implementation of vacuum clean module."""
+
+ REQUIRED_COMPONENT = "clean"
+ _error_code = ErrorCode.Ok
+ _logged_error_code_warnings: set | None = None
+ _logged_status_code_warnings: set
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="vacuum_return_home",
+ name="Return home",
+ container=self,
+ attribute_setter="return_home",
+ category=Feature.Category.Primary,
+ type=Feature.Action,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="vacuum_start",
+ name="Start cleaning",
+ container=self,
+ attribute_setter="start",
+ category=Feature.Category.Primary,
+ type=Feature.Action,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="vacuum_pause",
+ name="Pause",
+ container=self,
+ attribute_setter="pause",
+ category=Feature.Category.Primary,
+ type=Feature.Action,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="vacuum_status",
+ name="Vacuum status",
+ container=self,
+ attribute_getter="status",
+ category=Feature.Category.Primary,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="vacuum_error",
+ name="Error",
+ container=self,
+ attribute_getter="error",
+ category=Feature.Category.Info,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="battery_level",
+ name="Battery level",
+ container=self,
+ attribute_getter="battery",
+ icon="mdi:battery",
+ unit_getter=lambda: "%",
+ category=Feature.Category.Info,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="vacuum_fan_speed",
+ name="Fan speed",
+ container=self,
+ attribute_getter="fan_speed_preset",
+ attribute_setter="set_fan_speed_preset",
+ icon="mdi:fan",
+ choices_getter=lambda: list(FanSpeed.__members__),
+ category=Feature.Category.Primary,
+ type=Feature.Type.Choice,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="clean_count",
+ name="Clean count",
+ container=self,
+ attribute_getter="clean_count",
+ attribute_setter="set_clean_count",
+ range_getter=lambda: (1, 3),
+ category=Feature.Category.Config,
+ type=Feature.Type.Number,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="carpet_clean_mode",
+ name="Carpet clean mode",
+ container=self,
+ attribute_getter="carpet_clean_mode",
+ attribute_setter="set_carpet_clean_mode",
+ icon="mdi:rug",
+ choices_getter=lambda: list(CarpetCleanMode.__members__),
+ category=Feature.Category.Config,
+ type=Feature.Type.Choice,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="clean_area",
+ name="Cleaning area",
+ container=self,
+ attribute_getter="clean_area",
+ unit_getter="area_unit",
+ category=Feature.Category.Info,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="clean_time",
+ name="Cleaning time",
+ container=self,
+ attribute_getter="clean_time",
+ category=Feature.Category.Info,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="clean_progress",
+ name="Cleaning progress",
+ container=self,
+ attribute_getter="clean_progress",
+ unit_getter=lambda: "%",
+ category=Feature.Category.Info,
+ type=Feature.Type.Sensor,
+ )
+ )
+
+ async def _post_update_hook(self) -> None:
+ """Set error code after update."""
+ if self._logged_error_code_warnings is None:
+ self._logged_error_code_warnings = set()
+ self._logged_status_code_warnings = set()
+
+ errors = self._vac_status.get("err_status")
+ if errors is None or not errors:
+ self._error_code = ErrorCode.Ok
+ return
+
+ if len(errors) > 1 and "multiple" not in self._logged_error_code_warnings:
+ self._logged_error_code_warnings.add("multiple")
+ _LOGGER.warning(
+ "Multiple error codes, using the first one only: %s", errors
+ )
+
+ error = errors.pop(0)
+ try:
+ self._error_code = ErrorCode(error)
+ except ValueError:
+ if error not in self._logged_error_code_warnings:
+ self._logged_error_code_warnings.add(error)
+ _LOGGER.warning(
+ "Unknown error code, please create an issue "
+ "describing the error: %s",
+ error,
+ )
+ self._error_code = ErrorCode.UnknownInternal
+
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ return {
+ "getVacStatus": {},
+ "getCleanInfo": {},
+ "getCarpetClean": {},
+ "getAreaUnit": {},
+ "getBatteryInfo": {},
+ "getCleanStatus": {},
+ "getCleanAttr": {"type": "global"},
+ }
+
+ async def start(self) -> dict:
+ """Start cleaning."""
+ # If we are paused, do not restart cleaning
+
+ if self.status is Status.Paused:
+ return await self.resume()
+
+ return await self.call(
+ "setSwitchClean",
+ {
+ "clean_mode": 0,
+ "clean_on": True,
+ "clean_order": True,
+ "force_clean": False,
+ },
+ )
+
+ async def pause(self) -> dict:
+ """Pause cleaning."""
+ if self.status is Status.GoingHome:
+ return await self.set_return_home(False)
+
+ return await self.set_pause(True)
+
+ async def resume(self) -> dict:
+ """Resume cleaning."""
+ return await self.set_pause(False)
+
+ async def set_pause(self, enabled: bool) -> dict:
+ """Pause or resume cleaning."""
+ return await self.call("setRobotPause", {"pause": enabled})
+
+ async def return_home(self) -> dict:
+ """Return home."""
+ return await self.set_return_home(True)
+
+ async def set_return_home(self, enabled: bool) -> dict:
+ """Return home / pause returning."""
+ return await self.call("setSwitchCharge", {"switch_charge": enabled})
+
+ @property
+ def error(self) -> ErrorCode:
+ """Return error."""
+ return self._error_code
+
+ @property
+ def fan_speed_preset(self) -> Annotated[str, FeatureAttribute()]:
+ """Return fan speed preset."""
+ return FanSpeed(self._settings["suction"]).name
+
+ async def set_fan_speed_preset(
+ self, speed: str
+ ) -> Annotated[dict, FeatureAttribute]:
+ """Set fan speed preset."""
+ name_to_value = {x.name: x.value for x in FanSpeed}
+ if speed not in name_to_value:
+ raise ValueError("Invalid fan speed %s, available %s", speed, name_to_value)
+ return await self._change_setting("suction", name_to_value[speed])
+
+ async def _change_setting(
+ self, name: str, value: int, *, scope: Literal["global", "pose"] = "global"
+ ) -> dict:
+ """Change device setting."""
+ params = {
+ name: value,
+ "type": scope,
+ }
+ return await self.call("setCleanAttr", params)
+
+ @property
+ def battery(self) -> int:
+ """Return battery level."""
+ return self.data["getBatteryInfo"]["battery_percentage"]
+
+ @property
+ def _vac_status(self) -> dict:
+ """Return vac status container."""
+ return self.data["getVacStatus"]
+
+ @property
+ def _info(self) -> dict:
+ """Return current cleaning info."""
+ return self.data["getCleanInfo"]
+
+ @property
+ def _settings(self) -> dict:
+ """Return cleaning settings."""
+ return self.data["getCleanAttr"]
+
+ @property
+ def status(self) -> Status:
+ """Return current status."""
+ if self._error_code is not ErrorCode.Ok:
+ return Status.Error
+
+ status_code = self._vac_status["status"]
+ try:
+ return Status(status_code)
+ except ValueError:
+ if status_code not in self._logged_status_code_warnings:
+ self._logged_status_code_warnings.add(status_code)
+ _LOGGER.warning(
+ "Got unknown status code: %s (%s)", status_code, self.data
+ )
+ return Status.UnknownInternal
+
+ @property
+ def carpet_clean_mode(self) -> Annotated[str, FeatureAttribute()]:
+ """Return carpet clean mode."""
+ return CarpetCleanMode(self.data["getCarpetClean"]["carpet_clean_prefer"]).name
+
+ async def set_carpet_clean_mode(
+ self, mode: str
+ ) -> Annotated[dict, FeatureAttribute()]:
+ """Set carpet clean mode."""
+ name_to_value = {x.name: x.value for x in CarpetCleanMode}
+ if mode not in name_to_value:
+ raise ValueError(
+ "Invalid carpet clean mode %s, available %s", mode, name_to_value
+ )
+ return await self.call(
+ "setCarpetClean", {"carpet_clean_prefer": name_to_value[mode]}
+ )
+
+ @property
+ def area_unit(self) -> AreaUnit:
+ """Return area unit."""
+ return AreaUnit(self.data["getAreaUnit"]["area_unit"])
+
+ @property
+ def clean_area(self) -> Annotated[int, FeatureAttribute()]:
+ """Return currently cleaned area."""
+ return self._info["clean_area"]
+
+ @property
+ def clean_time(self) -> timedelta:
+ """Return current cleaning time."""
+ return timedelta(minutes=self._info["clean_time"])
+
+ @property
+ def clean_progress(self) -> int:
+ """Return amount of currently cleaned area."""
+ return self._info["clean_percent"]
+
+ @property
+ def clean_count(self) -> Annotated[int, FeatureAttribute()]:
+ """Return number of times to clean."""
+ return self._settings["clean_number"]
+
+ async def set_clean_count(self, count: int) -> Annotated[dict, FeatureAttribute()]:
+ """Set number of times to clean."""
+ return await self._change_setting("clean_number", count)
diff --git a/kasa/smart/modules/cleanrecords.py b/kasa/smart/modules/cleanrecords.py
new file mode 100644
index 00000000..fdd0daee
--- /dev/null
+++ b/kasa/smart/modules/cleanrecords.py
@@ -0,0 +1,205 @@
+"""Implementation of vacuum cleaning records."""
+
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass, field
+from datetime import datetime, timedelta, tzinfo
+from typing import Annotated, cast
+
+from mashumaro import DataClassDictMixin, field_options
+from mashumaro.config import ADD_DIALECT_SUPPORT
+from mashumaro.dialect import Dialect
+from mashumaro.types import SerializationStrategy
+
+from ...feature import Feature
+from ...module import FeatureAttribute
+from ..smartmodule import Module, SmartModule
+from .clean import AreaUnit, Clean
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@dataclass
+class Record(DataClassDictMixin):
+ """Historical cleanup result."""
+
+ class Config:
+ """Configuration class."""
+
+ code_generation_options = [ADD_DIALECT_SUPPORT]
+
+ #: Total time cleaned (in minutes)
+ clean_time: timedelta = field(
+ metadata=field_options(deserialize=lambda x: timedelta(minutes=x))
+ )
+ #: Total area cleaned
+ clean_area: int
+ dust_collection: bool
+ timestamp: datetime
+
+ info_num: int | None = None
+ message: int | None = None
+ map_id: int | None = None
+ start_type: int | None = None
+ task_type: int | None = None
+ record_index: int | None = None
+
+ #: Error code from cleaning
+ error: int = field(default=0)
+
+
+class _DateTimeSerializationStrategy(SerializationStrategy):
+ def __init__(self, tz: tzinfo) -> None:
+ self.tz = tz
+
+ def deserialize(self, value: float) -> datetime:
+ return datetime.fromtimestamp(value, self.tz)
+
+
+def _get_tz_strategy(tz: tzinfo) -> type[Dialect]:
+ """Return a timezone aware de-serialization strategy."""
+
+ class TimezoneDialect(Dialect):
+ serialization_strategy = {datetime: _DateTimeSerializationStrategy(tz)}
+
+ return TimezoneDialect
+
+
+@dataclass
+class Records(DataClassDictMixin):
+ """Response payload for getCleanRecords."""
+
+ class Config:
+ """Configuration class."""
+
+ code_generation_options = [ADD_DIALECT_SUPPORT]
+
+ total_time: timedelta = field(
+ metadata=field_options(deserialize=lambda x: timedelta(minutes=x))
+ )
+ total_area: int
+ total_count: int = field(metadata=field_options(alias="total_number"))
+
+ records: list[Record] = field(metadata=field_options(alias="record_list"))
+ last_clean: Record = field(metadata=field_options(alias="lastest_day_record"))
+
+ @classmethod
+ def __pre_deserialize__(cls, d: dict) -> dict:
+ if ldr := d.get("lastest_day_record"):
+ d["lastest_day_record"] = {
+ "timestamp": ldr[0],
+ "clean_time": ldr[1],
+ "clean_area": ldr[2],
+ "dust_collection": ldr[3],
+ }
+ return d
+
+
+class CleanRecords(SmartModule):
+ """Implementation of vacuum cleaning records."""
+
+ REQUIRED_COMPONENT = "clean_percent"
+ _parsed_data: Records
+
+ async def _post_update_hook(self) -> None:
+ """Cache parsed data after an update."""
+ self._parsed_data = Records.from_dict(
+ self.data, dialect=_get_tz_strategy(self._device.timezone)
+ )
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ for type_ in ["total", "last"]:
+ self._add_feature(
+ Feature(
+ self._device,
+ id=f"{type_}_clean_area",
+ name=f"{type_.capitalize()} area cleaned",
+ container=self,
+ attribute_getter=f"{type_}_clean_area",
+ unit_getter="area_unit",
+ category=Feature.Category.Debug,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id=f"{type_}_clean_time",
+ name=f"{type_.capitalize()} time cleaned",
+ container=self,
+ attribute_getter=f"{type_}_clean_time",
+ category=Feature.Category.Debug,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="total_clean_count",
+ name="Total clean count",
+ container=self,
+ attribute_getter="total_clean_count",
+ category=Feature.Category.Debug,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="last_clean_timestamp",
+ name="Last clean timestamp",
+ container=self,
+ attribute_getter="last_clean_timestamp",
+ category=Feature.Category.Debug,
+ type=Feature.Type.Sensor,
+ )
+ )
+
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ return {
+ "getCleanRecords": {},
+ }
+
+ @property
+ def total_clean_area(self) -> Annotated[int, FeatureAttribute()]:
+ """Return total cleaning area."""
+ return self._parsed_data.total_area
+
+ @property
+ def total_clean_time(self) -> timedelta:
+ """Return total cleaning time."""
+ return self._parsed_data.total_time
+
+ @property
+ def total_clean_count(self) -> int:
+ """Return total clean count."""
+ return self._parsed_data.total_count
+
+ @property
+ def last_clean_area(self) -> Annotated[int, FeatureAttribute()]:
+ """Return latest cleaning area."""
+ return self._parsed_data.last_clean.clean_area
+
+ @property
+ def last_clean_time(self) -> timedelta:
+ """Return total cleaning time."""
+ return self._parsed_data.last_clean.clean_time
+
+ @property
+ def last_clean_timestamp(self) -> datetime:
+ """Return latest cleaning timestamp."""
+ return self._parsed_data.last_clean.timestamp
+
+ @property
+ def area_unit(self) -> AreaUnit:
+ """Return area unit."""
+ clean = cast(Clean, self._device.modules[Module.Clean])
+ return clean.area_unit
+
+ @property
+ def parsed_data(self) -> Records:
+ """Return parsed records data."""
+ return self._parsed_data
diff --git a/kasa/smart/modules/consumables.py b/kasa/smart/modules/consumables.py
new file mode 100644
index 00000000..10de583e
--- /dev/null
+++ b/kasa/smart/modules/consumables.py
@@ -0,0 +1,170 @@
+"""Implementation of vacuum consumables."""
+
+from __future__ import annotations
+
+import logging
+from collections.abc import Mapping
+from dataclasses import dataclass
+from datetime import timedelta
+
+from ...feature import Feature
+from ..smartmodule import SmartModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@dataclass
+class _ConsumableMeta:
+ """Consumable meta container."""
+
+ #: Name of the consumable.
+ name: str
+ #: Internal id of the consumable
+ id: str
+ #: Data key in the device reported data
+ data_key: str
+ #: Lifetime
+ lifetime: timedelta
+
+
+@dataclass
+class Consumable:
+ """Consumable container."""
+
+ #: Name of the consumable.
+ name: str
+ #: Id of the consumable
+ id: str
+ #: Lifetime
+ lifetime: timedelta
+ #: Used
+ used: timedelta
+ #: Remaining
+ remaining: timedelta
+ #: Device data key
+ _data_key: str
+
+
+CONSUMABLE_METAS = [
+ _ConsumableMeta(
+ "Main brush",
+ id="main_brush",
+ data_key="roll_brush_time",
+ lifetime=timedelta(hours=400),
+ ),
+ _ConsumableMeta(
+ "Side brush",
+ id="side_brush",
+ data_key="edge_brush_time",
+ lifetime=timedelta(hours=200),
+ ),
+ _ConsumableMeta(
+ "Filter",
+ id="filter",
+ data_key="filter_time",
+ lifetime=timedelta(hours=200),
+ ),
+ _ConsumableMeta(
+ "Sensor",
+ id="sensor",
+ data_key="sensor_time",
+ lifetime=timedelta(hours=30),
+ ),
+ _ConsumableMeta(
+ "Charging contacts",
+ id="charging_contacts",
+ data_key="charge_contact_time",
+ lifetime=timedelta(hours=30),
+ ),
+ # Unknown keys: main_brush_lid_time, rag_time
+]
+
+
+class Consumables(SmartModule):
+ """Implementation of vacuum consumables."""
+
+ REQUIRED_COMPONENT = "consumables"
+ QUERY_GETTER_NAME = "getConsumablesInfo"
+
+ _consumables: dict[str, Consumable] = {}
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ for c_meta in CONSUMABLE_METAS:
+ if c_meta.data_key not in self.data:
+ continue
+
+ self._add_feature(
+ Feature(
+ self._device,
+ id=f"{c_meta.id}_used",
+ name=f"{c_meta.name} used",
+ container=self,
+ attribute_getter=lambda _, c_id=c_meta.id: self._consumables[
+ c_id
+ ].used,
+ category=Feature.Category.Debug,
+ type=Feature.Type.Sensor,
+ )
+ )
+
+ self._add_feature(
+ Feature(
+ self._device,
+ id=f"{c_meta.id}_remaining",
+ name=f"{c_meta.name} remaining",
+ container=self,
+ attribute_getter=lambda _, c_id=c_meta.id: self._consumables[
+ c_id
+ ].remaining,
+ category=Feature.Category.Info,
+ type=Feature.Type.Sensor,
+ )
+ )
+
+ self._add_feature(
+ Feature(
+ self._device,
+ id=f"{c_meta.id}_reset",
+ name=f"Reset {c_meta.name.lower()} consumable",
+ container=self,
+ attribute_setter=lambda c_id=c_meta.id: self.reset_consumable(c_id),
+ category=Feature.Category.Debug,
+ type=Feature.Type.Action,
+ )
+ )
+
+ async def _post_update_hook(self) -> None:
+ """Update the consumables."""
+ if not self._consumables:
+ for consumable_meta in CONSUMABLE_METAS:
+ if consumable_meta.data_key not in self.data:
+ continue
+ used = timedelta(minutes=self.data[consumable_meta.data_key])
+ consumable = Consumable(
+ id=consumable_meta.id,
+ name=consumable_meta.name,
+ lifetime=consumable_meta.lifetime,
+ used=used,
+ remaining=consumable_meta.lifetime - used,
+ _data_key=consumable_meta.data_key,
+ )
+ self._consumables[consumable_meta.id] = consumable
+ else:
+ for consumable in self._consumables.values():
+ consumable.used = timedelta(minutes=self.data[consumable._data_key])
+ consumable.remaining = consumable.lifetime - consumable.used
+
+ async def reset_consumable(self, consumable_id: str) -> dict:
+ """Reset consumable stats."""
+ consumable_name = self._consumables[consumable_id]._data_key.removesuffix(
+ "_time"
+ )
+ return await self.call(
+ "resetConsumablesTime", {"reset_list": [consumable_name]}
+ )
+
+ @property
+ def consumables(self) -> Mapping[str, Consumable]:
+ """Get list of consumables on the device."""
+ return self._consumables
diff --git a/kasa/smart/modules/contactsensor.py b/kasa/smart/modules/contactsensor.py
index f388b781..d0bebb07 100644
--- a/kasa/smart/modules/contactsensor.py
+++ b/kasa/smart/modules/contactsensor.py
@@ -10,7 +10,7 @@ class ContactSensor(SmartModule):
"""Implementation of contact sensor module."""
REQUIRED_COMPONENT = None # we depend on availability of key
- REQUIRED_KEY_ON_PARENT = "open"
+ SYSINFO_LOOKUP_KEYS = ["open"]
def _initialize_features(self) -> None:
"""Initialize features after the initial update."""
diff --git a/kasa/smart/modules/devicemodule.py b/kasa/smart/modules/devicemodule.py
index bf112e2d..692745bb 100644
--- a/kasa/smart/modules/devicemodule.py
+++ b/kasa/smart/modules/devicemodule.py
@@ -19,12 +19,15 @@ class DeviceModule(SmartModule):
def query(self) -> dict:
"""Query to execute during the update cycle."""
+ if self._device._is_hub_child:
+ # Child devices get their device info updated by the parent device.
+ return {}
query = {
"get_device_info": None,
}
# Device usage is not available on older firmware versions
# or child devices of hubs
- if self.supported_version >= 2 and not self._device._is_hub_child:
+ if self.supported_version >= 2:
query["get_device_usage"] = None
return query
diff --git a/kasa/smart/modules/dustbin.py b/kasa/smart/modules/dustbin.py
new file mode 100644
index 00000000..08c35d5e
--- /dev/null
+++ b/kasa/smart/modules/dustbin.py
@@ -0,0 +1,117 @@
+"""Implementation of vacuum dustbin."""
+
+from __future__ import annotations
+
+import logging
+from enum import IntEnum
+
+from ...feature import Feature
+from ..smartmodule import SmartModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Mode(IntEnum):
+ """Dust collection modes."""
+
+ Smart = 0
+ Light = 1
+ Balanced = 2
+ Max = 3
+
+
+class Dustbin(SmartModule):
+ """Implementation of vacuum dustbin."""
+
+ REQUIRED_COMPONENT = "dust_bucket"
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="dustbin_empty",
+ name="Empty dustbin",
+ container=self,
+ attribute_setter="start_emptying",
+ category=Feature.Category.Primary,
+ type=Feature.Action,
+ )
+ )
+
+ self._add_feature(
+ Feature(
+ self._device,
+ id="dustbin_autocollection_enabled",
+ name="Automatic emptying enabled",
+ container=self,
+ attribute_getter="auto_collection",
+ attribute_setter="set_auto_collection",
+ category=Feature.Category.Config,
+ type=Feature.Switch,
+ )
+ )
+
+ self._add_feature(
+ Feature(
+ self._device,
+ id="dustbin_mode",
+ name="Automatic emptying mode",
+ container=self,
+ attribute_getter="mode",
+ attribute_setter="set_mode",
+ icon="mdi:fan",
+ choices_getter=lambda: list(Mode.__members__),
+ category=Feature.Category.Config,
+ type=Feature.Type.Choice,
+ )
+ )
+
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ return {
+ "getAutoDustCollection": {},
+ "getDustCollectionInfo": {},
+ }
+
+ async def start_emptying(self) -> dict:
+ """Start emptying the bin."""
+ return await self.call(
+ "setSwitchDustCollection",
+ {
+ "switch_dust_collection": True,
+ },
+ )
+
+ @property
+ def _settings(self) -> dict:
+ """Return auto-empty settings."""
+ return self.data["getDustCollectionInfo"]
+
+ @property
+ def mode(self) -> str:
+ """Return auto-emptying mode."""
+ return Mode(self._settings["dust_collection_mode"]).name
+
+ async def set_mode(self, mode: str) -> dict:
+ """Set auto-emptying mode."""
+ name_to_value = {x.name: x.value for x in Mode}
+ if mode not in name_to_value:
+ raise ValueError(
+ "Invalid auto/emptying mode speed %s, available %s", mode, name_to_value
+ )
+
+ settings = self._settings.copy()
+ settings["dust_collection_mode"] = name_to_value[mode]
+ return await self.call("setDustCollectionInfo", settings)
+
+ @property
+ def auto_collection(self) -> dict:
+ """Return auto-emptying config."""
+ return self._settings["auto_dust_collection"]
+
+ async def set_auto_collection(self, on: bool) -> dict:
+ """Toggle auto-emptying."""
+ settings = self._settings.copy()
+ settings["auto_dust_collection"] = on
+ return await self.call("setDustCollectionInfo", settings)
diff --git a/kasa/smart/modules/energy.py b/kasa/smart/modules/energy.py
index 6b5bdb57..0cfdc92c 100644
--- a/kasa/smart/modules/energy.py
+++ b/kasa/smart/modules/energy.py
@@ -2,10 +2,10 @@
from __future__ import annotations
-from typing import NoReturn
+from typing import Any, NoReturn
from ...emeterstatus import EmeterStatus
-from ...exceptions import KasaException
+from ...exceptions import DeviceError, KasaException
from ...interfaces.energy import Energy as EnergyInterface
from ..smartmodule import SmartModule, raise_if_update_error
@@ -15,12 +15,39 @@ class Energy(SmartModule, EnergyInterface):
REQUIRED_COMPONENT = "energy_monitoring"
+ _energy: dict[str, Any]
+ _current_consumption: float | None
+
async def _post_update_hook(self) -> None:
- if "voltage_mv" in self.data.get("get_emeter_data", {}):
+ try:
+ data = self.data
+ except DeviceError as de:
+ self._energy = {}
+ self._current_consumption = None
+ raise de
+
+ # If version is 1 then data is get_energy_usage
+ self._energy = data.get("get_energy_usage", data)
+
+ if "voltage_mv" in data.get("get_emeter_data", {}):
self._supported = (
self._supported | EnergyInterface.ModuleFeature.VOLTAGE_CURRENT
)
+ if (power := self._energy.get("current_power")) is not None or (
+ power := data.get("get_emeter_data", {}).get("power_mw")
+ ) is not None:
+ self._current_consumption = power / 1_000
+ # Fallback if get_energy_usage does not provide current_power,
+ # which can happen on some newer devices (e.g. P304M).
+ # This may not be valid scenario as it pre-dates trying get_emeter_data
+ elif (
+ power := self.data.get("get_current_power", {}).get("current_power")
+ ) is not None:
+ self._current_consumption = power
+ else:
+ self._current_consumption = None
+
def query(self) -> dict:
"""Query to execute during the update cycle."""
req = {
@@ -33,28 +60,21 @@ class Energy(SmartModule, EnergyInterface):
return req
@property
- @raise_if_update_error
- def current_consumption(self) -> float | None:
- """Current power in watts."""
- if (power := self.energy.get("current_power")) is not None or (
- power := self.data.get("get_emeter_data", {}).get("power_mw")
- ) is not None:
- return power / 1_000
- # Fallback if get_energy_usage does not provide current_power,
- # which can happen on some newer devices (e.g. P304M).
- elif (
- power := self.data.get("get_current_power", {}).get("current_power")
- ) is not None:
- return power
- return None
+ def optional_response_keys(self) -> list[str]:
+ """Return optional response keys for the module."""
+ if self.supported_version > 1:
+ return ["get_energy_usage"]
+ return []
+
+ @property
+ def current_consumption(self) -> float | None:
+ """Current power in watts."""
+ return self._current_consumption
@property
- @raise_if_update_error
def energy(self) -> dict:
"""Return get_energy_usage results."""
- if en := self.data.get("get_energy_usage"):
- return en
- return self.data
+ return self._energy
def _get_status_from_energy(self, energy: dict) -> EmeterStatus:
return EmeterStatus(
@@ -83,16 +103,18 @@ class Energy(SmartModule, EnergyInterface):
return self._get_status_from_energy(res["get_energy_usage"])
@property
- @raise_if_update_error
def consumption_this_month(self) -> float | None:
"""Get the emeter value for this month in kWh."""
- return self.energy.get("month_energy", 0) / 1_000
+ if (month := self.energy.get("month_energy")) is not None:
+ return month / 1_000
+ return None
@property
- @raise_if_update_error
def consumption_today(self) -> float | None:
"""Get the emeter value for today in kWh."""
- return self.energy.get("today_energy", 0) / 1_000
+ if (today := self.energy.get("today_energy")) is not None:
+ return today / 1_000
+ return None
@property
@raise_if_update_error
diff --git a/kasa/smart/modules/homekit.py b/kasa/smart/modules/homekit.py
new file mode 100644
index 00000000..2df8db1f
--- /dev/null
+++ b/kasa/smart/modules/homekit.py
@@ -0,0 +1,32 @@
+"""Implementation of homekit module."""
+
+from __future__ import annotations
+
+from ...feature import Feature
+from ..smartmodule import SmartModule
+
+
+class HomeKit(SmartModule):
+ """Implementation of homekit module."""
+
+ QUERY_GETTER_NAME: str = "get_homekit_info"
+ REQUIRED_COMPONENT = "homekit"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="homekit_setup_code",
+ name="Homekit setup code",
+ container=self,
+ attribute_getter=lambda x: x.info["mfi_setup_code"],
+ type=Feature.Type.Sensor,
+ category=Feature.Category.Debug,
+ )
+ )
+
+ @property
+ def info(self) -> dict[str, str]:
+ """Homekit mfi setup info."""
+ return self.data
diff --git a/kasa/smart/modules/light.py b/kasa/smart/modules/light.py
index 73098875..d548811f 100644
--- a/kasa/smart/modules/light.py
+++ b/kasa/smart/modules/light.py
@@ -7,7 +7,7 @@ from typing import Annotated
from ...exceptions import KasaException
from ...feature import Feature
-from ...interfaces.light import HSV, ColorTempRange, LightState
+from ...interfaces.light import HSV, LightState
from ...interfaces.light import Light as LightInterface
from ...module import FeatureAttribute, Module
from ..smartmodule import SmartModule
@@ -34,39 +34,13 @@ class Light(SmartModule, LightInterface):
"""Query to execute during the update cycle."""
return {}
- @property
- def is_color(self) -> bool:
- """Whether the bulb supports color changes."""
- return Module.Color in self._device.modules
-
- @property
- def is_dimmable(self) -> bool:
- """Whether the bulb supports brightness changes."""
- return Module.Brightness in self._device.modules
-
- @property
- def is_variable_color_temp(self) -> bool:
- """Whether the bulb supports color temperature changes."""
- return Module.ColorTemperature in self._device.modules
-
- @property
- def valid_temperature_range(self) -> ColorTempRange:
- """Return the device-specific white temperature range (in Kelvin).
-
- :return: White temperature range in Kelvin (minimum, maximum)
- """
- if not self.is_variable_color_temp:
- raise KasaException("Color temperature not supported")
-
- return self._device.modules[Module.ColorTemperature].valid_temperature_range
-
@property
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
"""Return the current HSV state of the bulb.
:return: hue, saturation and value (degrees, %, %)
"""
- if not self.is_color:
+ if Module.Color not in self._device.modules:
raise KasaException("Bulb does not support color.")
return self._device.modules[Module.Color].hsv
@@ -74,7 +48,7 @@ class Light(SmartModule, LightInterface):
@property
def color_temp(self) -> Annotated[int, FeatureAttribute()]:
"""Whether the bulb supports color temperature changes."""
- if not self.is_variable_color_temp:
+ if Module.ColorTemperature not in self._device.modules:
raise KasaException("Bulb does not support colortemp.")
return self._device.modules[Module.ColorTemperature].color_temp
@@ -82,7 +56,7 @@ class Light(SmartModule, LightInterface):
@property
def brightness(self) -> Annotated[int, FeatureAttribute()]:
"""Return the current brightness in percentage."""
- if not self.is_dimmable: # pragma: no cover
+ if Module.Brightness not in self._device.modules: # pragma: no cover
raise KasaException("Bulb is not dimmable.")
return self._device.modules[Module.Brightness].brightness
@@ -104,7 +78,7 @@ class Light(SmartModule, LightInterface):
:param int value: value between 1 and 100
:param int transition: transition in milliseconds.
"""
- if not self.is_color:
+ if Module.Color not in self._device.modules:
raise KasaException("Bulb does not support color.")
return await self._device.modules[Module.Color].set_hsv(hue, saturation, value)
@@ -119,7 +93,7 @@ class Light(SmartModule, LightInterface):
:param int temp: The new color temperature, in Kelvin
:param int transition: transition in milliseconds.
"""
- if not self.is_variable_color_temp:
+ if Module.ColorTemperature not in self._device.modules:
raise KasaException("Bulb does not support colortemp.")
return await self._device.modules[Module.ColorTemperature].set_color_temp(
temp, brightness=brightness
@@ -135,16 +109,11 @@ class Light(SmartModule, LightInterface):
:param int brightness: brightness in percent
:param int transition: transition in milliseconds.
"""
- if not self.is_dimmable: # pragma: no cover
+ if Module.Brightness not in self._device.modules: # pragma: no cover
raise KasaException("Bulb is not dimmable.")
return await self._device.modules[Module.Brightness].set_brightness(brightness)
- @property
- def has_effects(self) -> bool:
- """Return True if the device supports effects."""
- return Module.LightEffect in self._device.modules
-
async def set_state(self, state: LightState) -> dict:
"""Set the light state."""
state_dict = asdict(state)
@@ -167,16 +136,17 @@ class Light(SmartModule, LightInterface):
return self._light_state
async def _post_update_hook(self) -> None:
- if self._device.is_on is False:
+ device = self._device
+ if device.is_on is False:
state = LightState(light_on=False)
else:
state = LightState(light_on=True)
- if self.is_dimmable:
+ if Module.Brightness in device.modules:
state.brightness = self.brightness
- if self.is_color:
+ if Module.Color in device.modules:
hsv = self.hsv
state.hue = hsv.hue
state.saturation = hsv.saturation
- if self.is_variable_color_temp:
+ if Module.ColorTemperature in device.modules:
state.color_temp = self.color_temp
self._light_state = state
diff --git a/kasa/smart/modules/lightpreset.py b/kasa/smart/modules/lightpreset.py
index 2eba7572..87e96eae 100644
--- a/kasa/smart/modules/lightpreset.py
+++ b/kasa/smart/modules/lightpreset.py
@@ -96,13 +96,18 @@ class LightPreset(SmartModule, LightPresetInterface):
"""Return current preset name."""
light = self._device.modules[SmartModule.Light]
brightness = light.brightness
- color_temp = light.color_temp if light.is_variable_color_temp else None
- h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
+ color_temp = light.color_temp if light.has_feature("color_temp") else None
+ h, s = (
+ (light.hsv.hue, light.hsv.saturation)
+ if light.has_feature("hsv")
+ else (None, None)
+ )
for preset_name, preset in self._presets.items():
if (
preset.brightness == brightness
and (
- preset.color_temp == color_temp or not light.is_variable_color_temp
+ preset.color_temp == color_temp
+ or not light.has_feature("color_temp")
)
and preset.hue == h
and preset.saturation == s
@@ -117,7 +122,7 @@ class LightPreset(SmartModule, LightPresetInterface):
"""Set a light preset for the device."""
light = self._device.modules[SmartModule.Light]
if preset_name == self.PRESET_NOT_SET:
- if light.is_color:
+ if light.has_feature("hsv"):
preset = LightState(hue=0, saturation=0, brightness=100)
else:
preset = LightState(brightness=100)
diff --git a/kasa/smart/modules/matter.py b/kasa/smart/modules/matter.py
new file mode 100644
index 00000000..c6bfe2d8
--- /dev/null
+++ b/kasa/smart/modules/matter.py
@@ -0,0 +1,43 @@
+"""Implementation of matter module."""
+
+from __future__ import annotations
+
+from ...feature import Feature
+from ..smartmodule import SmartModule
+
+
+class Matter(SmartModule):
+ """Implementation of matter module."""
+
+ QUERY_GETTER_NAME: str = "get_matter_setup_info"
+ REQUIRED_COMPONENT = "matter"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="matter_setup_code",
+ name="Matter setup code",
+ container=self,
+ attribute_getter=lambda x: x.info["setup_code"],
+ type=Feature.Type.Sensor,
+ category=Feature.Category.Debug,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="matter_setup_payload",
+ name="Matter setup payload",
+ container=self,
+ attribute_getter=lambda x: x.info["setup_payload"],
+ type=Feature.Type.Sensor,
+ category=Feature.Category.Debug,
+ )
+ )
+
+ @property
+ def info(self) -> dict[str, str]:
+ """Matter setup info."""
+ return self.data
diff --git a/kasa/smart/modules/mop.py b/kasa/smart/modules/mop.py
new file mode 100644
index 00000000..851279e9
--- /dev/null
+++ b/kasa/smart/modules/mop.py
@@ -0,0 +1,90 @@
+"""Implementation of vacuum mop."""
+
+from __future__ import annotations
+
+import logging
+from enum import IntEnum
+from typing import Annotated
+
+from ...feature import Feature
+from ...module import FeatureAttribute
+from ..smartmodule import SmartModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Waterlevel(IntEnum):
+ """Water level for mopping."""
+
+ Disable = 0
+ Low = 1
+ Medium = 2
+ High = 3
+
+
+class Mop(SmartModule):
+ """Implementation of vacuum mop."""
+
+ REQUIRED_COMPONENT = "mop"
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="mop_attached",
+ name="Mop attached",
+ container=self,
+ icon="mdi:square-rounded",
+ attribute_getter="mop_attached",
+ category=Feature.Category.Info,
+ type=Feature.BinarySensor,
+ )
+ )
+
+ self._add_feature(
+ Feature(
+ self._device,
+ id="mop_waterlevel",
+ name="Mop water level",
+ container=self,
+ attribute_getter="waterlevel",
+ attribute_setter="set_waterlevel",
+ icon="mdi:water",
+ choices_getter=lambda: list(Waterlevel.__members__),
+ category=Feature.Category.Config,
+ type=Feature.Type.Choice,
+ )
+ )
+
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ return {
+ "getMopState": {},
+ "getCleanAttr": {"type": "global"},
+ }
+
+ @property
+ def mop_attached(self) -> bool:
+ """Return True if mop is attached."""
+ return self.data["getMopState"]["mop_state"]
+
+ @property
+ def _settings(self) -> dict:
+ """Return settings settings."""
+ return self.data["getCleanAttr"]
+
+ @property
+ def waterlevel(self) -> Annotated[str, FeatureAttribute()]:
+ """Return water level."""
+ return Waterlevel(int(self._settings["cistern"])).name
+
+ async def set_waterlevel(self, mode: str) -> Annotated[dict, FeatureAttribute()]:
+ """Set waterlevel mode."""
+ name_to_value = {x.name: x.value for x in Waterlevel}
+ if mode not in name_to_value:
+ raise ValueError("Invalid waterlevel %s, available %s", mode, name_to_value)
+
+ settings = self._settings.copy()
+ settings["cistern"] = name_to_value[mode]
+ return await self.call("setCleanAttr", settings)
diff --git a/kasa/smart/modules/overheatprotection.py b/kasa/smart/modules/overheatprotection.py
new file mode 100644
index 00000000..cdaba4e8
--- /dev/null
+++ b/kasa/smart/modules/overheatprotection.py
@@ -0,0 +1,41 @@
+"""Overheat module."""
+
+from __future__ import annotations
+
+from ...feature import Feature
+from ..smartmodule import SmartModule
+
+
+class OverheatProtection(SmartModule):
+ """Implementation for overheat_protection."""
+
+ SYSINFO_LOOKUP_KEYS = ["overheated", "overheat_status"]
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ container=self,
+ id="overheated",
+ name="Overheated",
+ attribute_getter="overheated",
+ icon="mdi:heat-wave",
+ type=Feature.Type.BinarySensor,
+ category=Feature.Category.Info,
+ )
+ )
+
+ @property
+ def overheated(self) -> bool:
+ """Return True if device reports overheating."""
+ if (value := self._device.sys_info.get("overheat_status")) is not None:
+ # Value can be normal, cooldown, or overheated.
+ # We report all but normal as overheated.
+ return value != "normal"
+
+ return self._device.sys_info["overheated"]
+
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ return {}
diff --git a/kasa/smart/modules/speaker.py b/kasa/smart/modules/speaker.py
new file mode 100644
index 00000000..e36758b4
--- /dev/null
+++ b/kasa/smart/modules/speaker.py
@@ -0,0 +1,67 @@
+"""Implementation of vacuum speaker."""
+
+from __future__ import annotations
+
+import logging
+from typing import Annotated
+
+from ...feature import Feature
+from ...module import FeatureAttribute
+from ..smartmodule import SmartModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Speaker(SmartModule):
+ """Implementation of vacuum speaker."""
+
+ REQUIRED_COMPONENT = "speaker"
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="locate",
+ name="Locate device",
+ container=self,
+ attribute_setter="locate",
+ category=Feature.Category.Primary,
+ type=Feature.Action,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="volume",
+ name="Volume",
+ container=self,
+ attribute_getter="volume",
+ attribute_setter="set_volume",
+ range_getter=lambda: (0, 100),
+ category=Feature.Category.Config,
+ type=Feature.Type.Number,
+ )
+ )
+
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ return {
+ "getVolume": None,
+ }
+
+ @property
+ def volume(self) -> Annotated[str, FeatureAttribute()]:
+ """Return volume."""
+ return self.data["volume"]
+
+ async def set_volume(self, volume: int) -> Annotated[dict, FeatureAttribute()]:
+ """Set volume."""
+ if volume < 0 or volume > 100:
+ raise ValueError("Volume must be between 0 and 100")
+
+ return await self.call("setVolume", {"volume": volume})
+
+ async def locate(self) -> dict:
+ """Play sound to locate the device."""
+ return await self.call("playSelectAudio", {"audio_type": "seek_me"})
diff --git a/kasa/smart/smartchilddevice.py b/kasa/smart/smartchilddevice.py
index db3319f3..3f730f0e 100644
--- a/kasa/smart/smartchilddevice.py
+++ b/kasa/smart/smartchilddevice.py
@@ -6,10 +6,11 @@ import logging
import time
from typing import Any
+from ..device import DeviceInfo
from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
-from .smartdevice import SmartDevice
+from .smartdevice import ComponentsRaw, SmartDevice
from .smartmodule import SmartModule
_LOGGER = logging.getLogger(__name__)
@@ -23,6 +24,7 @@ class SmartChildDevice(SmartDevice):
CHILD_DEVICE_TYPE_MAP = {
"plug.powerstrip.sub-plug": DeviceType.Plug,
+ "subg.plugswitch.switch": DeviceType.WallSwitch,
"subg.trigger.contact-sensor": DeviceType.Sensor,
"subg.trigger.temp-hmdt-sensor": DeviceType.Sensor,
"subg.trigger.water-leak-sensor": DeviceType.Sensor,
@@ -37,7 +39,7 @@ class SmartChildDevice(SmartDevice):
self,
parent: SmartDevice,
info: dict,
- component_info: dict,
+ component_info_raw: ComponentsRaw,
*,
config: DeviceConfig | None = None,
protocol: SmartProtocol | None = None,
@@ -47,7 +49,24 @@ class SmartChildDevice(SmartDevice):
super().__init__(parent.host, config=parent.config, protocol=_protocol)
self._parent = parent
self._update_internal_state(info)
- self._components = component_info
+ self._components_raw = component_info_raw
+ self._components = self._parse_components(self._components_raw)
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device info.
+
+ Child device does not have it info and components in _last_update so
+ this overrides the base implementation to call _get_device_info with
+ info and components combined as they would be in _last_update.
+ """
+ return self._get_device_info(
+ {
+ "get_device_info": self._info,
+ "component_nego": self._components_raw,
+ },
+ None,
+ )
async def update(self, update_children: bool = True) -> None:
"""Update child module info.
@@ -67,11 +86,22 @@ class SmartChildDevice(SmartDevice):
module_queries: list[SmartModule] = []
req: dict[str, Any] = {}
for module in self.modules.values():
- if module.disabled is False and (mod_query := module.query()):
+ if (
+ module.disabled is False
+ and (mod_query := module.query())
+ and module._should_update(now)
+ ):
module_queries.append(module)
req.update(mod_query)
if req:
- self._last_update = await self.protocol.query(req)
+ first_update = self._last_update != {}
+ try:
+ resp = await self.protocol.query(req)
+ except Exception as ex:
+ resp = await self._handle_modular_update_error(
+ ex, first_update, ", ".join(mod.name for mod in module_queries), req
+ )
+ self._last_update = resp
for module in self.modules.values():
await self._handle_module_post_update(
@@ -79,12 +109,17 @@ class SmartChildDevice(SmartDevice):
)
self._last_update_time = now
+ # We can first initialize the features after the first update.
+ # We make here an assumption that every device has at least a single feature.
+ if not self._features:
+ await self._initialize_features()
+
@classmethod
async def create(
cls,
parent: SmartDevice,
child_info: dict,
- child_components: dict,
+ child_components_raw: ComponentsRaw,
protocol: SmartProtocol | None = None,
*,
last_update: dict | None = None,
@@ -97,7 +132,7 @@ class SmartChildDevice(SmartDevice):
derived from the parent.
"""
child: SmartChildDevice = cls(
- parent, child_info, child_components, protocol=protocol
+ parent, child_info, child_components_raw, protocol=protocol
)
if last_update:
child._last_update = last_update
diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py
index adb4829d..f2daf0d7 100644
--- a/kasa/smart/smartdevice.py
+++ b/kasa/smart/smartdevice.py
@@ -5,11 +5,12 @@ from __future__ import annotations
import base64
import logging
import time
-from collections.abc import Mapping, Sequence
+from collections import OrderedDict
+from collections.abc import Sequence
from datetime import UTC, datetime, timedelta, tzinfo
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, Any, TypeAlias, cast
-from ..device import Device, WifiNetwork, _DeviceInfo
+from ..device import Device, DeviceInfo, WifiNetwork
from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
@@ -40,6 +41,8 @@ _LOGGER = logging.getLogger(__name__)
# same issue, homekit perhaps?
NON_HUB_PARENT_ONLY_MODULES = [DeviceModule, Time, Firmware, Cloud]
+ComponentsRaw: TypeAlias = dict[str, list[dict[str, int | str]]]
+
# Device must go last as the other interfaces also inherit Device
# and python needs a consistent method resolution order.
@@ -61,16 +64,18 @@ class SmartDevice(Device):
)
super().__init__(host=host, config=config, protocol=_protocol)
self.protocol: SmartProtocol
- self._components_raw: dict[str, Any] | None = None
+ self._components_raw: ComponentsRaw | None = None
self._components: dict[str, int] = {}
self._state_information: dict[str, Any] = {}
- self._modules: dict[str | ModuleName[Module], SmartModule] = {}
+ self._modules: OrderedDict[str | ModuleName[Module], SmartModule] = (
+ OrderedDict()
+ )
self._parent: SmartDevice | None = None
- self._children: Mapping[str, SmartDevice] = {}
- self._last_update = {}
+ self._children: dict[str, SmartDevice] = {}
self._last_update_time: float | None = None
self._on_since: datetime | None = None
self._info: dict[str, Any] = {}
+ self._logged_missing_child_ids: set[str] = set()
async def _initialize_children(self) -> None:
"""Initialize children for power strips."""
@@ -81,25 +86,86 @@ class SmartDevice(Device):
resp = await self.protocol.query(child_info_query)
self.internal_state.update(resp)
- children = self.internal_state["get_child_device_list"]["child_device_list"]
- children_components = {
- child["device_id"]: {
- comp["id"]: int(comp["ver_code"]) for comp in child["component_list"]
- }
- for child in self.internal_state["get_child_device_component_list"][
- "child_component_list"
- ]
- }
+ async def _try_create_child(
+ self, info: dict, child_components: dict
+ ) -> SmartDevice | None:
from .smartchilddevice import SmartChildDevice
- self._children = {
- child_info["device_id"]: await SmartChildDevice.create(
- parent=self,
- child_info=child_info,
- child_components=children_components[child_info["device_id"]],
- )
- for child_info in children
+ return await SmartChildDevice.create(
+ parent=self,
+ child_info=info,
+ child_components_raw=child_components,
+ )
+
+ async def _create_delete_children(
+ self,
+ child_device_resp: dict[str, list],
+ child_device_components_resp: dict[str, list],
+ ) -> bool:
+ """Create and delete children. Return True if children changed.
+
+ Adds newly found children and deletes children that are no longer
+ reported by the device. It will only log once per child_id that
+ can't be created to avoid spamming the logs on every update.
+ """
+ changed = False
+ smart_children_components = {
+ child["device_id"]: child
+ for child in child_device_components_resp["child_component_list"]
}
+ children = self._children
+ child_ids: set[str] = set()
+ existing_child_ids = set(self._children.keys())
+
+ for info in child_device_resp["child_device_list"]:
+ if (child_id := info.get("device_id")) and (
+ child_components := smart_children_components.get(child_id)
+ ):
+ child_ids.add(child_id)
+
+ if child_id in existing_child_ids:
+ continue
+
+ child = await self._try_create_child(info, child_components)
+ if child:
+ _LOGGER.debug("Created child device %s for %s", child, self.host)
+ changed = True
+ children[child_id] = child
+ continue
+
+ if child_id not in self._logged_missing_child_ids:
+ self._logged_missing_child_ids.add(child_id)
+ _LOGGER.debug("Child device type not supported: %s", info)
+ continue
+
+ if child_id:
+ if child_id not in self._logged_missing_child_ids:
+ self._logged_missing_child_ids.add(child_id)
+ _LOGGER.debug(
+ "Could not find child components for device %s, "
+ "child_id %s, components: %s: ",
+ self.host,
+ child_id,
+ smart_children_components,
+ )
+ continue
+
+ # If we couldn't get a child device id we still only want to
+ # log once to avoid spamming the logs on every update cycle
+ # so store it under an empty string
+ if "" not in self._logged_missing_child_ids:
+ self._logged_missing_child_ids.add("")
+ _LOGGER.debug(
+ "Could not find child id for device %s, info: %s", self.host, info
+ )
+
+ removed_ids = existing_child_ids - child_ids
+ for removed_id in removed_ids:
+ changed = True
+ removed = children.pop(removed_id)
+ _LOGGER.debug("Removed child device %s from %s", removed, self.host)
+
+ return changed
@property
def children(self) -> Sequence[SmartDevice]:
@@ -131,6 +197,13 @@ class SmartDevice(Device):
f"{request} not found in {responses} for device {self.host}"
)
+ @staticmethod
+ def _parse_components(components_raw: ComponentsRaw) -> dict[str, int]:
+ return {
+ str(comp["id"]): int(comp["ver_code"])
+ for comp in components_raw["component_list"]
+ }
+
async def _negotiate(self) -> None:
"""Perform initialization.
@@ -151,29 +224,41 @@ class SmartDevice(Device):
self._info = self._try_get_response(resp, "get_device_info")
# Create our internal presentation of available components
- self._components_raw = cast(dict, resp["component_nego"])
+ self._components_raw = cast(ComponentsRaw, resp["component_nego"])
- self._components = {
- comp["id"]: int(comp["ver_code"])
- for comp in self._components_raw["component_list"]
- }
+ self._components = self._parse_components(self._components_raw)
if "child_device" in self._components and not self.children:
await self._initialize_children()
- def _update_children_info(self) -> None:
- """Update the internal child device info from the parent info."""
+ async def _update_children_info(self) -> bool:
+ """Update the internal child device info from the parent info.
+
+ Return true if children added or deleted.
+ """
+ changed = False
if child_info := self._try_get_response(
self._last_update, "get_child_device_list", {}
):
+ changed = await self._create_delete_children(
+ child_info, self._last_update["get_child_device_component_list"]
+ )
+
for info in child_info["child_device_list"]:
- self._children[info["device_id"]]._update_internal_state(info)
+ child_id = info.get("device_id")
+ if child_id not in self._children:
+ # _create_delete_children has already logged a message
+ continue
+
+ self._children[child_id]._update_internal_state(info)
+
+ return changed
def _update_internal_info(self, info_resp: dict) -> None:
"""Update the internal device info."""
self._info = self._try_get_response(info_resp, "get_device_info")
- async def update(self, update_children: bool = False) -> None:
+ async def update(self, update_children: bool = True) -> None:
"""Update the device."""
if self.credentials is None and self.credentials_hash is None:
raise AuthenticationError("Tapo plug requires authentication.")
@@ -191,13 +276,13 @@ class SmartDevice(Device):
resp = await self._modular_update(first_update, now)
- self._update_children_info()
+ children_changed = await self._update_children_info()
# Call child update which will only update module calls, info is updated
# from get_child_device_list. update_children only affects hub devices, other
# devices will always update children to prevent errors on module access.
# This needs to go after updating the internal state of the children so that
# child modules have access to their sysinfo.
- if update_children or self.device_type != DeviceType.Hub:
+ if children_changed or update_children or self.device_type != DeviceType.Hub:
for child in self._children.values():
if TYPE_CHECKING:
assert isinstance(child, SmartChildDevice)
@@ -250,11 +335,7 @@ class SmartDevice(Device):
if first_update and module.__class__ in self.FIRST_UPDATE_MODULES:
module._last_update_time = update_time
continue
- if (
- not module.update_interval
- or not module._last_update_time
- or (update_time - module._last_update_time) >= module.update_interval
- ):
+ if module._should_update(update_time):
module_queries.append(module)
req.update(query)
@@ -342,9 +423,8 @@ class SmartDevice(Device):
) or mod.__name__ in child_modules_to_skip:
continue
required_component = cast(str, mod.REQUIRED_COMPONENT)
- if required_component in self._components or (
- mod.REQUIRED_KEY_ON_PARENT
- and self.sys_info.get(mod.REQUIRED_KEY_ON_PARENT) is not None
+ if required_component in self._components or any(
+ self.sys_info.get(key) is not None for key in mod.SYSINFO_LOOKUP_KEYS
):
_LOGGER.debug(
"Device %s, found required %s, adding %s to modules.",
@@ -368,6 +448,11 @@ class SmartDevice(Device):
):
self._modules[Thermostat.__name__] = Thermostat(self, "thermostat")
+ # We move time to the beginning so other modules can access the
+ # time and timezone after update if required. e.g. cleanrecords
+ if Time.__name__ in self._modules:
+ self._modules.move_to_end(Time.__name__, last=False)
+
async def _initialize_features(self) -> None:
"""Initialize device features."""
self._add_feature(
@@ -433,19 +518,6 @@ class SmartDevice(Device):
)
)
- if "overheated" in self._info:
- self._add_feature(
- Feature(
- self,
- id="overheated",
- name="Overheated",
- attribute_getter=lambda x: x._info["overheated"],
- icon="mdi:heat-wave",
- type=Feature.Type.BinarySensor,
- category=Feature.Category.Info,
- )
- )
-
# We check for the key available, and not for the property truthiness,
# as the value is falsy when the device is off.
if "on_time" in self._info:
@@ -473,12 +545,25 @@ class SmartDevice(Device):
)
)
+ if self.parent is not None and (
+ cs := self.parent.modules.get(Module.ChildSetup)
+ ):
+ self._add_feature(
+ Feature(
+ device=self,
+ id="unpair",
+ name="Unpair device",
+ container=cs,
+ attribute_setter=lambda: cs.unpair(self.device_id),
+ category=Feature.Category.Debug,
+ type=Feature.Type.Action,
+ )
+ )
+
for module in self.modules.values():
module._initialize_features()
for feat in module._module_features.values():
self._add_feature(feat)
- for child in self._children.values():
- await child._initialize_features()
@property
def _is_hub_child(self) -> bool:
@@ -500,18 +585,13 @@ class SmartDevice(Device):
@property
def model(self) -> str:
"""Returns the device model."""
- return str(self._info.get("model"))
+ # If update hasn't been called self._device_info can't be used
+ if self._last_update:
+ return self.device_info.short_name
- @property
- def _model_region(self) -> str:
- """Return device full model name and region."""
- if (disco := self._discovery_info) and (
- disco_model := disco.get("device_model")
- ):
- return disco_model
- # Some devices have the region in the specs element.
- region = f"({specs})" if (specs := self._info.get("specs")) else ""
- return f"{self.model}{region}"
+ disco_model = str(self._info.get("device_model"))
+ long_name, _, _ = disco_model.partition("(")
+ return long_name
@property
def alias(self) -> str | None:
@@ -611,12 +691,8 @@ class SmartDevice(Device):
"""
self._info = info
- async def _query_helper(
- self, method: str, params: dict | None = None, child_ids: None = None
- ) -> dict:
- res = await self.protocol.query({method: params})
-
- return res
+ async def _query_helper(self, method: str, params: dict | None = None) -> dict:
+ return await self.protocol.query({method: params})
@property
def ssid(self) -> str:
@@ -765,10 +841,11 @@ class SmartDevice(Device):
if self._device_type is not DeviceType.Unknown:
return self._device_type
- # Fallback to device_type (from disco info)
- type_str = self._info.get("type", self._info.get("device_type"))
-
- if not type_str: # no update or discovery info
+ if (
+ not (type_str := self._info.get("type", self._info.get("device_type")))
+ or not self._components
+ ):
+ # no update or discovery info
return self._device_type
self._device_type = self._get_device_type_from_components(
@@ -804,13 +881,15 @@ class SmartDevice(Device):
return DeviceType.Thermostat
if "ROBOVAC" in device_type:
return DeviceType.Vacuum
+ if "TAPOCHIME" in device_type:
+ return DeviceType.Chime
_LOGGER.warning("Unknown device type, falling back to plug")
return DeviceType.Plug
@staticmethod
def _get_device_info(
info: dict[str, Any], discovery_info: dict[str, Any] | None
- ) -> _DeviceInfo:
+ ) -> DeviceInfo:
"""Get model information for a device."""
di = info["get_device_info"]
components = [comp["id"] for comp in info["component_nego"]["component_list"]]
@@ -839,7 +918,7 @@ class SmartDevice(Device):
# Brand inferred from SMART.KASAPLUG/SMART.TAPOPLUG etc.
brand = devicetype[:4].lower()
- return _DeviceInfo(
+ return DeviceInfo(
short_name=short_name,
long_name=long_name,
brand=brand,
diff --git a/kasa/smart/smartmodule.py b/kasa/smart/smartmodule.py
index c5697043..243852e0 100644
--- a/kasa/smart/smartmodule.py
+++ b/kasa/smart/smartmodule.py
@@ -54,14 +54,16 @@ class SmartModule(Module):
NAME: str
#: Module is initialized, if the given component is available
REQUIRED_COMPONENT: str | None = None
- #: Module is initialized, if the given key available in the main sysinfo
- REQUIRED_KEY_ON_PARENT: str | None = None
+ #: Module is initialized, if any of the given keys exists in the sysinfo
+ SYSINFO_LOOKUP_KEYS: list[str] = []
#: Query to execute during the main update cycle
- QUERY_GETTER_NAME: str
+ QUERY_GETTER_NAME: str = ""
REGISTERED_MODULES: dict[str, type[SmartModule]] = {}
MINIMUM_UPDATE_INTERVAL_SECS = 0
+ MINIMUM_HUB_CHILD_UPDATE_INTERVAL_SECS = 60 * 60 * 24
+
UPDATE_INTERVAL_AFTER_ERROR_SECS = 30
DISABLE_AFTER_ERROR_COUNT = 10
@@ -72,6 +74,7 @@ class SmartModule(Module):
self._last_update_time: float | None = None
self._last_update_error: KasaException | None = None
self._error_count = 0
+ self._logged_remove_keys: list[str] = []
def __init_subclass__(cls, **kwargs) -> None:
# We only want to register submodules in a modules package so that
@@ -106,16 +109,27 @@ class SmartModule(Module):
@property
def update_interval(self) -> int:
"""Time to wait between updates."""
- if self._last_update_error is None:
- return self.MINIMUM_UPDATE_INTERVAL_SECS
+ if self._last_update_error:
+ return self.UPDATE_INTERVAL_AFTER_ERROR_SECS * self._error_count
- return self.UPDATE_INTERVAL_AFTER_ERROR_SECS * self._error_count
+ if self._device._is_hub_child:
+ return self.MINIMUM_HUB_CHILD_UPDATE_INTERVAL_SECS
+
+ return self.MINIMUM_UPDATE_INTERVAL_SECS
@property
def disabled(self) -> bool:
"""Return true if the module is disabled due to errors."""
return self._error_count >= self.DISABLE_AFTER_ERROR_COUNT
+ def _should_update(self, update_time: float) -> bool:
+ """Return true if module should update based on delay parameters."""
+ return (
+ not self.update_interval
+ or not self._last_update_time
+ or (update_time - self._last_update_time) >= self.update_interval
+ )
+
@classmethod
def _module_name(cls) -> str:
return getattr(cls, "NAME", cls.__name__)
@@ -138,7 +152,9 @@ class SmartModule(Module):
Default implementation uses the raw query getter w/o parameters.
"""
- return {self.QUERY_GETTER_NAME: None}
+ if self.QUERY_GETTER_NAME:
+ return {self.QUERY_GETTER_NAME: None}
+ return {}
async def call(self, method: str, params: dict | None = None) -> dict:
"""Call a method.
@@ -147,6 +163,15 @@ class SmartModule(Module):
"""
return await self._device._query_helper(method, params)
+ @property
+ def optional_response_keys(self) -> list[str]:
+ """Return optional response keys for the module.
+
+ Defaults to no keys. Overriding this and providing keys will remove
+ instead of raise on error.
+ """
+ return []
+
@property
def data(self) -> dict[str, Any]:
"""Return response data for the module.
@@ -179,12 +204,31 @@ class SmartModule(Module):
filtered_data = {k: v for k, v in dev._last_update.items() if k in q_keys}
+ remove_keys: list[str] = []
for data_item in filtered_data:
if isinstance(filtered_data[data_item], SmartErrorCode):
- raise DeviceError(
- f"{data_item} for {self.name}", error_code=filtered_data[data_item]
+ if data_item in self.optional_response_keys:
+ remove_keys.append(data_item)
+ else:
+ raise DeviceError(
+ f"{data_item} for {self.name}",
+ error_code=filtered_data[data_item],
+ )
+
+ for key in remove_keys:
+ if key not in self._logged_remove_keys:
+ self._logged_remove_keys.append(key)
+ _LOGGER.debug(
+ "Removed key %s from response for device %s as it returned "
+ "error: %s. This message will only be logged once per key.",
+ key,
+ self._device.host,
+ filtered_data[key],
)
- if len(filtered_data) == 1:
+
+ filtered_data.pop(key)
+
+ if len(filtered_data) == 1 and not remove_keys:
return next(iter(filtered_data.values()))
return filtered_data
diff --git a/kasa/smartcam/__init__.py b/kasa/smartcam/__init__.py
index 574459f4..21cbeb50 100644
--- a/kasa/smartcam/__init__.py
+++ b/kasa/smartcam/__init__.py
@@ -1,5 +1,6 @@
"""Package for supporting tapo-branded cameras."""
+from .smartcamchild import SmartCamChild
from .smartcamdevice import SmartCamDevice
-__all__ = ["SmartCamDevice"]
+__all__ = ["SmartCamDevice", "SmartCamChild"]
diff --git a/kasa/smartcam/modules/__init__.py b/kasa/smartcam/modules/__init__.py
index 16d59581..4f6ed866 100644
--- a/kasa/smartcam/modules/__init__.py
+++ b/kasa/smartcam/modules/__init__.py
@@ -1,19 +1,39 @@
"""Modules for SMARTCAM devices."""
from .alarm import Alarm
+from .babycrydetection import BabyCryDetection
+from .battery import Battery
from .camera import Camera
from .childdevice import ChildDevice
+from .childsetup import ChildSetup
from .device import DeviceModule
+from .homekit import HomeKit
from .led import Led
+from .lensmask import LensMask
+from .matter import Matter
+from .motiondetection import MotionDetection
from .pantilt import PanTilt
+from .persondetection import PersonDetection
+from .petdetection import PetDetection
+from .tamperdetection import TamperDetection
from .time import Time
__all__ = [
"Alarm",
+ "BabyCryDetection",
+ "Battery",
"Camera",
"ChildDevice",
+ "ChildSetup",
"DeviceModule",
"Led",
"PanTilt",
+ "PersonDetection",
+ "PetDetection",
"Time",
+ "HomeKit",
+ "Matter",
+ "MotionDetection",
+ "LensMask",
+ "TamperDetection",
]
diff --git a/kasa/smartcam/modules/alarm.py b/kasa/smartcam/modules/alarm.py
index 12d43464..5330f309 100644
--- a/kasa/smartcam/modules/alarm.py
+++ b/kasa/smartcam/modules/alarm.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from ...feature import Feature
+from ...smart.smartmodule import allow_update_after
from ..smartcammodule import SmartCamModule
DURATION_MIN = 0
@@ -110,6 +111,7 @@ class Alarm(SmartCamModule):
"""Return current alarm sound."""
return self.data["getSirenConfig"]["siren_type"]
+ @allow_update_after
async def set_alarm_sound(self, sound: str) -> dict:
"""Set alarm sound.
@@ -134,6 +136,7 @@ class Alarm(SmartCamModule):
"""
return int(self.data["getSirenConfig"]["volume"])
+ @allow_update_after
async def set_alarm_volume(self, volume: int) -> dict:
"""Set alarm volume."""
if volume < VOLUME_MIN or volume > VOLUME_MAX:
@@ -145,6 +148,7 @@ class Alarm(SmartCamModule):
"""Return alarm duration."""
return self.data["getSirenConfig"]["duration"]
+ @allow_update_after
async def set_alarm_duration(self, duration: int) -> dict:
"""Set alarm volume."""
if duration < DURATION_MIN or duration > DURATION_MAX:
diff --git a/kasa/smartcam/modules/babycrydetection.py b/kasa/smartcam/modules/babycrydetection.py
new file mode 100644
index 00000000..75399885
--- /dev/null
+++ b/kasa/smartcam/modules/babycrydetection.py
@@ -0,0 +1,49 @@
+"""Implementation of baby cry detection module."""
+
+from __future__ import annotations
+
+import logging
+
+from ...feature import Feature
+from ...smart.smartmodule import allow_update_after
+from ..smartcammodule import SmartCamModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class BabyCryDetection(SmartCamModule):
+ """Implementation of baby cry detection module."""
+
+ REQUIRED_COMPONENT = "babyCryDetection"
+
+ QUERY_GETTER_NAME = "getBCDConfig"
+ QUERY_MODULE_NAME = "sound_detection"
+ QUERY_SECTION_NAMES = "bcd"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="baby_cry_detection",
+ name="Baby cry detection",
+ container=self,
+ attribute_getter="enabled",
+ attribute_setter="set_enabled",
+ type=Feature.Type.Switch,
+ category=Feature.Category.Config,
+ )
+ )
+
+ @property
+ def enabled(self) -> bool:
+ """Return the baby cry detection enabled state."""
+ return self.data["bcd"]["enabled"] == "on"
+
+ @allow_update_after
+ async def set_enabled(self, enable: bool) -> dict:
+ """Set the baby cry detection enabled state."""
+ params = {"enabled": "on" if enable else "off"}
+ return await self._device._query_setter_helper(
+ "setBCDConfig", self.QUERY_MODULE_NAME, "bcd", params
+ )
diff --git a/kasa/smartcam/modules/battery.py b/kasa/smartcam/modules/battery.py
new file mode 100644
index 00000000..d6bd97f3
--- /dev/null
+++ b/kasa/smartcam/modules/battery.py
@@ -0,0 +1,113 @@
+"""Implementation of baby cry detection module."""
+
+from __future__ import annotations
+
+import logging
+
+from ...feature import Feature
+from ..smartcammodule import SmartCamModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Battery(SmartCamModule):
+ """Implementation of a battery module."""
+
+ REQUIRED_COMPONENT = "battery"
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ self._add_feature(
+ Feature(
+ self._device,
+ "battery_low",
+ "Battery low",
+ container=self,
+ attribute_getter="battery_low",
+ icon="mdi:alert",
+ type=Feature.Type.BinarySensor,
+ category=Feature.Category.Debug,
+ )
+ )
+
+ self._add_feature(
+ Feature(
+ self._device,
+ "battery_level",
+ "Battery level",
+ container=self,
+ attribute_getter="battery_percent",
+ icon="mdi:battery",
+ unit_getter=lambda: "%",
+ category=Feature.Category.Info,
+ type=Feature.Type.Sensor,
+ )
+ )
+
+ 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,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ "battery_charging",
+ "Battery charging",
+ container=self,
+ attribute_getter="battery_charging",
+ icon="mdi:alert",
+ type=Feature.Type.BinarySensor,
+ category=Feature.Category.Debug,
+ )
+ )
+
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ return {}
+
+ @property
+ def battery_percent(self) -> int:
+ """Return battery level."""
+ return self._device.sys_info["battery_percent"]
+
+ @property
+ def battery_low(self) -> bool:
+ """Return True if battery is low."""
+ 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"]
+
+ @property
+ def battery_voltage(self) -> bool:
+ """Return battery voltage in V."""
+ return self._device.sys_info["battery_voltage"] / 1_000
+
+ @property
+ def battery_charging(self) -> bool:
+ """Return True if battery is charging."""
+ return self._device.sys_info["battery_voltage"] != "NO"
diff --git a/kasa/smartcam/modules/camera.py b/kasa/smartcam/modules/camera.py
index 815db62b..bd4b2808 100644
--- a/kasa/smartcam/modules/camera.py
+++ b/kasa/smartcam/modules/camera.py
@@ -1,47 +1,69 @@
-"""Implementation of device module."""
+"""Implementation of camera module."""
from __future__ import annotations
import base64
import logging
+from enum import StrEnum
+from typing import Annotated
from urllib.parse import quote_plus
from ...credentials import Credentials
-from ...device_type import DeviceType
from ...feature import Feature
from ...json import loads as json_loads
+from ...module import FeatureAttribute, Module
from ..smartcammodule import SmartCamModule
_LOGGER = logging.getLogger(__name__)
LOCAL_STREAMING_PORT = 554
+ONVIF_PORT = 2020
+
+
+class StreamResolution(StrEnum):
+ """Class for stream resolution."""
+
+ HD = "HD"
+ SD = "SD"
class Camera(SmartCamModule):
"""Implementation of device module."""
- QUERY_GETTER_NAME = "getLensMaskConfig"
- QUERY_MODULE_NAME = "lens_mask"
- QUERY_SECTION_NAMES = "lens_mask_info"
+ REQUIRED_COMPONENT = "video"
def _initialize_features(self) -> None:
"""Initialize features after the initial update."""
- self._add_feature(
- Feature(
- self._device,
- id="state",
- name="State",
- attribute_getter="is_on",
- attribute_setter="set_state",
- type=Feature.Type.Switch,
- category=Feature.Category.Primary,
+ if Module.LensMask in self._device.modules:
+ self._add_feature(
+ Feature(
+ self._device,
+ id="state",
+ name="State",
+ container=self,
+ attribute_getter="is_on",
+ attribute_setter="set_state",
+ type=Feature.Type.Switch,
+ category=Feature.Category.Primary,
+ )
)
- )
@property
def is_on(self) -> bool:
- """Return the device id."""
- return self.data["lens_mask_info"]["enabled"] == "off"
+ """Return the device on state."""
+ if lens_mask := self._device.modules.get(Module.LensMask):
+ return not lens_mask.enabled
+ return True
+
+ async def set_state(self, on: bool) -> Annotated[dict, FeatureAttribute()]:
+ """Set the device on state.
+
+ If the device does not support setting state will do nothing.
+ """
+ if lens_mask := self._device.modules.get(Module.LensMask):
+ # Turning off enables the privacy mask which is why value is reversed.
+ return await lens_mask.set_enabled(not on)
+ return {}
def _get_credentials(self) -> Credentials | None:
"""Get credentials from ."""
@@ -64,7 +86,12 @@ class Camera(SmartCamModule):
return None
- def stream_rtsp_url(self, credentials: Credentials | None = None) -> str | None:
+ def stream_rtsp_url(
+ self,
+ credentials: Credentials | None = None,
+ *,
+ stream_resolution: StreamResolution = StreamResolution.HD,
+ ) -> str | None:
"""Return the local rtsp streaming url.
:param credentials: Credentials for camera account.
@@ -73,26 +100,30 @@ class Camera(SmartCamModule):
:return: rtsp url with escaped credentials or None if no credentials or
camera is off.
"""
- if not self.is_on:
+ if self._device._is_hub_child:
return None
- dev = self._device
+
+ streams = {
+ StreamResolution.HD: "stream1",
+ StreamResolution.SD: "stream2",
+ }
+ if (stream := streams.get(stream_resolution)) is None:
+ return None
+
if not credentials:
credentials = self._get_credentials()
if not credentials or not credentials.username or not credentials.password:
return None
+
username = quote_plus(credentials.username)
password = quote_plus(credentials.password)
- return f"rtsp://{username}:{password}@{dev.host}:{LOCAL_STREAMING_PORT}/stream1"
- async def set_state(self, on: bool) -> dict:
- """Set the device state."""
- # Turning off enables the privacy mask which is why value is reversed.
- params = {"enabled": "off" if on else "on"}
- return await self._device._query_setter_helper(
- "setLensMaskConfig", self.QUERY_MODULE_NAME, "lens_mask_info", params
- )
+ return f"rtsp://{username}:{password}@{self._device.host}:{LOCAL_STREAMING_PORT}/{stream}"
- async def _check_supported(self) -> bool:
- """Additional check to see if the module is supported by the device."""
- return self._device.device_type is DeviceType.Camera
+ def onvif_url(self) -> str | None:
+ """Return the onvif url."""
+ if self._device._is_hub_child:
+ return None
+
+ return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service"
diff --git a/kasa/smartcam/modules/childdevice.py b/kasa/smartcam/modules/childdevice.py
index c4de5838..812fd0c1 100644
--- a/kasa/smartcam/modules/childdevice.py
+++ b/kasa/smartcam/modules/childdevice.py
@@ -19,7 +19,10 @@ class ChildDevice(SmartCamModule):
Default implementation uses the raw query getter w/o parameters.
"""
- return {self.QUERY_GETTER_NAME: {"childControl": {"start_index": 0}}}
+ q = {self.QUERY_GETTER_NAME: {"childControl": {"start_index": 0}}}
+ if self._device.device_type is DeviceType.Hub:
+ q["getChildDeviceComponentList"] = {"childControl": {"start_index": 0}}
+ return q
async def _check_supported(self) -> bool:
"""Additional check to see if the module is supported by the device."""
diff --git a/kasa/smartcam/modules/childsetup.py b/kasa/smartcam/modules/childsetup.py
new file mode 100644
index 00000000..d54bce4e
--- /dev/null
+++ b/kasa/smartcam/modules/childsetup.py
@@ -0,0 +1,107 @@
+"""Implementation for child device setup.
+
+This module allows pairing and disconnecting child devices.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+
+from ...feature import Feature
+from ..smartcammodule import SmartCamModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ChildSetup(SmartCamModule):
+ """Implementation for child device setup."""
+
+ REQUIRED_COMPONENT = "childQuickSetup"
+ QUERY_GETTER_NAME = "getSupportChildDeviceCategory"
+ QUERY_MODULE_NAME = "childControl"
+ _categories: list[str] = []
+
+ def _initialize_features(self) -> None:
+ """Initialize features."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="pair",
+ name="Pair",
+ container=self,
+ attribute_setter="pair",
+ category=Feature.Category.Config,
+ type=Feature.Type.Action,
+ )
+ )
+
+ async def _post_update_hook(self) -> None:
+ if not self._categories:
+ self._categories = [
+ cat["category"].replace("ipcamera", "camera")
+ for cat in self.data["device_category_list"]
+ ]
+
+ @property
+ def supported_child_device_categories(self) -> list[str]:
+ """Supported child device categories."""
+ return self._categories
+
+ async def pair(self, *, timeout: int = 10) -> list[dict]:
+ """Scan for new devices and pair after discovering first new device."""
+ await self.call(
+ "startScanChildDevice", {"childControl": {"category": self._categories}}
+ )
+
+ _LOGGER.info("Waiting %s seconds for discovering new devices", timeout)
+
+ await asyncio.sleep(timeout)
+ res = await self.call(
+ "getScanChildDeviceList", {"childControl": {"category": self._categories}}
+ )
+
+ detected_list = res["getScanChildDeviceList"]["child_device_list"]
+ if not detected_list:
+ _LOGGER.warning(
+ "No devices found, make sure to activate pairing "
+ "mode on the devices to be added."
+ )
+ return []
+
+ _LOGGER.info(
+ "Discovery done, found %s devices: %s",
+ len(detected_list),
+ detected_list,
+ )
+ return await self._add_devices(detected_list)
+
+ async def _add_devices(self, detected_list: list[dict]) -> list:
+ """Add devices based on getScanChildDeviceList response."""
+ await self.call(
+ "addScanChildDeviceList",
+ {"childControl": {"child_device_list": detected_list}},
+ )
+
+ await self._device.update()
+
+ successes = []
+ for detected in detected_list:
+ device_id = detected["device_id"]
+
+ result = "not added"
+ if device_id in self._device._children:
+ result = "added"
+ successes.append(detected)
+
+ msg = f"{detected['device_model']} - {device_id} - {result}"
+ _LOGGER.info("Adding child to %s: %s", self._device.host, msg)
+
+ return successes
+
+ async def unpair(self, device_id: str) -> dict:
+ """Remove device from the hub."""
+ _LOGGER.info("Going to unpair %s from %s", device_id, self)
+
+ payload = {"childControl": {"child_device_list": [{"device_id": device_id}]}}
+ return await self.call("removeChildDeviceList", payload)
diff --git a/kasa/smartcam/modules/device.py b/kasa/smartcam/modules/device.py
index 0541d75c..7f84de1e 100644
--- a/kasa/smartcam/modules/device.py
+++ b/kasa/smartcam/modules/device.py
@@ -14,6 +14,18 @@ class DeviceModule(SmartCamModule):
QUERY_MODULE_NAME = "device_info"
QUERY_SECTION_NAMES = ["basic_info", "info"]
+ def query(self) -> dict:
+ """Query to execute during the update cycle."""
+ if self._device._is_hub_child:
+ # Child devices get their device info updated by the parent device.
+ # and generally don't support connection type as they're not
+ # connected to the network
+ return {}
+ q = super().query()
+ q["getConnectionType"] = {"network": {"get_connection_type": []}}
+
+ return q
+
def _initialize_features(self) -> None:
"""Initialize features after the initial update."""
self._add_feature(
@@ -26,6 +38,32 @@ class DeviceModule(SmartCamModule):
type=Feature.Type.Sensor,
)
)
+ if self.rssi is not None:
+ self._add_feature(
+ Feature(
+ self._device,
+ container=self,
+ id="rssi",
+ name="RSSI",
+ attribute_getter="rssi",
+ icon="mdi:signal",
+ unit_getter=lambda: "dBm",
+ category=Feature.Category.Debug,
+ type=Feature.Type.Sensor,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ container=self,
+ id="signal_level",
+ name="Signal Level",
+ attribute_getter="signal_level",
+ icon="mdi:signal",
+ category=Feature.Category.Info,
+ type=Feature.Type.Sensor,
+ )
+ )
async def _post_update_hook(self) -> None:
"""Overriden to prevent module disabling.
@@ -37,4 +75,14 @@ class DeviceModule(SmartCamModule):
@property
def device_id(self) -> str:
"""Return the device id."""
- return self.data["basic_info"]["dev_id"]
+ return self._device._info["device_id"]
+
+ @property
+ def rssi(self) -> int | None:
+ """Return the device id."""
+ return self.data.get("getConnectionType", {}).get("rssiValue")
+
+ @property
+ def signal_level(self) -> int | None:
+ """Return the device id."""
+ return self.data.get("getConnectionType", {}).get("rssi")
diff --git a/kasa/smartcam/modules/homekit.py b/kasa/smartcam/modules/homekit.py
new file mode 100644
index 00000000..a35de4f9
--- /dev/null
+++ b/kasa/smartcam/modules/homekit.py
@@ -0,0 +1,16 @@
+"""Implementation of homekit module."""
+
+from __future__ import annotations
+
+from ..smartcammodule import SmartCamModule
+
+
+class HomeKit(SmartCamModule):
+ """Implementation of homekit module."""
+
+ REQUIRED_COMPONENT = "homekit"
+
+ @property
+ def info(self) -> dict[str, str]:
+ """Not supported, return empty dict."""
+ return {}
diff --git a/kasa/smartcam/modules/led.py b/kasa/smartcam/modules/led.py
index fb62c52d..5b0912e7 100644
--- a/kasa/smartcam/modules/led.py
+++ b/kasa/smartcam/modules/led.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from ...interfaces.led import Led as LedInterface
+from ...smart.smartmodule import allow_update_after
from ..smartcammodule import SmartCamModule
@@ -19,6 +20,7 @@ class Led(SmartCamModule, LedInterface):
"""Return current led status."""
return self.data["config"]["enabled"] == "on"
+ @allow_update_after
async def set_led(self, enable: bool) -> dict:
"""Set led.
diff --git a/kasa/smartcam/modules/lensmask.py b/kasa/smartcam/modules/lensmask.py
new file mode 100644
index 00000000..22ae0ab3
--- /dev/null
+++ b/kasa/smartcam/modules/lensmask.py
@@ -0,0 +1,33 @@
+"""Implementation of lens mask privacy module."""
+
+from __future__ import annotations
+
+import logging
+
+from ...smart.smartmodule import allow_update_after
+from ..smartcammodule import SmartCamModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class LensMask(SmartCamModule):
+ """Implementation of lens mask module."""
+
+ REQUIRED_COMPONENT = "lensMask"
+
+ QUERY_GETTER_NAME = "getLensMaskConfig"
+ QUERY_MODULE_NAME = "lens_mask"
+ QUERY_SECTION_NAMES = "lens_mask_info"
+
+ @property
+ def enabled(self) -> bool:
+ """Return the lens mask state."""
+ return self.data["lens_mask_info"]["enabled"] == "on"
+
+ @allow_update_after
+ async def set_enabled(self, enable: bool) -> dict:
+ """Set the lens mask state."""
+ params = {"enabled": "on" if enable else "off"}
+ return await self._device._query_setter_helper(
+ "setLensMaskConfig", self.QUERY_MODULE_NAME, "lens_mask_info", params
+ )
diff --git a/kasa/smartcam/modules/matter.py b/kasa/smartcam/modules/matter.py
new file mode 100644
index 00000000..8ea0e4cf
--- /dev/null
+++ b/kasa/smartcam/modules/matter.py
@@ -0,0 +1,44 @@
+"""Implementation of matter module."""
+
+from __future__ import annotations
+
+from ...feature import Feature
+from ..smartcammodule import SmartCamModule
+
+
+class Matter(SmartCamModule):
+ """Implementation of matter module."""
+
+ QUERY_GETTER_NAME = "getMatterSetupInfo"
+ QUERY_MODULE_NAME = "matter"
+ REQUIRED_COMPONENT = "matter"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="matter_setup_code",
+ name="Matter setup code",
+ container=self,
+ attribute_getter=lambda x: x.info["setup_code"],
+ type=Feature.Type.Sensor,
+ category=Feature.Category.Debug,
+ )
+ )
+ self._add_feature(
+ Feature(
+ self._device,
+ id="matter_setup_payload",
+ name="Matter setup payload",
+ container=self,
+ attribute_getter=lambda x: x.info["setup_payload"],
+ type=Feature.Type.Sensor,
+ category=Feature.Category.Debug,
+ )
+ )
+
+ @property
+ def info(self) -> dict[str, str]:
+ """Matter setup info."""
+ return self.data
diff --git a/kasa/smartcam/modules/motiondetection.py b/kasa/smartcam/modules/motiondetection.py
new file mode 100644
index 00000000..dd3c168e
--- /dev/null
+++ b/kasa/smartcam/modules/motiondetection.py
@@ -0,0 +1,49 @@
+"""Implementation of motion detection module."""
+
+from __future__ import annotations
+
+import logging
+
+from ...feature import Feature
+from ...smart.smartmodule import allow_update_after
+from ..smartcammodule import SmartCamModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class MotionDetection(SmartCamModule):
+ """Implementation of motion detection module."""
+
+ REQUIRED_COMPONENT = "detection"
+
+ QUERY_GETTER_NAME = "getDetectionConfig"
+ QUERY_MODULE_NAME = "motion_detection"
+ QUERY_SECTION_NAMES = "motion_det"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="motion_detection",
+ name="Motion detection",
+ container=self,
+ attribute_getter="enabled",
+ attribute_setter="set_enabled",
+ type=Feature.Type.Switch,
+ category=Feature.Category.Config,
+ )
+ )
+
+ @property
+ def enabled(self) -> bool:
+ """Return the motion detection enabled state."""
+ return self.data["motion_det"]["enabled"] == "on"
+
+ @allow_update_after
+ async def set_enabled(self, enable: bool) -> dict:
+ """Set the motion detection enabled state."""
+ params = {"enabled": "on" if enable else "off"}
+ return await self._device._query_setter_helper(
+ "setDetectionConfig", self.QUERY_MODULE_NAME, "motion_det", params
+ )
diff --git a/kasa/smartcam/modules/persondetection.py b/kasa/smartcam/modules/persondetection.py
new file mode 100644
index 00000000..96b31dc4
--- /dev/null
+++ b/kasa/smartcam/modules/persondetection.py
@@ -0,0 +1,49 @@
+"""Implementation of person detection module."""
+
+from __future__ import annotations
+
+import logging
+
+from ...feature import Feature
+from ...smart.smartmodule import allow_update_after
+from ..smartcammodule import SmartCamModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class PersonDetection(SmartCamModule):
+ """Implementation of person detection module."""
+
+ REQUIRED_COMPONENT = "personDetection"
+
+ QUERY_GETTER_NAME = "getPersonDetectionConfig"
+ QUERY_MODULE_NAME = "people_detection"
+ QUERY_SECTION_NAMES = "detection"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="person_detection",
+ name="Person detection",
+ container=self,
+ attribute_getter="enabled",
+ attribute_setter="set_enabled",
+ type=Feature.Type.Switch,
+ category=Feature.Category.Config,
+ )
+ )
+
+ @property
+ def enabled(self) -> bool:
+ """Return the person detection enabled state."""
+ return self.data["detection"]["enabled"] == "on"
+
+ @allow_update_after
+ async def set_enabled(self, enable: bool) -> dict:
+ """Set the person detection enabled state."""
+ params = {"enabled": "on" if enable else "off"}
+ return await self._device._query_setter_helper(
+ "setPersonDetectionConfig", self.QUERY_MODULE_NAME, "detection", params
+ )
diff --git a/kasa/smartcam/modules/petdetection.py b/kasa/smartcam/modules/petdetection.py
new file mode 100644
index 00000000..2c716230
--- /dev/null
+++ b/kasa/smartcam/modules/petdetection.py
@@ -0,0 +1,49 @@
+"""Implementation of pet detection module."""
+
+from __future__ import annotations
+
+import logging
+
+from ...feature import Feature
+from ...smart.smartmodule import allow_update_after
+from ..smartcammodule import SmartCamModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class PetDetection(SmartCamModule):
+ """Implementation of pet detection module."""
+
+ REQUIRED_COMPONENT = "petDetection"
+
+ QUERY_GETTER_NAME = "getPetDetectionConfig"
+ QUERY_MODULE_NAME = "pet_detection"
+ QUERY_SECTION_NAMES = "detection"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="pet_detection",
+ name="Pet detection",
+ container=self,
+ attribute_getter="enabled",
+ attribute_setter="set_enabled",
+ type=Feature.Type.Switch,
+ category=Feature.Category.Config,
+ )
+ )
+
+ @property
+ def enabled(self) -> bool:
+ """Return the pet detection enabled state."""
+ return self.data["detection"]["enabled"] == "on"
+
+ @allow_update_after
+ async def set_enabled(self, enable: bool) -> dict:
+ """Set the pet detection enabled state."""
+ params = {"enabled": "on" if enable else "off"}
+ return await self._device._query_setter_helper(
+ "setPetDetectionConfig", self.QUERY_MODULE_NAME, "detection", params
+ )
diff --git a/kasa/smartcam/modules/tamperdetection.py b/kasa/smartcam/modules/tamperdetection.py
new file mode 100644
index 00000000..f572ded6
--- /dev/null
+++ b/kasa/smartcam/modules/tamperdetection.py
@@ -0,0 +1,49 @@
+"""Implementation of tamper detection module."""
+
+from __future__ import annotations
+
+import logging
+
+from ...feature import Feature
+from ...smart.smartmodule import allow_update_after
+from ..smartcammodule import SmartCamModule
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TamperDetection(SmartCamModule):
+ """Implementation of tamper detection module."""
+
+ REQUIRED_COMPONENT = "tamperDetection"
+
+ QUERY_GETTER_NAME = "getTamperDetectionConfig"
+ QUERY_MODULE_NAME = "tamper_detection"
+ QUERY_SECTION_NAMES = "tamper_det"
+
+ def _initialize_features(self) -> None:
+ """Initialize features after the initial update."""
+ self._add_feature(
+ Feature(
+ self._device,
+ id="tamper_detection",
+ name="Tamper detection",
+ container=self,
+ attribute_getter="enabled",
+ attribute_setter="set_enabled",
+ type=Feature.Type.Switch,
+ category=Feature.Category.Config,
+ )
+ )
+
+ @property
+ def enabled(self) -> bool:
+ """Return the tamper detection enabled state."""
+ return self.data["tamper_det"]["enabled"] == "on"
+
+ @allow_update_after
+ async def set_enabled(self, enable: bool) -> dict:
+ """Set the tamper detection enabled state."""
+ params = {"enabled": "on" if enable else "off"}
+ return await self._device._query_setter_helper(
+ "setTamperDetectionConfig", self.QUERY_MODULE_NAME, "tamper_det", params
+ )
diff --git a/kasa/smartcam/modules/time.py b/kasa/smartcam/modules/time.py
index 4e5cb8df..54ee30e5 100644
--- a/kasa/smartcam/modules/time.py
+++ b/kasa/smartcam/modules/time.py
@@ -9,6 +9,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from ...cachedzoneinfo import CachedZoneInfo
from ...feature import Feature
from ...interfaces import Time as TimeInterface
+from ...smart.smartmodule import allow_update_after
from ..smartcammodule import SmartCamModule
@@ -73,6 +74,7 @@ class Time(SmartCamModule, TimeInterface):
"""Return device's current datetime."""
return self._time
+ @allow_update_after
async def set_time(self, dt: datetime) -> dict:
"""Set device time."""
if not dt.tzinfo:
diff --git a/kasa/smartcam/smartcamchild.py b/kasa/smartcam/smartcamchild.py
new file mode 100644
index 00000000..d2614464
--- /dev/null
+++ b/kasa/smartcam/smartcamchild.py
@@ -0,0 +1,118 @@
+"""Child device implementation."""
+
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from ..device import DeviceInfo
+from ..device_type import DeviceType
+from ..deviceconfig import DeviceConfig
+from ..protocols.smartcamprotocol import _ChildCameraProtocolWrapper
+from ..protocols.smartprotocol import SmartProtocol
+from ..smart.smartchilddevice import SmartChildDevice
+from ..smart.smartdevice import ComponentsRaw, SmartDevice
+from .smartcamdevice import SmartCamDevice
+
+_LOGGER = logging.getLogger(__name__)
+
+# SmartCamChild devices have a different info format from getChildDeviceInfo
+# than when querying getDeviceInfo directly on the child.
+# As _get_device_info is also called by dump_devtools and generate_supported
+# this key will be expected by _get_device_info
+CHILD_INFO_FROM_PARENT = "child_info_from_parent"
+
+
+class SmartCamChild(SmartChildDevice, SmartCamDevice):
+ """Presentation of a child device.
+
+ This wraps the protocol communications and sets internal data for the child.
+ """
+
+ CHILD_DEVICE_TYPE_MAP = {
+ "camera": DeviceType.Camera,
+ }
+
+ def __init__(
+ self,
+ parent: SmartDevice,
+ info: dict,
+ component_info_raw: ComponentsRaw,
+ *,
+ config: DeviceConfig | None = None,
+ protocol: SmartProtocol | None = None,
+ ) -> None:
+ _protocol = protocol or _ChildCameraProtocolWrapper(
+ info["device_id"], parent.protocol
+ )
+ super().__init__(parent, info, component_info_raw, protocol=_protocol)
+ self._child_info_from_parent: dict = {}
+
+ @property
+ def device_info(self) -> DeviceInfo:
+ """Return device info.
+
+ Child device does not have it info and components in _last_update so
+ this overrides the base implementation to call _get_device_info with
+ info and components combined as they would be in _last_update.
+ """
+ return self._get_device_info(
+ {
+ CHILD_INFO_FROM_PARENT: self._child_info_from_parent,
+ },
+ None,
+ )
+
+ @staticmethod
+ def _map_child_info_from_parent(device_info: dict) -> dict:
+ mappings = {
+ "device_model": "model",
+ "sw_ver": "fw_ver",
+ "hw_id": "hwId",
+ }
+ return {mappings.get(k, k): v for k, v in device_info.items()}
+
+ def _update_internal_state(self, info: dict[str, Any]) -> None:
+ """Update the internal info state.
+
+ This is used by the parent to push updates to its children.
+ """
+ # smartcam children have info with different keys to their own
+ # getDeviceInfo queries
+ self._child_info_from_parent = info
+
+ # self._info will have the values normalized across smart and smartcam
+ # devices
+ self._info = self._map_child_info_from_parent(info)
+
+ @property
+ def device_type(self) -> DeviceType:
+ """Return the device type."""
+ if self._device_type == DeviceType.Unknown and self._info:
+ self._device_type = self._get_device_type_from_sysinfo(self._info)
+ return self._device_type
+
+ @staticmethod
+ def _get_device_info(
+ info: dict[str, Any], discovery_info: dict[str, Any] | None
+ ) -> DeviceInfo:
+ """Get model information for a device."""
+ if not (cifp := info.get(CHILD_INFO_FROM_PARENT)):
+ return SmartCamDevice._get_device_info(info, discovery_info)
+
+ model = cifp["device_model"]
+ device_type = SmartCamDevice._get_device_type_from_sysinfo(cifp)
+ fw_version_full = cifp["sw_ver"]
+ firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
+ return DeviceInfo(
+ short_name=model,
+ long_name=model,
+ brand="tapo",
+ device_family=cifp["device_type"],
+ device_type=device_type,
+ hardware_version=cifp["hw_ver"],
+ firmware_version=firmware_version,
+ firmware_build=firmware_build,
+ requires_auth=True,
+ region=cifp.get("region"),
+ )
diff --git a/kasa/smartcam/smartcamdevice.py b/kasa/smartcam/smartcamdevice.py
index 0e49be26..1bf58532 100644
--- a/kasa/smartcam/smartcamdevice.py
+++ b/kasa/smartcam/smartcamdevice.py
@@ -3,13 +3,14 @@
from __future__ import annotations
import logging
-from typing import Any
+from typing import Any, cast
-from ..device import _DeviceInfo
+from ..device import DeviceInfo
from ..device_type import DeviceType
from ..module import Module
from ..protocols.smartcamprotocol import _ChildCameraProtocolWrapper
from ..smart import SmartChildDevice, SmartDevice
+from ..smart.smartdevice import ComponentsRaw
from .modules import ChildDevice, DeviceModule
from .smartcammodule import SmartCamModule
@@ -25,18 +26,21 @@ class SmartCamDevice(SmartDevice):
@staticmethod
def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType:
"""Find type to be displayed as a supported device category."""
- if (
- sysinfo
- and (device_type := sysinfo.get("device_type"))
- and device_type.endswith("HUB")
- ):
+ if not (device_type := sysinfo.get("device_type")):
+ return DeviceType.Unknown
+
+ if device_type.endswith("HUB"):
return DeviceType.Hub
+
+ if "DOORBELL" in device_type:
+ return DeviceType.Doorbell
+
return DeviceType.Camera
@staticmethod
def _get_device_info(
info: dict[str, Any], discovery_info: dict[str, Any] | None
- ) -> _DeviceInfo:
+ ) -> DeviceInfo:
"""Get model information for a device."""
basic_info = info["getDeviceInfo"]["device_info"]["basic_info"]
short_name = basic_info["device_model"]
@@ -44,7 +48,7 @@ class SmartCamDevice(SmartDevice):
device_type = SmartCamDevice._get_device_type_from_sysinfo(basic_info)
fw_version_full = basic_info["sw_version"]
firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
- return _DeviceInfo(
+ return DeviceInfo(
short_name=basic_info["device_model"],
long_name=long_name,
brand="tapo",
@@ -62,16 +66,38 @@ class SmartCamDevice(SmartDevice):
info = self._try_get_response(info_resp, "getDeviceInfo")
self._info = self._map_info(info["device_info"])
- def _update_children_info(self) -> None:
- """Update the internal child device info from the parent info."""
+ def _update_internal_state(self, info: dict[str, Any]) -> None:
+ """Update the internal info state.
+
+ This is used by the parent to push updates to its children.
+ """
+ self._info = self._map_info(info)
+
+ async def _update_children_info(self) -> bool:
+ """Update the internal child device info from the parent info.
+
+ Return true if children added or deleted.
+ """
+ changed = False
if child_info := self._try_get_response(
self._last_update, "getChildDeviceList", {}
):
+ changed = await self._create_delete_children(
+ child_info, self._last_update["getChildDeviceComponentList"]
+ )
+
for info in child_info["child_device_list"]:
- self._children[info["device_id"]]._update_internal_state(info)
+ child_id = info.get("device_id")
+ if child_id not in self._children:
+ # _create_delete_children has already logged a message
+ continue
+
+ self._children[child_id]._update_internal_state(info)
+
+ return changed
async def _initialize_smart_child(
- self, info: dict, child_components: dict
+ self, info: dict, child_components_raw: ComponentsRaw
) -> SmartDevice:
"""Initialize a smart child device attached to a smartcam device."""
child_id = info["device_id"]
@@ -86,11 +112,30 @@ class SmartCamDevice(SmartDevice):
return await SmartChildDevice.create(
parent=self,
child_info=info,
- child_components=child_components,
+ child_components_raw=child_components_raw,
protocol=child_protocol,
last_update=initial_response,
)
+ async def _initialize_smartcam_child(
+ self, info: dict, child_components_raw: ComponentsRaw
+ ) -> SmartDevice:
+ """Initialize a smart child device attached to a smartcam device."""
+ child_id = info["device_id"]
+ child_protocol = _ChildCameraProtocolWrapper(child_id, self.protocol)
+
+ app_component_list = {
+ "app_component_list": child_components_raw["component_list"]
+ }
+ from .smartcamchild import SmartCamChild
+
+ return await SmartCamChild.create(
+ parent=self,
+ child_info=info,
+ child_components_raw=app_component_list,
+ protocol=child_protocol,
+ )
+
async def _initialize_children(self) -> None:
"""Initialize children for hubs."""
child_info_query = {
@@ -100,34 +145,22 @@ class SmartCamDevice(SmartDevice):
resp = await self.protocol.query(child_info_query)
self.internal_state.update(resp)
- smart_children_components = {
- child["device_id"]: {
- comp["id"]: int(comp["ver_code"]) for comp in component_list
- }
- for child in resp["getChildDeviceComponentList"]["child_component_list"]
- if (component_list := child.get("component_list"))
- # Child camera devices will have a different component schema so only
- # extract smart values.
- and (first_comp := next(iter(component_list), None))
- and isinstance(first_comp, dict)
- and "id" in first_comp
- and "ver_code" in first_comp
- }
- children = {}
- for info in resp["getChildDeviceList"]["child_device_list"]:
- if (
- (category := info.get("category"))
- and category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP
- and (child_id := info.get("device_id"))
- and (child_components := smart_children_components.get(child_id))
- ):
- children[child_id] = await self._initialize_smart_child(
- info, child_components
- )
- else:
- _LOGGER.debug("Child device type not supported: %s", info)
+ async def _try_create_child(
+ self, info: dict, child_components: dict
+ ) -> SmartDevice | None:
+ if not (category := info.get("category")):
+ return None
- self._children = children
+ # Smart
+ if category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP:
+ return await self._initialize_smart_child(info, child_components)
+ # Smartcam
+ from .smartcamchild import SmartCamChild
+
+ if category in SmartCamChild.CHILD_DEVICE_TYPE_MAP:
+ return await self._initialize_smartcam_child(info, child_components)
+
+ return None
async def _initialize_modules(self) -> None:
"""Initialize modules based on component negotiation response."""
@@ -148,9 +181,6 @@ class SmartCamDevice(SmartDevice):
for feat in module._module_features.values():
self._add_feature(feat)
- for child in self._children.values():
- await child._initialize_features()
-
async def _query_setter_helper(
self, method: str, module: str, section: str, params: dict | None = None
) -> dict:
@@ -158,12 +188,12 @@ class SmartCamDevice(SmartDevice):
return res
- async def _query_getter_helper(
- self, method: str, module: str, sections: str | list[str]
- ) -> Any:
- res = await self.protocol.query({method: {module: {"name": sections}}})
-
- return res
+ @staticmethod
+ def _parse_components(components_raw: ComponentsRaw) -> dict[str, int]:
+ return {
+ str(comp["name"]): int(comp["version"])
+ for comp in components_raw["app_component_list"]
+ }
async def _negotiate(self) -> None:
"""Perform initialization.
@@ -174,33 +204,32 @@ class SmartCamDevice(SmartDevice):
initial_query = {
"getDeviceInfo": {"device_info": {"name": ["basic_info", "info"]}},
"getAppComponentList": {"app_component": {"name": "app_component_list"}},
+ "getConnectionType": {"network": {"get_connection_type": {}}},
}
resp = await self.protocol.query(initial_query)
self._last_update.update(resp)
self._update_internal_info(resp)
- self._components = {
- comp["name"]: int(comp["version"])
- for comp in resp["getAppComponentList"]["app_component"][
- "app_component_list"
- ]
- }
+ self._components_raw = cast(
+ ComponentsRaw, resp["getAppComponentList"]["app_component"]
+ )
+ self._components = self._parse_components(self._components_raw)
if "childControl" in self._components and not self.children:
await self._initialize_children()
def _map_info(self, device_info: dict) -> dict:
+ """Map the basic keys to the keys used by SmartDevices."""
basic_info = device_info["basic_info"]
- return {
- "model": basic_info["device_model"],
- "device_type": basic_info["device_type"],
- "alias": basic_info["device_alias"],
- "fw_ver": basic_info["sw_version"],
- "hw_ver": basic_info["hw_version"],
- "mac": basic_info["mac"],
- "hwId": basic_info.get("hw_id"),
- "oem_id": basic_info["oem_id"],
+ mappings = {
+ "device_model": "model",
+ "device_alias": "alias",
+ "sw_version": "fw_ver",
+ "hw_version": "hw_ver",
+ "hw_id": "hwId",
+ "dev_id": "device_id",
}
+ return {mappings.get(k, k): v for k, v in basic_info.items()}
@property
def is_on(self) -> bool:
@@ -220,7 +249,7 @@ class SmartCamDevice(SmartDevice):
@property
def device_type(self) -> DeviceType:
"""Return the device type."""
- if self._device_type == DeviceType.Unknown:
+ if self._device_type == DeviceType.Unknown and self._info:
self._device_type = self._get_device_type_from_sysinfo(self._info)
return self._device_type
@@ -243,11 +272,16 @@ class SmartCamDevice(SmartDevice):
def hw_info(self) -> dict:
"""Return hardware info for the device."""
return {
- "sw_ver": self._info.get("hw_ver"),
- "hw_ver": self._info.get("fw_ver"),
+ "sw_ver": self._info.get("fw_ver"),
+ "hw_ver": self._info.get("hw_ver"),
"mac": self._info.get("mac"),
"type": self._info.get("type"),
"hwId": self._info.get("hwId"),
"dev_name": self.alias,
"oemId": self._info.get("oem_id"),
}
+
+ @property
+ def rssi(self) -> int | None:
+ """Return the device id."""
+ return self.modules[SmartCamModule.SmartCamDeviceModule].rssi
diff --git a/kasa/smartcam/smartcammodule.py b/kasa/smartcam/smartcammodule.py
index ca1a3b82..400b1674 100644
--- a/kasa/smartcam/smartcammodule.py
+++ b/kasa/smartcam/smartcammodule.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING, Any, Final, cast
+from typing import TYPE_CHECKING, Final
from ..exceptions import DeviceError, KasaException, SmartErrorCode
from ..modulemapping import ModuleName
@@ -20,9 +20,28 @@ class SmartCamModule(SmartModule):
"""Base class for SMARTCAM modules."""
SmartCamAlarm: Final[ModuleName[modules.Alarm]] = ModuleName("SmartCamAlarm")
+ SmartCamMotionDetection: Final[ModuleName[modules.MotionDetection]] = ModuleName(
+ "MotionDetection"
+ )
+ SmartCamPersonDetection: Final[ModuleName[modules.PersonDetection]] = ModuleName(
+ "PersonDetection"
+ )
+ SmartCamPetDetection: Final[ModuleName[modules.PetDetection]] = ModuleName(
+ "PetDetection"
+ )
+ SmartCamTamperDetection: Final[ModuleName[modules.TamperDetection]] = ModuleName(
+ "TamperDetection"
+ )
+ SmartCamBabyCryDetection: Final[ModuleName[modules.BabyCryDetection]] = ModuleName(
+ "BabyCryDetection"
+ )
+
+ SmartCamBattery: Final[ModuleName[modules.Battery]] = ModuleName("Battery")
+
+ SmartCamDeviceModule: Final[ModuleName[modules.DeviceModule]] = ModuleName(
+ "devicemodule"
+ )
- #: Query to execute during the main update cycle
- QUERY_GETTER_NAME: str
#: Module name to be queried
QUERY_MODULE_NAME: str
#: Section name or names to be queried
@@ -37,6 +56,8 @@ class SmartCamModule(SmartModule):
Default implementation uses the raw query getter w/o parameters.
"""
+ if not self.QUERY_GETTER_NAME:
+ return {}
section_names = (
{"name": self.QUERY_SECTION_NAMES} if self.QUERY_SECTION_NAMES else {}
)
@@ -47,21 +68,7 @@ class SmartCamModule(SmartModule):
Just a helper method.
"""
- if params:
- module = next(iter(params))
- section = next(iter(params[module]))
- else:
- module = "system"
- section = "null"
-
- if method[:3] == "get":
- return await self._device._query_getter_helper(method, module, section)
-
- if TYPE_CHECKING:
- params = cast(dict[str, dict[str, Any]], params)
- return await self._device._query_setter_helper(
- method, module, section, params[module][section]
- )
+ return await self._device._query_helper(method, params)
@property
def data(self) -> dict:
@@ -86,7 +93,8 @@ class SmartCamModule(SmartModule):
f" for '{self._module}'"
)
- return query_resp.get(self.QUERY_MODULE_NAME)
+ # Some calls return the data under the module, others not
+ return query_resp.get(self.QUERY_MODULE_NAME, query_resp)
else:
found = {key: val for key, val in dev._last_update.items() if key in q}
for key in q:
diff --git a/kasa/transports/__init__.py b/kasa/transports/__init__.py
index 3438aab7..192b4156 100644
--- a/kasa/transports/__init__.py
+++ b/kasa/transports/__init__.py
@@ -3,6 +3,8 @@
from .aestransport import AesEncyptionSession, AesTransport
from .basetransport import BaseTransport
from .klaptransport import KlapTransport, KlapTransportV2
+from .linkietransport import LinkieTransportV2
+from .sslaestransport import SslAesTransport
from .ssltransport import SslTransport
from .xortransport import XorEncryption, XorTransport
@@ -10,9 +12,11 @@ __all__ = [
"AesTransport",
"AesEncyptionSession",
"SslTransport",
+ "SslAesTransport",
"BaseTransport",
"KlapTransport",
"KlapTransportV2",
+ "LinkieTransportV2",
"XorTransport",
"XorEncryption",
]
diff --git a/kasa/transports/aestransport.py b/kasa/transports/aestransport.py
index 3466ca98..45b963fe 100644
--- a/kasa/transports/aestransport.py
+++ b/kasa/transports/aestransport.py
@@ -120,6 +120,8 @@ class AesTransport(BaseTransport):
@property
def default_port(self) -> int:
"""Default port for the transport."""
+ if port := self._config.connection_type.http_port:
+ return port
return self.DEFAULT_PORT
@property
diff --git a/kasa/transports/klaptransport.py b/kasa/transports/klaptransport.py
index 8934b2cc..8253e0ae 100644
--- a/kasa/transports/klaptransport.py
+++ b/kasa/transports/klaptransport.py
@@ -48,6 +48,7 @@ import datetime
import hashlib
import logging
import secrets
+import ssl
import struct
import time
from asyncio import Future
@@ -92,8 +93,21 @@ class KlapTransport(BaseTransport):
"""
DEFAULT_PORT: int = 80
+ DEFAULT_HTTPS_PORT: int = 4433
+
SESSION_COOKIE_NAME = "TP_SESSIONID"
TIMEOUT_COOKIE_NAME = "TIMEOUT"
+ # Copy & paste from sslaestransport
+ CIPHERS = ":".join(
+ [
+ "AES256-GCM-SHA384",
+ "AES256-SHA256",
+ "AES128-GCM-SHA256",
+ "AES128-SHA256",
+ "AES256-SHA",
+ ]
+ )
+ _ssl_context: ssl.SSLContext | None = None
def __init__(
self,
@@ -125,12 +139,20 @@ class KlapTransport(BaseTransport):
self._session_cookie: dict[str, Any] | None = None
_LOGGER.debug("Created KLAP transport for %s", self._host)
- self._app_url = URL(f"http://{self._host}:{self._port}/app")
+ protocol = "https" if config.connection_type.https else "http"
+ self._app_url = URL(f"{protocol}://{self._host}:{self._port}/app")
self._request_url = self._app_url / "request"
@property
def default_port(self) -> int:
"""Default port for the transport."""
+ config = self._config
+ if port := config.connection_type.http_port:
+ return port
+
+ if config.connection_type.https:
+ return self.DEFAULT_HTTPS_PORT
+
return self.DEFAULT_PORT
@property
@@ -152,7 +174,9 @@ class KlapTransport(BaseTransport):
url = self._app_url / "handshake1"
- response_status, response_data = await self._http_client.post(url, data=payload)
+ response_status, response_data = await self._http_client.post(
+ url, data=payload, ssl=await self._get_ssl_context()
+ )
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
@@ -214,8 +238,8 @@ class KlapTransport(BaseTransport):
if default_credentials_seed_auth_hash == server_hash:
_LOGGER.debug(
- "Server response doesn't match our expected hash on ip %s, "
- "but an authentication with %s default credentials matched",
+ "Device response did not match our expected hash on ip %s,"
+ "but an authentication with %s default credentials worked",
self._host,
key,
)
@@ -235,13 +259,16 @@ class KlapTransport(BaseTransport):
if blank_seed_auth_hash == server_hash:
_LOGGER.debug(
- "Server response doesn't match our expected hash on ip %s, "
- "but an authentication with blank credentials matched",
+ "Device response did not match our expected hash on ip %s, "
+ "but an authentication with blank credentials worked",
self._host,
)
return local_seed, remote_seed, self._blank_auth_hash # type: ignore
- msg = f"Server response doesn't match our challenge on ip {self._host}"
+ msg = (
+ f"Device response did not match our challenge on ip {self._host}, "
+ f"check that your e-mail and password (both case-sensitive) are correct. "
+ )
_LOGGER.debug(msg)
raise AuthenticationError(msg)
@@ -260,6 +287,7 @@ class KlapTransport(BaseTransport):
url,
data=payload,
cookies_dict=self._session_cookie,
+ ssl=await self._get_ssl_context(),
)
if _LOGGER.isEnabledFor(logging.DEBUG):
@@ -334,6 +362,7 @@ class KlapTransport(BaseTransport):
params={"seq": seq},
data=payload,
cookies_dict=self._session_cookie,
+ ssl=await self._get_ssl_context(),
)
msg = (
@@ -410,6 +439,23 @@ class KlapTransport(BaseTransport):
un = creds.username
return md5(un.encode())
+ # Copy & paste from sslaestransport.
+ def _create_ssl_context(self) -> ssl.SSLContext:
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.set_ciphers(self.CIPHERS)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ return context
+
+ # Copy & paste from sslaestransport.
+ async def _get_ssl_context(self) -> ssl.SSLContext:
+ if not self._ssl_context:
+ loop = asyncio.get_running_loop()
+ self._ssl_context = await loop.run_in_executor(
+ None, self._create_ssl_context
+ )
+ return self._ssl_context
+
class KlapTransportV2(KlapTransport):
"""Implementation of the KLAP encryption protocol with v2 hanshake hashes."""
diff --git a/kasa/transports/linkietransport.py b/kasa/transports/linkietransport.py
new file mode 100644
index 00000000..b817373c
--- /dev/null
+++ b/kasa/transports/linkietransport.py
@@ -0,0 +1,145 @@
+"""Implementation of the linkie kasa camera transport."""
+
+from __future__ import annotations
+
+import asyncio
+import base64
+import logging
+import ssl
+from typing import TYPE_CHECKING, cast
+from urllib.parse import quote
+
+from yarl import URL
+
+from kasa.credentials import DEFAULT_CREDENTIALS, get_default_credentials
+from kasa.deviceconfig import DeviceConfig
+from kasa.exceptions import KasaException, _RetryableError
+from kasa.httpclient import HttpClient
+from kasa.json import loads as json_loads
+from kasa.transports.xortransport import XorEncryption
+
+from .basetransport import BaseTransport
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class LinkieTransportV2(BaseTransport):
+ """Implementation of the Linkie encryption protocol.
+
+ Linkie is used as the endpoint for TP-Link's camera encryption
+ protocol, used by newer firmware versions.
+ """
+
+ DEFAULT_PORT: int = 10443
+ CIPHERS = ":".join(
+ [
+ "AES256-GCM-SHA384",
+ "AES256-SHA256",
+ "AES128-GCM-SHA256",
+ "AES128-SHA256",
+ "AES256-SHA",
+ ]
+ )
+
+ def __init__(self, *, config: DeviceConfig) -> None:
+ super().__init__(config=config)
+ self._http_client = HttpClient(config)
+ self._ssl_context: ssl.SSLContext | None = None
+ self._app_url = URL(f"https://{self._host}:{self._port}/data/LINKIE2.json")
+
+ self._headers = {
+ "Authorization": f"Basic {self.credentials_hash}",
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+
+ @property
+ def default_port(self) -> int:
+ """Default port for the transport."""
+ if port := self._config.connection_type.http_port:
+ return port
+ return self.DEFAULT_PORT
+
+ @property
+ def credentials_hash(self) -> str | None:
+ """The hashed credentials used by the transport."""
+ creds = get_default_credentials(DEFAULT_CREDENTIALS["KASACAMERA"])
+ creds_combined = f"{creds.username}:{creds.password}"
+ return base64.b64encode(creds_combined.encode()).decode()
+
+ async def _execute_send(self, request: str) -> dict:
+ """Execute a query on the device and wait for the response."""
+ _LOGGER.debug("%s >> %s", self._host, request)
+
+ encrypted_cmd = XorEncryption.encrypt(request)[4:]
+ b64_cmd = base64.b64encode(encrypted_cmd).decode()
+ url_safe_cmd = quote(b64_cmd, safe="!~*'()")
+
+ status_code, response = await self._http_client.post(
+ self._app_url,
+ headers=self._headers,
+ data=f"content={url_safe_cmd}".encode(),
+ ssl=await self._get_ssl_context(),
+ )
+
+ if TYPE_CHECKING:
+ response = cast(bytes, response)
+
+ if status_code != 200:
+ raise KasaException(
+ f"{self._host} responded with an unexpected "
+ + f"status code {status_code} to passthrough"
+ )
+
+ # Expected response
+ try:
+ json_payload: dict = json_loads(
+ XorEncryption.decrypt(base64.b64decode(response))
+ )
+ _LOGGER.debug("%s << %s", self._host, json_payload)
+ return json_payload
+ except Exception: # noqa: S110
+ pass
+
+ # Device returned error as json plaintext
+ to_raise: KasaException | None = None
+ try:
+ error_payload: dict = json_loads(response)
+ to_raise = KasaException(f"Device {self._host} send error: {error_payload}")
+ except Exception as ex:
+ raise KasaException("Unable to read response") from ex
+ raise to_raise
+
+ async def close(self) -> None:
+ """Close the http client and reset internal state."""
+ await self._http_client.close()
+
+ async def reset(self) -> None:
+ """Reset the transport.
+
+ NOOP for this transport.
+ """
+
+ async def send(self, request: str) -> dict:
+ """Send a message to the device and return a response."""
+ try:
+ return await self._execute_send(request)
+ except Exception as ex:
+ await self.reset()
+ raise _RetryableError(
+ f"Unable to query the device {self._host}:{self._port}: {ex}"
+ ) from ex
+
+ async def _get_ssl_context(self) -> ssl.SSLContext:
+ if not self._ssl_context:
+ loop = asyncio.get_running_loop()
+ self._ssl_context = await loop.run_in_executor(
+ None, self._create_ssl_context
+ )
+ return self._ssl_context
+
+ def _create_ssl_context(self) -> ssl.SSLContext:
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.set_ciphers(self.CIPHERS)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ return context
diff --git a/kasa/transports/sslaestransport.py b/kasa/transports/sslaestransport.py
index 2061d293..eeb29809 100644
--- a/kasa/transports/sslaestransport.py
+++ b/kasa/transports/sslaestransport.py
@@ -8,6 +8,7 @@ import hashlib
import logging
import secrets
import ssl
+from contextlib import suppress
from enum import Enum, auto
from typing import TYPE_CHECKING, Any, cast
@@ -125,12 +126,15 @@ class SslAesTransport(BaseTransport):
self._password = ch["pwd"]
self._username = ch["un"]
self._local_nonce: str | None = None
+ self._send_secure = True
_LOGGER.debug("Created AES transport for %s", self._host)
@property
def default_port(self) -> int:
"""Default port for the transport."""
+ if port := self._config.connection_type.http_port:
+ return port
return self.DEFAULT_PORT
@staticmethod
@@ -160,6 +164,25 @@ class SslAesTransport(BaseTransport):
error_code = SmartErrorCode.INTERNAL_UNKNOWN_ERROR
return error_code
+ def _get_response_inner_error(self, resp_dict: Any) -> SmartErrorCode | None:
+ # Device blocked errors have 'data' element at the root level, other inner
+ # errors are inside 'result'
+ error_code_raw = resp_dict.get("data", {}).get("code")
+
+ if error_code_raw is None:
+ error_code_raw = resp_dict.get("result", {}).get("data", {}).get("code")
+
+ if error_code_raw is None:
+ return None
+ try:
+ error_code = SmartErrorCode.from_int(error_code_raw)
+ except ValueError:
+ _LOGGER.warning(
+ "Device %s received unknown error code: %s", self._host, error_code_raw
+ )
+ error_code = SmartErrorCode.INTERNAL_UNKNOWN_ERROR
+ return error_code
+
def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None:
error_code = self._get_response_error(resp_dict)
if error_code is SmartErrorCode.SUCCESS:
@@ -194,6 +217,10 @@ class SslAesTransport(BaseTransport):
else:
url = self._app_url
+ _LOGGER.debug(
+ "Sending secure passthrough from %s",
+ self._host,
+ )
encrypted_payload = self._encryption_session.encrypt(request.encode()) # type: ignore
passthrough_request = {
"method": "securePassthrough",
@@ -216,6 +243,31 @@ class SslAesTransport(BaseTransport):
ssl=await self._get_ssl_context(),
)
+ if TYPE_CHECKING:
+ assert self._encryption_session is not None
+
+ # Devices can respond with 500 if another session is created from
+ # the same host. Decryption may not succeed after that
+ if status_code == 500:
+ msg = (
+ f"Device {self._host} replied with status 500 after handshake, "
+ f"response: "
+ )
+ decrypted = None
+ if isinstance(resp_dict, dict) and (
+ response := resp_dict.get("result", {}).get("response")
+ ):
+ with suppress(Exception):
+ decrypted = self._encryption_session.decrypt(response.encode())
+
+ if decrypted:
+ msg += decrypted
+ else:
+ msg += str(resp_dict)
+
+ _LOGGER.debug(msg)
+ raise _RetryableError(msg)
+
if status_code != 200:
raise KasaException(
f"{self._host} responded with an unexpected "
@@ -228,7 +280,6 @@ class SslAesTransport(BaseTransport):
if TYPE_CHECKING:
resp_dict = cast(dict[str, Any], resp_dict)
- assert self._encryption_session is not None
if "result" in resp_dict and "response" in resp_dict["result"]:
raw_response: str = resp_dict["result"]["response"]
@@ -254,6 +305,34 @@ class SslAesTransport(BaseTransport):
) from ex
return ret_val # type: ignore[return-value]
+ async def send_unencrypted(self, request: str) -> dict[str, Any]:
+ """Send encrypted message as passthrough."""
+ url = cast(URL, self._token_url)
+
+ _LOGGER.debug(
+ "Sending unencrypted to %s",
+ self._host,
+ )
+
+ status_code, resp_dict = await self._http_client.post(
+ url,
+ json=request,
+ headers=self._headers,
+ ssl=await self._get_ssl_context(),
+ )
+
+ if status_code != 200:
+ raise KasaException(
+ f"{self._host} responded with an unexpected "
+ + f"status code {status_code} to unencrypted send"
+ )
+
+ self._handle_response_error_code(resp_dict, "Error sending message")
+
+ if TYPE_CHECKING:
+ resp_dict = cast(dict[str, Any], resp_dict)
+ return resp_dict
+
@staticmethod
def generate_confirm_hash(
local_nonce: str, server_nonce: str, pwd_hash: str
@@ -302,8 +381,50 @@ class SslAesTransport(BaseTransport):
async def perform_handshake(self) -> None:
"""Perform the handshake."""
- local_nonce, server_nonce, pwd_hash = await self.perform_handshake1()
- await self.perform_handshake2(local_nonce, server_nonce, pwd_hash)
+ result = await self.perform_handshake1()
+ if result:
+ local_nonce, server_nonce, pwd_hash = result
+ await self.perform_handshake2(local_nonce, server_nonce, pwd_hash)
+
+ async def try_perform_less_secure_login(self, username: str, password: str) -> bool:
+ """Perform the md5 login."""
+ _LOGGER.debug("Performing less secure login...")
+
+ pwd_hash = _md5_hash(password.encode())
+ body = {
+ "method": "login",
+ "params": {
+ "hashed": True,
+ "password": pwd_hash,
+ "username": username,
+ },
+ }
+
+ status_code, resp_dict = await self._http_client.post(
+ self._app_url,
+ json=body,
+ headers=self._headers,
+ ssl=await self._get_ssl_context(),
+ )
+ if status_code != 200:
+ raise KasaException(
+ f"{self._host} responded with an unexpected "
+ + f"status code {status_code} to login"
+ )
+ resp_dict = cast(dict, resp_dict)
+ if resp_dict.get("error_code") == 0 and (
+ stok := resp_dict.get("result", {}).get("stok")
+ ):
+ _LOGGER.debug(
+ "Succesfully logged in to %s with less secure passthrough", self._host
+ )
+ self._send_secure = False
+ self._token_url = URL(f"{str(self._app_url)}/stok={stok}/ds")
+ self._pwd_hash = pwd_hash
+ return True
+
+ _LOGGER.debug("Unable to log in to %s with less secure login", self._host)
+ return False
async def perform_handshake2(
self, local_nonce: str, server_nonce: str, pwd_hash: str
@@ -355,13 +476,50 @@ class SslAesTransport(BaseTransport):
self._state = TransportState.ESTABLISHED
_LOGGER.debug("Handshake2 complete ...")
- async def perform_handshake1(self) -> tuple[str, str, str]:
+ def _pwd_to_hash(self) -> str:
+ """Return the password to hash."""
+ if self._credentials and self._credentials != Credentials():
+ return self._credentials.password
+
+ if self._username and self._password:
+ return self._password
+
+ return self._default_credentials.password
+
+ def _is_less_secure_login(self, resp_dict: dict[str, Any]) -> bool:
+ result = (
+ self._get_response_error(resp_dict) is SmartErrorCode.SESSION_EXPIRED
+ and (data := resp_dict.get("result", {}).get("data", {}))
+ and (encrypt_type := data.get("encrypt_type"))
+ and (encrypt_type != ["3"])
+ )
+ if result:
+ _LOGGER.debug(
+ "Received encrypt_type %s for %s, trying less secure login",
+ encrypt_type,
+ self._host,
+ )
+ return result
+
+ async def perform_handshake1(self) -> tuple[str, str, str] | None:
"""Perform the handshake1."""
resp_dict = None
if self._username:
local_nonce = secrets.token_bytes(8).hex().upper()
resp_dict = await self.try_send_handshake1(self._username, local_nonce)
+ if (
+ resp_dict
+ and self._is_less_secure_login(resp_dict)
+ and self._get_response_inner_error(resp_dict)
+ is not SmartErrorCode.BAD_USERNAME
+ and await self.try_perform_less_secure_login(
+ cast(str, self._username), self._pwd_to_hash()
+ )
+ ):
+ self._state = TransportState.ESTABLISHED
+ return None
+
# Try the default username. If it fails raise the original error_code
if (
not resp_dict
@@ -369,27 +527,54 @@ class SslAesTransport(BaseTransport):
is not SmartErrorCode.INVALID_NONCE
or "nonce" not in resp_dict["result"].get("data", {})
):
+ _LOGGER.debug("Trying default credentials to %s", self._host)
local_nonce = secrets.token_bytes(8).hex().upper()
default_resp_dict = await self.try_send_handshake1(
self._default_credentials.username, local_nonce
)
+ # INVALID_NONCE means device should perform secure login
if (
default_error_code := self._get_response_error(default_resp_dict)
) is SmartErrorCode.INVALID_NONCE and "nonce" in default_resp_dict[
"result"
].get("data", {}):
- _LOGGER.debug("Connected to {self._host} with default username")
+ _LOGGER.debug("Connected to %s with default username", self._host)
self._username = self._default_credentials.username
error_code = default_error_code
resp_dict = default_resp_dict
+ # Otherwise could be less secure login
+ elif self._is_less_secure_login(
+ default_resp_dict
+ ) and await self.try_perform_less_secure_login(
+ self._default_credentials.username, self._pwd_to_hash()
+ ):
+ self._username = self._default_credentials.username
+ self._state = TransportState.ESTABLISHED
+ return None
+ # If the default login worked it's ok not to provide credentials but if
+ # it didn't raise auth error here.
if not self._username:
raise AuthenticationError(
f"Credentials must be supplied to connect to {self._host}"
)
+
+ # Device responds with INVALID_NONCE and a "nonce" to indicate ready
+ # for secure login. Otherwise error.
if error_code is not SmartErrorCode.INVALID_NONCE or (
- resp_dict and "nonce" not in resp_dict["result"].get("data", {})
+ resp_dict and "nonce" not in resp_dict.get("result", {}).get("data", {})
):
+ if (
+ resp_dict
+ and self._get_response_inner_error(resp_dict)
+ is SmartErrorCode.DEVICE_BLOCKED
+ ):
+ sec_left = resp_dict.get("data", {}).get("sec_left")
+ msg = "Device blocked" + (
+ f" for {sec_left} seconds" if sec_left else ""
+ )
+ raise DeviceError(msg, error_code=SmartErrorCode.DEVICE_BLOCKED)
+
raise AuthenticationError(f"Error trying handshake1: {resp_dict}")
if TYPE_CHECKING:
@@ -397,12 +582,8 @@ class SslAesTransport(BaseTransport):
server_nonce = resp_dict["result"]["data"]["nonce"]
device_confirm = resp_dict["result"]["data"]["device_confirm"]
- if self._credentials and self._credentials != Credentials():
- pwd_hash = _sha256_hash(self._credentials.password.encode())
- elif self._username and self._password:
- pwd_hash = _sha256_hash(self._password.encode())
- else:
- pwd_hash = _sha256_hash(self._default_credentials.password.encode())
+
+ pwd_hash = _sha256_hash(self._pwd_to_hash().encode())
expected_confirm_sha256 = self.generate_confirm_hash(
local_nonce, server_nonce, pwd_hash
@@ -414,7 +595,9 @@ class SslAesTransport(BaseTransport):
if TYPE_CHECKING:
assert self._credentials
assert self._credentials.password
- pwd_hash = _md5_hash(self._credentials.password.encode())
+
+ pwd_hash = _md5_hash(self._pwd_to_hash().encode())
+
expected_confirm_md5 = self.generate_confirm_hash(
local_nonce, server_nonce, pwd_hash
)
@@ -422,13 +605,17 @@ class SslAesTransport(BaseTransport):
_LOGGER.debug("Credentials match")
return local_nonce, server_nonce, pwd_hash
- msg = f"Server response doesn't match our challenge on ip {self._host}"
+ msg = (
+ f"Device response did not match our challenge on ip {self._host}, "
+ f"check that your e-mail and password (both case-sensitive) are correct. "
+ )
_LOGGER.debug(msg)
+
raise AuthenticationError(msg)
async def try_send_handshake1(self, username: str, local_nonce: str) -> dict:
"""Perform the handshake."""
- _LOGGER.debug("Will to send handshake1...")
+ _LOGGER.debug("Sending handshake1...")
body = {
"method": "login",
@@ -447,7 +634,7 @@ class SslAesTransport(BaseTransport):
ssl=await self._get_ssl_context(),
)
- _LOGGER.debug("Device responded with: %s", resp_dict)
+ _LOGGER.debug("Device responded with status %s: %s", status_code, resp_dict)
if status_code != 200:
raise KasaException(
@@ -462,7 +649,10 @@ class SslAesTransport(BaseTransport):
if self._state is TransportState.HANDSHAKE_REQUIRED:
await self.perform_handshake()
- return await self.send_secure_passthrough(request)
+ if self._send_secure:
+ return await self.send_secure_passthrough(request)
+
+ return await self.send_unencrypted(request)
async def close(self) -> None:
"""Close the http client and reset internal state."""
diff --git a/kasa/transports/ssltransport.py b/kasa/transports/ssltransport.py
index 5ffc935f..e4fef9a3 100644
--- a/kasa/transports/ssltransport.py
+++ b/kasa/transports/ssltransport.py
@@ -94,6 +94,8 @@ class SslTransport(BaseTransport):
@property
def default_port(self) -> int:
"""Default port for the transport."""
+ if port := self._config.connection_type.http_port:
+ return port
return self.DEFAULT_PORT
@property
@@ -215,7 +217,7 @@ class SslTransport(BaseTransport):
async def send(self, request: str) -> dict[str, Any]:
"""Send the request."""
- _LOGGER.info("Going to send %s", request)
+ _LOGGER.debug("Going to send %s", request)
if self._state is not TransportState.ESTABLISHED or self._session_expired():
_LOGGER.debug("Transport not established or session expired, logging in")
await self.perform_login()
diff --git a/kasa/transports/xortransport.py b/kasa/transports/xortransport.py
index 77a232f0..8cce6eb5 100644
--- a/kasa/transports/xortransport.py
+++ b/kasa/transports/xortransport.py
@@ -23,6 +23,7 @@ from collections.abc import Generator
from kasa.deviceconfig import DeviceConfig
from kasa.exceptions import KasaException, _RetryableError
+from kasa.exceptions import TimeoutError as KasaTimeoutError
from kasa.json import loads as json_loads
from .basetransport import BaseTransport
@@ -126,6 +127,12 @@ class XorTransport(BaseTransport):
# This is especially import when there are multiple tplink devices being polled.
try:
await self._connect(self._timeout)
+ except TimeoutError as ex:
+ await self.reset()
+ raise KasaTimeoutError(
+ f"Timeout after {self._timeout} seconds connecting to the device:"
+ f" {self._host}:{self._port}: {ex}"
+ ) from ex
except ConnectionRefusedError as ex:
await self.reset()
raise KasaException(
@@ -159,6 +166,12 @@ class XorTransport(BaseTransport):
assert self.writer is not None # noqa: S101
async with asyncio_timeout(self._timeout):
return await self._execute_send(request)
+ except TimeoutError as ex:
+ await self.reset()
+ raise KasaTimeoutError(
+ f"Timeout after {self._timeout} seconds sending request to the device"
+ f" {self._host}:{self._port}: {ex}"
+ ) from ex
except Exception as ex:
await self.reset()
raise _RetryableError(
diff --git a/pyproject.toml b/pyproject.toml
index 506888cd..eed43e2b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "python-kasa"
-version = "0.8.0"
+version = "0.9.1"
description = "Python API for TP-Link Kasa and Tapo devices"
license = {text = "GPL-3.0-or-later"}
authors = [ { name = "python-kasa developers" }]
@@ -112,7 +112,7 @@ markers = [
]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
-timeout = 10
+#timeout = 10
# dist=loadgroup enables grouping of tests into single worker.
# required as caplog doesn't play nicely with multiple workers.
addopts = "--disable-socket --allow-unix-socket --dist=loadgroup"
diff --git a/tests/cli/__init__.py b/tests/cli/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/cli/test_hub.py b/tests/cli/test_hub.py
new file mode 100644
index 00000000..5236f4cd
--- /dev/null
+++ b/tests/cli/test_hub.py
@@ -0,0 +1,53 @@
+import pytest
+from pytest_mock import MockerFixture
+
+from kasa import DeviceType, Module
+from kasa.cli.hub import hub
+
+from ..device_fixtures import HUBS_SMART, hubs_smart, parametrize, plug_iot
+
+
+@hubs_smart
+async def test_hub_pair(dev, mocker: MockerFixture, runner, caplog):
+ """Test that pair calls the expected methods."""
+ cs = dev.modules.get(Module.ChildSetup)
+ # Patch if the device supports the module
+ if cs is not None:
+ mock_pair = mocker.patch.object(cs, "pair")
+
+ res = await runner.invoke(hub, ["pair"], obj=dev, catch_exceptions=False)
+ if cs is None:
+ assert "is not a hub" in res.output
+ return
+
+ mock_pair.assert_awaited()
+ assert "Finding new devices for 10 seconds" in res.output
+ assert res.exit_code == 0
+
+
+@parametrize("hubs smart", model_filter=HUBS_SMART, protocol_filter={"SMART"})
+async def test_hub_unpair(dev, mocker: MockerFixture, runner):
+ """Test that unpair calls the expected method."""
+ if not dev.children:
+ pytest.skip("Cannot test without child devices")
+
+ id_ = next(iter(dev.children)).device_id
+
+ cs = dev.modules.get(Module.ChildSetup)
+ mock_unpair = mocker.spy(cs, "unpair")
+
+ res = await runner.invoke(hub, ["unpair", id_], obj=dev, catch_exceptions=False)
+
+ mock_unpair.assert_awaited()
+ assert f"Unpaired {id_}" in res.output
+ assert res.exit_code == 0
+
+
+@plug_iot
+async def test_non_hub(dev, mocker: MockerFixture, runner):
+ """Test that hub commands return an error if executed on a non-hub."""
+ assert dev.device_type is not DeviceType.Hub
+ res = await runner.invoke(
+ hub, ["unpair", "dummy_id"], obj=dev, catch_exceptions=False
+ )
+ assert "is not a hub" in res.output
diff --git a/tests/cli/test_vacuum.py b/tests/cli/test_vacuum.py
new file mode 100644
index 00000000..a790286e
--- /dev/null
+++ b/tests/cli/test_vacuum.py
@@ -0,0 +1,114 @@
+from pytest_mock import MockerFixture
+
+from kasa import DeviceType, Module
+from kasa.cli.vacuum import vacuum
+
+from ..device_fixtures import plug_iot
+from ..device_fixtures import vacuum as vacuum_devices
+
+
+@vacuum_devices
+async def test_vacuum_records_group(dev, mocker: MockerFixture, runner):
+ """Test that vacuum records calls the expected methods."""
+ rec = dev.modules.get(Module.CleanRecords)
+ assert rec
+
+ res = await runner.invoke(vacuum, ["records"], obj=dev, catch_exceptions=False)
+
+ latest = rec.parsed_data.last_clean
+ expected = (
+ f"Totals: {rec.total_clean_area} {rec.area_unit} in {rec.total_clean_time} "
+ f"(cleaned {rec.total_clean_count} times)\n"
+ f"Last clean: {latest.clean_area} {rec.area_unit} @ {latest.clean_time}"
+ )
+ assert expected in res.output
+ assert res.exit_code == 0
+
+
+@vacuum_devices
+async def test_vacuum_records_list(dev, mocker: MockerFixture, runner):
+ """Test that vacuum records list calls the expected methods."""
+ rec = dev.modules.get(Module.CleanRecords)
+ assert rec
+
+ res = await runner.invoke(
+ vacuum, ["records", "list"], obj=dev, catch_exceptions=False
+ )
+
+ data = rec.parsed_data
+ for record in data.records:
+ expected = (
+ f"* {record.timestamp}: cleaned {record.clean_area} {rec.area_unit}"
+ f" in {record.clean_time}"
+ )
+ assert expected in res.output
+ assert res.exit_code == 0
+
+
+@vacuum_devices
+async def test_vacuum_consumables(dev, runner):
+ """Test that vacuum consumables calls the expected methods."""
+ cons = dev.modules.get(Module.Consumables)
+ assert cons
+
+ res = await runner.invoke(vacuum, ["consumables"], obj=dev, catch_exceptions=False)
+
+ expected = ""
+ for c in cons.consumables.values():
+ expected += f"{c.name} ({c.id}): {c.used} used, {c.remaining} remaining\n"
+
+ assert expected in res.output
+ assert res.exit_code == 0
+
+
+@vacuum_devices
+async def test_vacuum_consumables_reset(dev, mocker: MockerFixture, runner):
+ """Test that vacuum consumables reset calls the expected methods."""
+ cons = dev.modules.get(Module.Consumables)
+ assert cons
+
+ reset_consumable_mock = mocker.spy(cons, "reset_consumable")
+ for c_id in cons.consumables:
+ reset_consumable_mock.reset_mock()
+ res = await runner.invoke(
+ vacuum, ["consumables", "reset", c_id], obj=dev, catch_exceptions=False
+ )
+ reset_consumable_mock.assert_awaited_once_with(c_id)
+ assert f"Consumable {c_id} reset" in res.output
+ assert res.exit_code == 0
+
+ res = await runner.invoke(
+ vacuum, ["consumables", "reset", "foobar"], obj=dev, catch_exceptions=False
+ )
+ expected = (
+ "Consumable foobar not found in "
+ f"device consumables: {', '.join(cons.consumables.keys())}."
+ )
+ assert expected in res.output.replace("\n", "")
+ assert res.exit_code != 0
+
+
+@plug_iot
+async def test_non_vacuum(dev, mocker: MockerFixture, runner):
+ """Test that vacuum commands return an error if executed on a non-vacuum."""
+ assert dev.device_type is not DeviceType.Vacuum
+
+ res = await runner.invoke(vacuum, ["records"], obj=dev, catch_exceptions=False)
+ assert "This device does not support records" in res.output
+ assert res.exit_code != 0
+
+ res = await runner.invoke(
+ vacuum, ["records", "list"], obj=dev, catch_exceptions=False
+ )
+ assert "This device does not support records" in res.output
+ assert res.exit_code != 0
+
+ res = await runner.invoke(vacuum, ["consumables"], obj=dev, catch_exceptions=False)
+ assert "This device does not support consumables" in res.output
+ assert res.exit_code != 0
+
+ res = await runner.invoke(
+ vacuum, ["consumables", "reset", "foobar"], obj=dev, catch_exceptions=False
+ )
+ assert "This device does not support consumables" in res.output
+ assert res.exit_code != 0
diff --git a/tests/conftest.py b/tests/conftest.py
index 3da689c5..6162d3af 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
+import os
import sys
import warnings
from pathlib import Path
@@ -8,6 +9,9 @@ from unittest.mock import MagicMock, patch
import pytest
+# TODO: this and runner fixture could be moved to tests/cli/conftest.py
+from asyncclick.testing import CliRunner
+
from kasa import (
DeviceConfig,
SmartProtocol,
@@ -149,3 +153,12 @@ def mock_datagram_endpoint(request): # noqa: PT004
side_effect=_create_datagram_endpoint,
):
yield
+
+
+@pytest.fixture
+def runner():
+ """Runner fixture that unsets the KASA_ environment variables for tests."""
+ KASA_VARS = {k: None for k, v in os.environ.items() if k.startswith("KASA_")}
+ runner = CliRunner(env=KASA_VARS)
+
+ return runner
diff --git a/tests/device_fixtures.py b/tests/device_fixtures.py
index d206b714..f6a2dfe4 100644
--- a/tests/device_fixtures.py
+++ b/tests/device_fixtures.py
@@ -79,8 +79,6 @@ PLUGS_IOT = {
"KP125",
"KP401",
}
-# P135 supports dimming, but its not currently support
-# by the library
PLUGS_SMART = {
"P100",
"P110",
@@ -98,6 +96,7 @@ PLUGS = {
SWITCHES_IOT = {
"HS200",
"HS210",
+ "KS200",
"KS200M",
}
SWITCHES_SMART = {
@@ -111,7 +110,7 @@ SWITCHES_SMART = {
}
SWITCHES = {*SWITCHES_IOT, *SWITCHES_SMART}
STRIPS_IOT = {"HS107", "HS300", "KP303", "KP200", "KP400", "EP40"}
-STRIPS_SMART = {"P300", "P304M", "TP25", "EP40M"}
+STRIPS_SMART = {"P300", "P304M", "TP25", "EP40M", "P210M", "P306"}
STRIPS = {*STRIPS_IOT, *STRIPS_SMART}
DIMMERS_IOT = {"ES20M", "HS220", "KS220", "KS220M", "KS230", "KP405"}
@@ -122,9 +121,22 @@ DIMMERS = {
}
HUBS_SMART = {"H100", "KH100"}
-SENSORS_SMART = {"T310", "T315", "T300", "T100", "T110", "S200B", "S200D"}
+SENSORS_SMART = {
+ "T310",
+ "T315",
+ "T300",
+ "T100",
+ "T110",
+ "S200B",
+ "S200D",
+ "S210",
+ "S220",
+ "D100C", # needs a home category?
+}
THERMOSTATS_SMART = {"KE100"}
+VACUUMS_SMART = {"RV20"}
+
WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
WITH_EMETER_SMART = {"P110", "P110M", "P115", "KP125M", "EP25", "P304M"}
WITH_EMETER = {*WITH_EMETER_IOT, *WITH_EMETER_SMART}
@@ -142,6 +154,7 @@ ALL_DEVICES_SMART = (
.union(SENSORS_SMART)
.union(SWITCHES_SMART)
.union(THERMOSTATS_SMART)
+ .union(VACUUMS_SMART)
)
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
@@ -326,13 +339,24 @@ device_smartcam = parametrize("devices smartcam", protocol_filter={"SMARTCAM"})
camera_smartcam = parametrize(
"camera smartcam",
device_type_filter=[DeviceType.Camera],
- protocol_filter={"SMARTCAM"},
+ protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"},
)
hub_smartcam = parametrize(
"hub smartcam",
device_type_filter=[DeviceType.Hub],
protocol_filter={"SMARTCAM"},
)
+doobell_smartcam = parametrize(
+ "doorbell smartcam",
+ device_type_filter=[DeviceType.Doorbell],
+ protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"},
+)
+chime_smart = parametrize(
+ "chime smart",
+ device_type_filter=[DeviceType.Chime],
+ protocol_filter={"SMART"},
+)
+vacuum = parametrize("vacuums", device_type_filter=[DeviceType.Vacuum])
def check_categories():
@@ -349,8 +373,11 @@ def check_categories():
+ hubs_smart.args[1]
+ sensors_smart.args[1]
+ thermostats_smart.args[1]
+ + chime_smart.args[1]
+ camera_smartcam.args[1]
+ + doobell_smartcam.args[1]
+ hub_smartcam.args[1]
+ + vacuum.args[1]
)
diffs: set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures)
if diffs:
@@ -368,7 +395,7 @@ check_categories()
def device_for_fixture_name(model, protocol):
if protocol in {"SMART", "SMART.CHILD"}:
return SmartDevice
- elif protocol == "SMARTCAM":
+ elif protocol in {"SMARTCAM", "SMARTCAM.CHILD"}:
return SmartCamDevice
else:
for d in STRIPS_IOT:
@@ -421,11 +448,20 @@ async def get_device_for_fixture(
d = device_for_fixture_name(fixture_data.name, fixture_data.protocol)(
host="127.0.0.123"
)
+
+ # smart child devices sometimes check _is_hub_child which needs a parent
+ # of DeviceType.Hub
+ class DummyParent:
+ device_type = DeviceType.Hub
+
+ if fixture_data.protocol in {"SMARTCAM.CHILD"}:
+ d._parent = DummyParent()
+
if fixture_data.protocol in {"SMART", "SMART.CHILD"}:
d.protocol = FakeSmartProtocol(
fixture_data.data, fixture_data.name, verbatim=verbatim
)
- elif fixture_data.protocol == "SMARTCAM":
+ elif fixture_data.protocol in {"SMARTCAM", "SMARTCAM.CHILD"}:
d.protocol = FakeSmartCamProtocol(
fixture_data.data, fixture_data.name, verbatim=verbatim
)
@@ -434,7 +470,7 @@ async def get_device_for_fixture(
discovery_data = None
if "discovery_result" in fixture_data.data:
- discovery_data = fixture_data.data["discovery_result"]
+ discovery_data = fixture_data.data["discovery_result"]["result"]
elif "system" in fixture_data.data:
discovery_data = {
"system": {"get_sysinfo": fixture_data.data["system"]["get_sysinfo"]}
@@ -472,8 +508,12 @@ def get_nearest_fixture_to_ip(dev):
assert protocol_fixtures, "Unknown device type"
# This will get the best fixture with a match on model region
- if model_region_fixtures := filter_fixtures(
- "", model_filter={dev._model_region}, fixture_list=protocol_fixtures
+ if (di := dev.device_info) and (
+ model_region_fixtures := filter_fixtures(
+ "",
+ model_filter={di.long_name + (f"({di.region})" if di.region else "")},
+ fixture_list=protocol_fixtures,
+ )
):
return next(iter(model_region_fixtures))
diff --git a/tests/discovery_fixtures.py b/tests/discovery_fixtures.py
index c65d47bd..3cf726f4 100644
--- a/tests/discovery_fixtures.py
+++ b/tests/discovery_fixtures.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+import asyncio
import copy
+from collections.abc import Coroutine
from dataclasses import dataclass
from json import dumps as json_dumps
from typing import Any, TypedDict
@@ -34,7 +36,7 @@ UNSUPPORTED_HOMEWIFISYSTEM = {
"group_id": "REDACTED_07d902da02fa9beab8a64",
"group_name": "I01BU0tFRF9TU0lEIw==", # '#MASKED_SSID#'
"hardware_version": "3.0",
- "ip": "192.168.1.192",
+ "ip": "127.0.0.1",
"mac": "24:2F:D0:00:00:00",
"master_device_id": "REDACTED_51f72a752213a6c45203530",
"need_account_digest": True,
@@ -130,14 +132,19 @@ new_discovery = parametrize_discovery(
"new discovery", data_root_filter="discovery_result"
)
+smart_discovery = parametrize_discovery("smart discovery", protocol_filter={"SMART"})
+
@pytest.fixture(
- params=filter_fixtures("discoverable", protocol_filter={"SMART", "IOT"}),
+ params=filter_fixtures(
+ "discoverable", protocol_filter={"SMART", "SMARTCAM", "IOT"}
+ ),
ids=idgenerator,
)
async def discovery_mock(request, mocker):
"""Mock discovery and patch protocol queries to use Fake protocols."""
- fixture_info: FixtureInfo = request.param
+ fi: FixtureInfo = request.param
+ fixture_info = FixtureInfo(fi.name, fi.protocol, copy.deepcopy(fi.data))
return patch_discovery({DISCOVERY_MOCK_IP: fixture_info}, mocker)
@@ -156,6 +163,18 @@ def create_discovery_mock(ip: str, fixture_data: dict):
https: bool
login_version: int | None = None
port_override: int | None = None
+ http_port: int | None = None
+
+ @property
+ def model(self) -> str:
+ dd = self.discovery_data
+ model_region = (
+ dd["result"]["device_model"]
+ if self.discovery_port == 20002
+ else dd["system"]["get_sysinfo"]["model"]
+ )
+ model, _, _ = model_region.partition("(")
+ return model
@property
def _datagram(self) -> bytes:
@@ -168,18 +187,27 @@ def create_discovery_mock(ip: str, fixture_data: dict):
)
if "discovery_result" in fixture_data:
- discovery_data = {"result": fixture_data["discovery_result"].copy()}
- discovery_result = fixture_data["discovery_result"]
+ discovery_data = fixture_data["discovery_result"].copy()
+ discovery_result = fixture_data["discovery_result"]["result"]
device_type = discovery_result["device_type"]
encrypt_type = discovery_result["mgt_encrypt_schm"].get(
"encrypt_type", discovery_result.get("encrypt_info", {}).get("sym_schm")
)
- login_version = discovery_result["mgt_encrypt_schm"].get("lv")
+ if not (login_version := discovery_result["mgt_encrypt_schm"].get("lv")) and (
+ et := discovery_result.get("encrypt_type")
+ ):
+ login_version = max([int(i) for i in et])
https = discovery_result["mgt_encrypt_schm"]["is_support_https"]
+ http_port = discovery_result["mgt_encrypt_schm"].get("http_port")
+ if not http_port: # noqa: SIM108
+ # Not all discovery responses set the http port, i.e. smartcam.
+ default_port = 443 if https else 80
+ else:
+ default_port = http_port
dm = _DiscoveryMock(
ip,
- 80,
+ default_port,
20002,
discovery_data,
fixture_data,
@@ -187,6 +215,7 @@ def create_discovery_mock(ip: str, fixture_data: dict):
encrypt_type,
https,
login_version,
+ http_port=http_port,
)
else:
sys_info = fixture_data["system"]["get_sysinfo"]
@@ -226,12 +255,46 @@ def patch_discovery(fixture_infos: dict[str, FixtureInfo], mocker):
first_ip = list(fixture_infos.keys())[0]
first_host = None
+ # Mock _run_callback_task so the tasks complete in the order they started.
+ # Otherwise test output is non-deterministic which affects readme examples.
+ callback_queue: asyncio.Queue = asyncio.Queue()
+ exception_queue: asyncio.Queue = asyncio.Queue()
+
+ async def process_callback_queue(finished_event: asyncio.Event) -> None:
+ while (finished_event.is_set() is False) or callback_queue.qsize():
+ coro = await callback_queue.get()
+ try:
+ await coro
+ except Exception as ex:
+ await exception_queue.put(ex)
+ else:
+ await exception_queue.put(None)
+ callback_queue.task_done()
+
+ async def wait_for_coro():
+ await callback_queue.join()
+ if ex := exception_queue.get_nowait():
+ raise ex
+
+ def _run_callback_task(self, coro: Coroutine) -> None:
+ callback_queue.put_nowait(coro)
+ task = asyncio.create_task(wait_for_coro())
+ self.callback_tasks.append(task)
+
+ mocker.patch(
+ "kasa.discover._DiscoverProtocol._run_callback_task", _run_callback_task
+ )
+
+ # do_discover_mock
async def mock_discover(self):
"""Call datagram_received for all mock fixtures.
Handles test cases modifying the ip and hostname of the first fixture
for discover_single testing.
"""
+ finished_event = asyncio.Event()
+ asyncio.create_task(process_callback_queue(finished_event))
+
for ip, dm in discovery_mocks.items():
first_ip = list(discovery_mocks.values())[0].ip
fixture_info = fixture_infos[ip]
@@ -258,10 +321,18 @@ def patch_discovery(fixture_infos: dict[str, FixtureInfo], mocker):
dm._datagram,
(dm.ip, port),
)
+ # Setting this event will stop the processing of callbacks
+ finished_event.set()
+ mocker.patch("kasa.discover._DiscoverProtocol.do_discover", mock_discover)
+
+ # query_mock
async def _query(self, request, retry_count: int = 3):
return await protos[self._host].query(request)
+ mocker.patch("kasa.IotProtocol.query", _query)
+ mocker.patch("kasa.SmartProtocol.query", _query)
+
def _getaddrinfo(host, *_, **__):
nonlocal first_host, first_ip
first_host = host # Store the hostname used by discover single
@@ -270,20 +341,21 @@ def patch_discovery(fixture_infos: dict[str, FixtureInfo], mocker):
].ip # ip could have been overridden in test
return [(None, None, None, None, (first_ip, 0))]
- mocker.patch("kasa.IotProtocol.query", _query)
- mocker.patch("kasa.SmartProtocol.query", _query)
- mocker.patch("kasa.discover._DiscoverProtocol.do_discover", mock_discover)
- mocker.patch(
- "socket.getaddrinfo",
- # side_effect=lambda *_, **__: [(None, None, None, None, (first_ip, 0))],
- side_effect=_getaddrinfo,
- )
+ mocker.patch("socket.getaddrinfo", side_effect=_getaddrinfo)
+
+ # Mock decrypt so it doesn't error with unencryptable empty data in the
+ # fixtures. The discovery result will already contain the decrypted data
+ # deserialized from the fixture
+ mocker.patch("kasa.discover.Discover._decrypt_discovery_data")
+
# Only return the first discovery mock to be used for testing discover single
return discovery_mocks[first_ip]
@pytest.fixture(
- params=filter_fixtures("discoverable", protocol_filter={"SMART", "IOT"}),
+ params=filter_fixtures(
+ "discoverable", protocol_filter={"SMART", "SMARTCAM", "IOT"}
+ ),
ids=idgenerator,
)
def discovery_data(request, mocker):
@@ -303,7 +375,7 @@ def discovery_data(request, mocker):
mocker.patch("kasa.IotProtocol.query", return_value=fixture_data)
mocker.patch("kasa.SmartProtocol.query", return_value=fixture_data)
if "discovery_result" in fixture_data:
- return {"result": fixture_data["discovery_result"]}
+ return fixture_data["discovery_result"].copy()
else:
return {"system": {"get_sysinfo": fixture_data["system"]["get_sysinfo"]}}
diff --git a/tests/fakeprotocol_smart.py b/tests/fakeprotocol_smart.py
index 7dbfefac..3645ff68 100644
--- a/tests/fakeprotocol_smart.py
+++ b/tests/fakeprotocol_smart.py
@@ -7,6 +7,8 @@ import pytest
from kasa import Credentials, DeviceConfig, SmartProtocol
from kasa.exceptions import SmartErrorCode
from kasa.smart import SmartChildDevice
+from kasa.smartcam import SmartCamChild
+from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
from kasa.transports.basetransport import BaseTransport
@@ -48,13 +50,18 @@ class FakeSmartTransport(BaseTransport):
),
)
self.fixture_name = fixture_name
+
+ # When True verbatim will bypass any extra processing of missing
+ # methods and is used to test the fixture creation itself.
+ self.verbatim = verbatim
+
# Don't copy the dict if the device is a child so that updates on the
# child are then still reflected on the parent's lis of child device in
if not is_child:
self.info = copy.deepcopy(info)
if get_child_fixtures:
self.child_protocols = self._get_child_protocols(
- self.info, self.fixture_name, "get_child_device_list"
+ self.info, self.fixture_name, "get_child_device_list", self.verbatim
)
else:
self.info = info
@@ -67,9 +74,6 @@ class FakeSmartTransport(BaseTransport):
self.warn_fixture_missing_methods = warn_fixture_missing_methods
self.fix_incomplete_fixture_lists = fix_incomplete_fixture_lists
- # When True verbatim will bypass any extra processing of missing
- # methods and is used to test the fixture creation itself.
- self.verbatim = verbatim
if verbatim:
self.warn_fixture_missing_methods = False
self.fix_incomplete_fixture_lists = False
@@ -114,8 +118,17 @@ class FakeSmartTransport(BaseTransport):
"type": 0,
},
),
+ "get_homekit_info": (
+ "homekit",
+ {
+ "mfi_setup_code": "000-00-000",
+ "mfi_setup_id": "0000",
+ "mfi_token_token": "000000000000000000000000000000000",
+ "mfi_token_uuid": "00000000-0000-0000-0000-000000000000",
+ },
+ ),
"get_auto_update_info": (
- "firmware",
+ ("firmware", 2),
{"enable": True, "random_range": 120, "time": 180},
),
"get_alarm_configure": (
@@ -149,8 +162,49 @@ class FakeSmartTransport(BaseTransport):
"energy_monitoring",
{"igain": 10861, "vgain": 118657},
),
+ "get_matter_setup_info": (
+ "matter",
+ {
+ "setup_code": "00000000000",
+ "setup_payload": "00:0000000-0000.00.000",
+ },
+ ),
+ # child setup
+ "get_support_child_device_category": (
+ "child_quick_setup",
+ {"device_category_list": [{"category": "subg.trv"}]},
+ ),
+ # no devices found
+ "get_scan_child_device_list": (
+ "child_quick_setup",
+ {"child_device_list": [{"dummy": "response"}], "scan_status": "idle"},
+ ),
}
+ def _missing_result(self, method):
+ """Check the FIXTURE_MISSING_MAP for responses.
+
+ Fixtures generated prior to a query being supported by dump_devinfo
+ do not have the response so this method checks whether the component
+ is supported and fills in the missing response.
+ If the first value of the lookup value is a tuple it will also check
+ the version, i.e. (component_name, component_version).
+ """
+ if not (missing := self.FIXTURE_MISSING_MAP.get(method)):
+ return None
+ condition = missing[0]
+ if (
+ isinstance(condition, tuple)
+ and (version := self.components.get(condition[0]))
+ and version >= condition[1]
+ ):
+ return copy.deepcopy(missing[1])
+
+ if condition in self.components:
+ return copy.deepcopy(missing[1])
+
+ return None
+
async def send(self, request: str):
request_dict = json_loads(request)
method = request_dict["method"]
@@ -171,7 +225,7 @@ class FakeSmartTransport(BaseTransport):
@staticmethod
def _get_child_protocols(
- parent_fixture_info, parent_fixture_name, child_devices_key
+ parent_fixture_info, parent_fixture_name, child_devices_key, verbatim
):
child_infos = parent_fixture_info.get(child_devices_key, {}).get(
"child_device_list", []
@@ -183,16 +237,20 @@ class FakeSmartTransport(BaseTransport):
# imported here to avoid circular import
from .conftest import filter_fixtures
- def try_get_child_fixture_info(child_dev_info):
+ def try_get_child_fixture_info(child_dev_info, protocol):
hw_version = child_dev_info["hw_ver"]
- sw_version = child_dev_info["fw_ver"]
+ sw_version = child_dev_info.get("sw_ver", child_dev_info.get("fw_ver"))
sw_version = sw_version.split(" ")[0]
- model = child_dev_info["model"]
- region = child_dev_info.get("specs", "XX")
- child_fixture_name = f"{model}({region})_{hw_version}_{sw_version}"
+ model = child_dev_info.get("device_model", child_dev_info.get("model"))
+ assert sw_version
+ assert model
+
+ region = child_dev_info.get("specs", child_dev_info.get("region"))
+ region = f"({region})" if region else ""
+ child_fixture_name = f"{model}{region}_{hw_version}_{sw_version}"
child_fixtures = filter_fixtures(
"Child fixture",
- protocol_filter={"SMART.CHILD"},
+ protocol_filter={protocol},
model_filter={child_fixture_name},
)
if child_fixtures:
@@ -205,12 +263,17 @@ class FakeSmartTransport(BaseTransport):
and (category := child_info.get("category"))
and category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP
):
- if fixture_info_tuple := try_get_child_fixture_info(child_info):
+ if fixture_info_tuple := try_get_child_fixture_info(
+ child_info, "SMART.CHILD"
+ ):
child_fixture = copy.deepcopy(fixture_info_tuple.data)
child_fixture["get_device_info"]["device_id"] = device_id
found_child_fixture_infos.append(child_fixture["get_device_info"])
child_protocols[device_id] = FakeSmartProtocol(
- child_fixture, fixture_info_tuple.name, is_child=True
+ child_fixture,
+ fixture_info_tuple.name,
+ is_child=True,
+ verbatim=verbatim,
)
# Look for fixture inline
elif (child_fixtures := parent_fixture_info.get("child_devices")) and (
@@ -221,19 +284,46 @@ class FakeSmartTransport(BaseTransport):
child_fixture,
f"{parent_fixture_name}-{device_id}",
is_child=True,
+ verbatim=verbatim,
)
else:
pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
parent_fixture_name, set()
).add("child_devices")
+ elif (
+ (device_id := child_info.get("device_id"))
+ and (category := child_info.get("category"))
+ and category in SmartCamChild.CHILD_DEVICE_TYPE_MAP
+ and (
+ fixture_info_tuple := try_get_child_fixture_info(
+ child_info, "SMARTCAM.CHILD"
+ )
+ )
+ ):
+ from .fakeprotocol_smartcam import FakeSmartCamProtocol
+
+ child_fixture = copy.deepcopy(fixture_info_tuple.data)
+ child_fixture["getDeviceInfo"]["device_info"]["basic_info"][
+ "dev_id"
+ ] = device_id
+ child_fixture[CHILD_INFO_FROM_PARENT]["device_id"] = device_id
+ # We copy the child device info to the parent getChildDeviceInfo
+ # list for smartcam children in order for updates to work.
+ found_child_fixture_infos.append(child_fixture[CHILD_INFO_FROM_PARENT])
+ child_protocols[device_id] = FakeSmartCamProtocol(
+ child_fixture,
+ fixture_info_tuple.name,
+ is_child=True,
+ verbatim=verbatim,
+ )
else:
warn(
- f"Child is a cameraprotocol which needs to be implemented {child_info}",
+ f"Child is a protocol which needs to be implemented {child_info}",
stacklevel=2,
)
# Replace parent child infos with the infos from the child fixtures so
# that updates update both
- if child_infos and found_child_fixture_infos:
+ if not verbatim and child_infos and found_child_fixture_infos:
parent_fixture_info[child_devices_key]["child_device_list"] = (
found_child_fixture_infos
)
@@ -300,18 +390,16 @@ class FakeSmartTransport(BaseTransport):
elif child_method in child_device_calls:
result = copy.deepcopy(child_device_calls[child_method])
return {"result": result, "error_code": 0}
- elif (
+ elif missing_result := self._missing_result(child_method):
# FIXTURE_MISSING is for service calls not in place when
# SMART fixtures started to be generated
- missing_result := self.FIXTURE_MISSING_MAP.get(child_method)
- ) and missing_result[0] in self.components:
# Copy to info so it will work with update methods
- child_device_calls[child_method] = copy.deepcopy(missing_result[1])
+ child_device_calls[child_method] = missing_result
result = copy.deepcopy(info[child_method])
retval = {"result": result, "error_code": 0}
return retval
- elif child_method[:4] == "set_":
- target_method = f"get_{child_method[4:]}"
+ elif child_method[:3] == "set":
+ target_method = f"get{child_method[3:]}"
if target_method not in child_device_calls:
raise RuntimeError(
f"No {target_method} in child info, calling set before get not supported."
@@ -468,6 +556,48 @@ class FakeSmartTransport(BaseTransport):
return {"error_code": 0}
+ def _hub_remove_device(self, info, params):
+ """Remove hub device."""
+ items_to_remove = [dev["device_id"] for dev in params["child_device_list"]]
+ children = info["get_child_device_list"]["child_device_list"]
+ new_children = [
+ dev for dev in children if dev["device_id"] not in items_to_remove
+ ]
+ info["get_child_device_list"]["child_device_list"] = new_children
+
+ return {"error_code": 0}
+
+ def get_child_device_queries(self, method, params):
+ return self._get_method_from_info(method, params)
+
+ def _get_method_from_info(self, method, params):
+ result = copy.deepcopy(self.info[method])
+ if result and "start_index" in result and "sum" in result:
+ list_key = next(
+ iter([key for key in result if isinstance(result[key], list)])
+ )
+ start_index = (
+ start_index
+ if (params and (start_index := params.get("start_index")))
+ else 0
+ )
+ # Fixtures generated before _handle_response_lists was implemented
+ # could have incomplete lists.
+ if (
+ len(result[list_key]) < result["sum"]
+ and self.fix_incomplete_fixture_lists
+ ):
+ result["sum"] = len(result[list_key])
+ if self.warn_fixture_missing_methods:
+ pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
+ self.fixture_name, set()
+ ).add(f"{method} (incomplete '{list_key}' list)")
+
+ result[list_key] = result[list_key][
+ start_index : start_index + self.list_return_size
+ ]
+ return {"result": result, "error_code": 0}
+
async def _send_request(self, request_dict: dict):
method = request_dict["method"]
@@ -476,34 +606,17 @@ class FakeSmartTransport(BaseTransport):
return await self._handle_control_child(request_dict["params"])
params = request_dict.get("params", {})
- if method in {"component_nego", "qs_component_nego"} or method[:4] == "get_":
- if method in info:
- result = copy.deepcopy(info[method])
- if result and "start_index" in result and "sum" in result:
- list_key = next(
- iter([key for key in result if isinstance(result[key], list)])
- )
- start_index = (
- start_index
- if (params and (start_index := params.get("start_index")))
- else 0
- )
- # Fixtures generated before _handle_response_lists was implemented
- # could have incomplete lists.
- if (
- len(result[list_key]) < result["sum"]
- and self.fix_incomplete_fixture_lists
- ):
- result["sum"] = len(result[list_key])
- if self.warn_fixture_missing_methods:
- pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
- self.fixture_name, set()
- ).add(f"{method} (incomplete '{list_key}' list)")
+ if method in {"component_nego", "qs_component_nego"} or method[:3] == "get":
+ # These methods are handled in get_child_device_query so it can be
+ # patched for tests to simulate dynamic devices.
+ if (
+ method in ("get_child_device_list", "get_child_device_component_list")
+ and method in info
+ ):
+ return self.get_child_device_queries(method, params)
- result[list_key] = result[list_key][
- start_index : start_index + self.list_return_size
- ]
- return {"result": result, "error_code": 0}
+ if method in info:
+ return self._get_method_from_info(method, params)
if self.verbatim:
return {
@@ -511,13 +624,11 @@ class FakeSmartTransport(BaseTransport):
"method": method,
}
- if (
+ if missing_result := self._missing_result(method):
# FIXTURE_MISSING is for service calls not in place when
# SMART fixtures started to be generated
- missing_result := self.FIXTURE_MISSING_MAP.get(method)
- ) and missing_result[0] in self.components:
# Copy to info so it will work with update methods
- info[method] = copy.deepcopy(missing_result[1])
+ info[method] = missing_result
result = copy.deepcopy(info[method])
retval = {"result": result, "error_code": 0}
elif (
@@ -566,9 +677,30 @@ class FakeSmartTransport(BaseTransport):
return self._set_on_off_gradually_info(info, params)
elif method == "set_child_protection":
return self._update_sysinfo_key(info, "child_protection", params["enable"])
- elif method[:4] == "set_":
- target_method = f"get_{method[4:]}"
+ elif method == "remove_child_device_list":
+ return self._hub_remove_device(info, params)
+ # actions
+ elif method in [
+ "begin_scanning_child_device", # hub pairing
+ "add_child_device_list", # hub pairing
+ "remove_child_device_list", # hub pairing
+ "playSelectAudio", # vacuum special actions
+ "resetConsumablesTime", # vacuum special actions
+ ]:
+ return {"error_code": 0}
+ elif method[:3] == "set":
+ target_method = f"get{method[3:]}"
+ # Some vacuum commands do not have a getter
+ if method in [
+ "setRobotPause",
+ "setSwitchClean",
+ "setSwitchCharge",
+ "setSwitchDustCollection",
+ ]:
+ return {"error_code": 0}
+
info[target_method].update(params)
+
return {"error_code": 0}
async def close(self) -> None:
diff --git a/tests/fakeprotocol_smartcam.py b/tests/fakeprotocol_smartcam.py
index d110e784..5e439626 100644
--- a/tests/fakeprotocol_smartcam.py
+++ b/tests/fakeprotocol_smartcam.py
@@ -6,6 +6,7 @@ from typing import Any
from kasa import Credentials, DeviceConfig, SmartProtocol
from kasa.protocols.smartcamprotocol import SmartCamProtocol
+from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT, SmartCamChild
from kasa.transports.basetransport import BaseTransport
from .fakeprotocol_smart import FakeSmartTransport
@@ -33,7 +34,9 @@ class FakeSmartCamTransport(BaseTransport):
*,
list_return_size=10,
is_child=False,
+ get_child_fixtures=True,
verbatim=False,
+ components_not_included=False,
):
super().__init__(
config=DeviceConfig(
@@ -44,20 +47,35 @@ class FakeSmartCamTransport(BaseTransport):
),
),
)
+
self.fixture_name = fixture_name
# When True verbatim will bypass any extra processing of missing
# methods and is used to test the fixture creation itself.
self.verbatim = verbatim
if not is_child:
self.info = copy.deepcopy(info)
- self.child_protocols = FakeSmartTransport._get_child_protocols(
- self.info, self.fixture_name, "getChildDeviceList"
- )
+ # We don't need to get the child fixtures if testing things like
+ # lists
+ if get_child_fixtures:
+ self.child_protocols = FakeSmartTransport._get_child_protocols(
+ self.info, self.fixture_name, "getChildDeviceList", self.verbatim
+ )
else:
self.info = info
- # self.child_protocols = self._get_child_protocols()
+
self.list_return_size = list_return_size
+ # Setting this flag allows tests to create dummy transports without
+ # full fixture info for testing specific cases like list handling etc
+ self.components_not_included = (components_not_included,)
+ if not components_not_included:
+ self.components = {
+ comp["name"]: comp["version"]
+ for comp in self.info["getAppComponentList"]["app_component"][
+ "app_component_list"
+ ]
+ }
+
@property
def default_port(self):
"""Default port for the transport."""
@@ -108,10 +126,61 @@ class FakeSmartCamTransport(BaseTransport):
@staticmethod
def _get_param_set_value(info: dict, set_keys: list[str], value):
+ cifp = info.get(CHILD_INFO_FROM_PARENT)
+
for key in set_keys[:-1]:
info = info[key]
info[set_keys[-1]] = value
+ if (
+ cifp
+ and set_keys[0] == "getDeviceInfo"
+ and (
+ child_info_parent_key
+ := FakeSmartCamTransport.CHILD_INFO_SETTER_MAP.get(set_keys[-1])
+ )
+ ):
+ cifp[child_info_parent_key] = value
+
+ CHILD_INFO_SETTER_MAP = {
+ "device_alias": "alias",
+ }
+
+ FIXTURE_MISSING_MAP = {
+ "getMatterSetupInfo": (
+ "matter",
+ {
+ "setup_code": "00000000000",
+ "setup_payload": "00:0000000-0000.00.000",
+ },
+ ),
+ "getSupportChildDeviceCategory": (
+ "childQuickSetup",
+ {
+ "device_category_list": [
+ {"category": "ipcamera"},
+ {"category": "subg.trv"},
+ {"category": "subg.trigger"},
+ {"category": "subg.plugswitch"},
+ ]
+ },
+ ),
+ "getScanChildDeviceList": (
+ "childQuickSetup",
+ {
+ "child_device_list": [
+ {
+ "device_id": "0000000000000000000000000000000000000000",
+ "category": "subg.trigger.button",
+ "device_model": "S200B",
+ "name": "I01BU0tFRF9OQU1FIw====",
+ }
+ ],
+ "scan_wait_time": 55,
+ "scan_status": "scanning",
+ },
+ ),
+ }
# Setters for when there's not a simple mapping of setters to getters
SETTERS = {
("system", "sys", "dev_alias"): [
@@ -136,6 +205,17 @@ class FakeSmartCamTransport(BaseTransport):
],
}
+ def _hub_remove_device(self, info, params):
+ """Remove hub device."""
+ items_to_remove = [dev["device_id"] for dev in params["child_device_list"]]
+ children = info["getChildDeviceList"]["child_device_list"]
+ new_children = [
+ dev for dev in children if dev["device_id"] not in items_to_remove
+ ]
+ info["getChildDeviceList"]["child_device_list"] = new_children
+
+ return {"result": {}, "error_code": 0}
+
@staticmethod
def _get_second_key(request_dict: dict[str, Any]) -> str:
assert (
@@ -145,6 +225,33 @@ class FakeSmartCamTransport(BaseTransport):
next(it, None)
return next(it)
+ def get_child_device_queries(self, method, params):
+ return self._get_method_from_info(method, params)
+
+ def _get_method_from_info(self, method, params):
+ result = copy.deepcopy(self.info[method])
+ if "start_index" in result and "sum" in result:
+ list_key = next(
+ iter([key for key in result if isinstance(result[key], list)])
+ )
+ assert isinstance(params, dict)
+ module_name = next(iter(params))
+
+ start_index = (
+ start_index
+ if (
+ params
+ and module_name
+ and (start_index := params[module_name].get("start_index"))
+ )
+ else 0
+ )
+
+ result[list_key] = result[list_key][
+ start_index : start_index + self.list_return_size
+ ]
+ return {"result": result, "error_code": 0}
+
async def _send_request(self, request_dict: dict):
method = request_dict["method"]
@@ -199,26 +306,55 @@ class FakeSmartCamTransport(BaseTransport):
return {**result, "error_code": 0}
else:
return {"error_code": -1}
- elif method[:3] == "get":
- params = request_dict.get("params")
- if method in info:
- result = copy.deepcopy(info[method])
- if "start_index" in result and "sum" in result:
- list_key = next(
- iter([key for key in result if isinstance(result[key], list)])
- )
- start_index = (
- start_index
- if (params and (start_index := params.get("start_index")))
- else 0
- )
+ elif method == "removeChildDeviceList":
+ return self._hub_remove_device(info, request_dict["params"]["childControl"])
+ # actions
+ elif method in [
+ "addScanChildDeviceList",
+ "startScanChildDevice",
+ ]:
+ return {"result": {}, "error_code": 0}
+
+ # smartcam child devices do not make requests for getDeviceInfo as they
+ # get updated from the parent's query. If this is being called from a
+ # child it must be because the fixture has been created directly on the
+ # child device with a dummy parent. In this case return the child info
+ # from parent that's inside the fixture.
+ if (
+ not self.verbatim
+ and method == "getDeviceInfo"
+ and (cifp := info.get(CHILD_INFO_FROM_PARENT))
+ ):
+ mapped = SmartCamChild._map_child_info_from_parent(cifp)
+ result = {"device_info": {"basic_info": mapped}}
+ return {"result": result, "error_code": 0}
+
+ # These methods are handled in get_child_device_query so it can be
+ # patched for tests to simulate dynamic devices.
+ if (
+ method in ("getChildDeviceList", "getChildDeviceComponentList")
+ and method in info
+ ):
+ params = request_dict.get("params")
+ return self.get_child_device_queries(method, params)
+
+ if method in info:
+ params = request_dict.get("params")
+ return self._get_method_from_info(method, params)
+
+ if self.verbatim:
+ return {"error_code": -1}
+
+ if (
+ # FIXTURE_MISSING is for service calls not in place when
+ # SMART fixtures started to be generated
+ missing_result := self.FIXTURE_MISSING_MAP.get(method)
+ ) and missing_result[0] in self.components:
+ # Copy to info so it will work with update methods
+ info[method] = copy.deepcopy(missing_result[1])
+ result = copy.deepcopy(info[method])
+ return {"result": result, "error_code": 0}
- result[list_key] = result[list_key][
- start_index : start_index + self.list_return_size
- ]
- return {"result": result, "error_code": 0}
- else:
- return {"error_code": -1}
return {"error_code": -1}
async def close(self) -> None:
diff --git a/tests/fixtureinfo.py b/tests/fixtureinfo.py
index fc1dd1fb..fbfe6ff8 100644
--- a/tests/fixtureinfo.py
+++ b/tests/fixtureinfo.py
@@ -60,11 +60,19 @@ SUPPORTED_SMARTCAM_DEVICES = [
)
]
+SUPPORTED_SMARTCAM_CHILD_DEVICES = [
+ (device, "SMARTCAM.CHILD")
+ for device in glob.glob(
+ os.path.dirname(os.path.abspath(__file__)) + "/fixtures/smartcam/child/*.json"
+ )
+]
+
SUPPORTED_DEVICES = (
SUPPORTED_IOT_DEVICES
+ SUPPORTED_SMART_DEVICES
+ SUPPORTED_SMART_CHILD_DEVICES
+ SUPPORTED_SMARTCAM_DEVICES
+ + SUPPORTED_SMARTCAM_CHILD_DEVICES
)
@@ -77,19 +85,13 @@ def idgenerator(paramtuple: FixtureInfo):
return None
-def get_fixture_info() -> list[FixtureInfo]:
+def get_fixture_infos() -> list[FixtureInfo]:
"""Return raw discovery file contents as JSON. Used for discovery tests."""
fixture_data = []
for file, protocol in SUPPORTED_DEVICES:
p = Path(file)
- folder = Path(__file__).parent / "fixtures"
- if protocol == "SMART":
- folder = folder / "smart"
- if protocol == "SMART.CHILD":
- folder = folder / "smart/child"
- p = folder / file
- with open(p) as f:
+ with open(file) as f:
data = json.load(f)
fixture_name = p.name
@@ -99,7 +101,7 @@ def get_fixture_info() -> list[FixtureInfo]:
return fixture_data
-FIXTURE_DATA: list[FixtureInfo] = get_fixture_info()
+FIXTURE_DATA: list[FixtureInfo] = get_fixture_infos()
def filter_fixtures(
@@ -145,12 +147,21 @@ def filter_fixtures(
def _component_match(
fixture_data: FixtureInfo, component_filter: str | ComponentFilter
):
- if (component_nego := fixture_data.data.get("component_nego")) is None:
+ components = {}
+ if component_nego := fixture_data.data.get("component_nego"):
+ components = {
+ component["id"]: component["ver_code"]
+ for component in component_nego["component_list"]
+ }
+ if get_app_component_list := fixture_data.data.get("getAppComponentList"):
+ components = {
+ component["name"]: component["version"]
+ for component in get_app_component_list["app_component"][
+ "app_component_list"
+ ]
+ }
+ if not components:
return False
- components = {
- component["id"]: component["ver_code"]
- for component in component_nego["component_list"]
- }
if isinstance(component_filter, str):
return component_filter in components
else:
@@ -179,7 +190,7 @@ def filter_fixtures(
IotDevice._get_device_type_from_sys_info(fixture_data.data)
in device_type
)
- elif fixture_data.protocol == "SMARTCAM":
+ elif fixture_data.protocol in {"SMARTCAM", "SMARTCAM.CHILD"}:
info = fixture_data.data["getDeviceInfo"]["device_info"]["basic_info"]
return SmartCamDevice._get_device_type_from_sysinfo(info) in device_type
return False
diff --git a/tests/fixtures/iot/EP10(US)_1.0_1.0.2.json b/tests/fixtures/iot/EP10(US)_1.0_1.0.2.json
index e40543d6..11cafb87 100644
--- a/tests/fixtures/iot/EP10(US)_1.0_1.0.2.json
+++ b/tests/fixtures/iot/EP10(US)_1.0_1.0.2.json
@@ -2,7 +2,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "167 lamp",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug Mini",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/EP40(US)_1.0_1.0.2.json b/tests/fixtures/iot/EP40(US)_1.0_1.0.2.json
index 238265a2..5be97e87 100644
--- a/tests/fixtures/iot/EP40(US)_1.0_1.0.2.json
+++ b/tests/fixtures/iot/EP40(US)_1.0_1.0.2.json
@@ -1,12 +1,12 @@
{
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Smart Plug_004F",
+ "alias": "#MASKED_NAME#",
"child_num": 2,
"children": [
{
- "alias": "Zombie",
- "id": "8006231E1499BAC4D4BC7EFCD4B075181E6393F200",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -14,8 +14,8 @@
"state": 0
},
{
- "alias": "Magic",
- "id": "8006231E1499BAC4D4BC7EFCD4B075181E6393F201",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/ES20M(US)_1.0_1.0.11.json b/tests/fixtures/iot/ES20M(US)_1.0_1.0.11.json
index 99ecdaa5..6d15034f 100644
--- a/tests/fixtures/iot/ES20M(US)_1.0_1.0.11.json
+++ b/tests/fixtures/iot/ES20M(US)_1.0_1.0.11.json
@@ -11,7 +11,7 @@
"stopConnect": 0,
"tcspInfo": "",
"tcspStatus": 1,
- "username": "#MASKED_NAME#"
+ "username": "user@example.com"
},
"get_intl_fw_list": {
"err_code": 0,
diff --git a/tests/fixtures/iot/ES20M(US)_1.0_1.0.8.json b/tests/fixtures/iot/ES20M(US)_1.0_1.0.8.json
index bb316b83..e28301d5 100644
--- a/tests/fixtures/iot/ES20M(US)_1.0_1.0.8.json
+++ b/tests/fixtures/iot/ES20M(US)_1.0_1.0.8.json
@@ -78,7 +78,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Test ES20M",
+ "alias": "#MASKED_NAME#",
"brightness": 35,
"dev_name": "Wi-Fi Smart Dimmer with sensor",
"deviceId": "0000000000000000000000000000000000000000",
diff --git a/tests/fixtures/iot/HS100(UK)_4.1_1.1.0.json b/tests/fixtures/iot/HS100(UK)_4.1_1.1.0.json
index 6e33fd7d..324e193a 100644
--- a/tests/fixtures/iot/HS100(UK)_4.1_1.1.0.json
+++ b/tests/fixtures/iot/HS100(UK)_4.1_1.1.0.json
@@ -1,18 +1,21 @@
{
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "HS100(UK)",
- "device_type": "IOT.SMARTPLUGSWITCH",
- "factory_default": true,
- "hw_ver": "4.1",
- "ip": "127.0.0.123",
- "mac": "CC-32-E5-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false
- },
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "HS100(UK)",
+ "device_type": "IOT.SMARTPLUGSWITCH",
+ "factory_default": true,
+ "hw_ver": "4.1",
+ "ip": "127.0.0.123",
+ "mac": "CC-32-E5-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false
+ },
+ "owner": "00000000000000000000000000000000"
+ }
},
"system": {
"get_sysinfo": {
diff --git a/tests/fixtures/iot/HS100(US)_1.0_1.2.5.json b/tests/fixtures/iot/HS100(US)_1.0_1.2.5.json
index 1bbe29d4..1f2cad62 100644
--- a/tests/fixtures/iot/HS100(US)_1.0_1.2.5.json
+++ b/tests/fixtures/iot/HS100(US)_1.0_1.2.5.json
@@ -18,7 +18,7 @@
"system": {
"get_sysinfo": {
"active_mode": "schedule",
- "alias": "Unused 3",
+ "alias": "#MASKED_NAME#",
"dev_name": "Wi-Fi Smart Plug",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS100(US)_2.0_1.5.6.json b/tests/fixtures/iot/HS100(US)_2.0_1.5.6.json
index 03dd42d5..f73d6233 100644
--- a/tests/fixtures/iot/HS100(US)_2.0_1.5.6.json
+++ b/tests/fixtures/iot/HS100(US)_2.0_1.5.6.json
@@ -20,7 +20,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "3D Printer",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS103(US)_1.0_1.5.7.json b/tests/fixtures/iot/HS103(US)_1.0_1.5.7.json
index e5928c3d..ec388dd3 100644
--- a/tests/fixtures/iot/HS103(US)_1.0_1.5.7.json
+++ b/tests/fixtures/iot/HS103(US)_1.0_1.5.7.json
@@ -20,7 +20,7 @@
"system": {
"get_sysinfo": {
"active_mode": "schedule",
- "alias": "Night lite",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug Lite",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS103(US)_2.1_1.1.2.json b/tests/fixtures/iot/HS103(US)_2.1_1.1.2.json
index 664845f6..a9064ac7 100644
--- a/tests/fixtures/iot/HS103(US)_2.1_1.1.2.json
+++ b/tests/fixtures/iot/HS103(US)_2.1_1.1.2.json
@@ -18,7 +18,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Corner",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug Lite",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS103(US)_2.1_1.1.4.json b/tests/fixtures/iot/HS103(US)_2.1_1.1.4.json
index 819c5bdd..cf7cb935 100644
--- a/tests/fixtures/iot/HS103(US)_2.1_1.1.4.json
+++ b/tests/fixtures/iot/HS103(US)_2.1_1.1.4.json
@@ -2,7 +2,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Plug",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug Lite",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS105(US)_1.0_1.5.6.json b/tests/fixtures/iot/HS105(US)_1.0_1.5.6.json
index 79691004..a84c0f49 100644
--- a/tests/fixtures/iot/HS105(US)_1.0_1.5.6.json
+++ b/tests/fixtures/iot/HS105(US)_1.0_1.5.6.json
@@ -20,7 +20,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Unused 1",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug Mini",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS107(US)_1.0_1.0.8.json b/tests/fixtures/iot/HS107(US)_1.0_1.0.8.json
index 046a89e9..ddc61ef8 100644
--- a/tests/fixtures/iot/HS107(US)_1.0_1.0.8.json
+++ b/tests/fixtures/iot/HS107(US)_1.0_1.0.8.json
@@ -17,12 +17,12 @@
},
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Smart Plug_D310",
+ "alias": "#MASKED_NAME#",
"child_num": 2,
"children": [
{
- "alias": "Garage Charger 1",
- "id": "00",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -30,8 +30,8 @@
"state": 0
},
{
- "alias": "Garage Charger 2",
- "id": "01",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
@@ -46,7 +46,7 @@
"hw_ver": "1.0",
"latitude_i": 0,
"led_off": 0,
- "longitude_i": -0,
+ "longitude_i": 0,
"mac": "00:00:00:00:00:00",
"mic_type": "IOT.SMARTPLUGSWITCH",
"model": "HS107(US)",
diff --git a/tests/fixtures/iot/HS110(EU)_1.0_1.2.5.json b/tests/fixtures/iot/HS110(EU)_1.0_1.2.5.json
index 99cba288..e75b18bc 100644
--- a/tests/fixtures/iot/HS110(EU)_1.0_1.2.5.json
+++ b/tests/fixtures/iot/HS110(EU)_1.0_1.2.5.json
@@ -11,7 +11,7 @@
"system": {
"get_sysinfo": {
"active_mode": "schedule",
- "alias": "Bedroom Lamp Plug",
+ "alias": "#MASKED_NAME#",
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS110(US)_1.0_1.2.6.json b/tests/fixtures/iot/HS110(US)_1.0_1.2.6.json
index 5e285e72..cf5ac065 100644
--- a/tests/fixtures/iot/HS110(US)_1.0_1.2.6.json
+++ b/tests/fixtures/iot/HS110(US)_1.0_1.2.6.json
@@ -11,7 +11,7 @@
"system": {
"get_sysinfo": {
"active_mode": "schedule",
- "alias": "Home Google WiFi HS110",
+ "alias": "#MASKED_NAME#",
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS200(US)_2.0_1.5.7.json b/tests/fixtures/iot/HS200(US)_2.0_1.5.7.json
index 2fbcc65c..31e4a5f9 100644
--- a/tests/fixtures/iot/HS200(US)_2.0_1.5.7.json
+++ b/tests/fixtures/iot/HS200(US)_2.0_1.5.7.json
@@ -20,7 +20,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Master Bedroom Fan",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Light Switch",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS200(US)_5.0_1.0.2.json b/tests/fixtures/iot/HS200(US)_5.0_1.0.2.json
index fc09e6f5..44370f2e 100644
--- a/tests/fixtures/iot/HS200(US)_5.0_1.0.2.json
+++ b/tests/fixtures/iot/HS200(US)_5.0_1.0.2.json
@@ -2,7 +2,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "House Fan",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Light Switch",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS210(US)_1.0_1.5.8.json b/tests/fixtures/iot/HS210(US)_1.0_1.5.8.json
index ced3e891..b286c53f 100644
--- a/tests/fixtures/iot/HS210(US)_1.0_1.5.8.json
+++ b/tests/fixtures/iot/HS210(US)_1.0_1.5.8.json
@@ -21,7 +21,7 @@
"get_sysinfo": {
"abnormal_detect": 1,
"active_mode": "none",
- "alias": "Garage Light",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi 3-Way Light Switch",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/HS210(US)_3.0_1.0.10.json b/tests/fixtures/iot/HS210(US)_3.0_1.0.10.json
new file mode 100644
index 00000000..30a401e9
--- /dev/null
+++ b/tests/fixtures/iot/HS210(US)_3.0_1.0.10.json
@@ -0,0 +1,63 @@
+{
+ "cnCloud": {
+ "get_info": {
+ "binded": 1,
+ "cld_connection": 1,
+ "err_code": 0,
+ "fwDlPage": "",
+ "fwNotifyType": -1,
+ "illegalType": 0,
+ "server": "n-devs.tplinkcloud.com",
+ "stopConnect": 0,
+ "tcspInfo": "",
+ "tcspStatus": 1,
+ "username": "user@example.com"
+ },
+ "get_intl_fw_list": {
+ "err_code": 0,
+ "fw_list": []
+ }
+ },
+ "schedule": {
+ "get_next_action": {
+ "err_code": 0,
+ "type": -1
+ },
+ "get_rules": {
+ "enable": 0,
+ "err_code": 0,
+ "rule_list": [],
+ "version": 2
+ }
+ },
+ "system": {
+ "get_sysinfo": {
+ "active_mode": "none",
+ "alias": "#MASKED_NAME#",
+ "dev_name": "Smart Wi-Fi 3-Way Light Switch",
+ "deviceId": "0000000000000000000000000000000000000000",
+ "err_code": 0,
+ "feature": "TIM",
+ "hwId": "00000000000000000000000000000000",
+ "hw_ver": "3.0",
+ "icon_hash": "",
+ "latitude_i": 0,
+ "led_off": 0,
+ "longitude_i": 0,
+ "mac": "60:83:E7:00:00:00",
+ "mic_type": "IOT.SMARTPLUGSWITCH",
+ "model": "HS210(US)",
+ "next_action": {
+ "type": -1
+ },
+ "obd_src": "tplink",
+ "oemId": "00000000000000000000000000000000",
+ "on_time": 6525,
+ "relay_state": 1,
+ "rssi": -31,
+ "status": "new",
+ "sw_ver": "1.0.10 Build 240122 Rel.193635",
+ "updating": 0
+ }
+ }
+}
diff --git a/tests/fixtures/iot/HS220(US)_1.0_1.5.7.json b/tests/fixtures/iot/HS220(US)_1.0_1.5.7.json
index eef806fb..3826d198 100644
--- a/tests/fixtures/iot/HS220(US)_1.0_1.5.7.json
+++ b/tests/fixtures/iot/HS220(US)_1.0_1.5.7.json
@@ -28,7 +28,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Living Room Dimmer Switch",
+ "alias": "#MASKED_NAME#",
"brightness": 25,
"dev_name": "Smart Wi-Fi Dimmer",
"deviceId": "000000000000000000000000000000000000000",
@@ -38,9 +38,9 @@
"hwId": "00000000000000000000000000000000",
"hw_ver": "1.0",
"icon_hash": "",
- "latitude_i": 11.6210,
+ "latitude_i": 0,
"led_off": 0,
- "longitude_i": 42.2074,
+ "longitude_i": 0,
"mac": "00:00:00:00:00:00",
"mic_type": "IOT.SMARTPLUGSWITCH",
"model": "HS220(US)",
diff --git a/tests/fixtures/iot/HS220(US)_2.0_1.0.3.json b/tests/fixtures/iot/HS220(US)_2.0_1.0.3.json
index 61e3d84e..d7d0a5a2 100644
--- a/tests/fixtures/iot/HS220(US)_2.0_1.0.3.json
+++ b/tests/fixtures/iot/HS220(US)_2.0_1.0.3.json
@@ -17,7 +17,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Living Room Dimmer Switch",
+ "alias": "#MASKED_NAME#",
"brightness": 100,
"dev_name": "Wi-Fi Smart Dimmer",
"deviceId": "0000000000000000000000000000000000000000",
diff --git a/tests/fixtures/iot/HS300(US)_1.0_1.0.10.json b/tests/fixtures/iot/HS300(US)_1.0_1.0.10.json
index a6d34957..0fc22a39 100644
--- a/tests/fixtures/iot/HS300(US)_1.0_1.0.10.json
+++ b/tests/fixtures/iot/HS300(US)_1.0_1.0.10.json
@@ -22,12 +22,12 @@
},
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Power Strip_DAE1",
+ "alias": "#MASKED_NAME#",
"child_num": 6,
"children": [
{
- "alias": "Office Monitor 1",
- "id": "00",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -35,8 +35,8 @@
"state": 0
},
{
- "alias": "Office Monitor 2",
- "id": "01",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
@@ -44,8 +44,8 @@
"state": 0
},
{
- "alias": "Office Monitor 3",
- "id": "02",
+ "alias": "#MASKED_NAME# 3",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_3",
"next_action": {
"type": -1
},
@@ -53,8 +53,8 @@
"state": 0
},
{
- "alias": "Office Laptop Dock",
- "id": "03",
+ "alias": "#MASKED_NAME# 4",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_4",
"next_action": {
"type": -1
},
@@ -62,8 +62,8 @@
"state": 0
},
{
- "alias": "Office Desk Light",
- "id": "04",
+ "alias": "#MASKED_NAME# 5",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_5",
"next_action": {
"type": -1
},
@@ -71,8 +71,8 @@
"state": 0
},
{
- "alias": "Laptop",
- "id": "05",
+ "alias": "#MASKED_NAME# 6",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_6",
"next_action": {
"type": -1
},
@@ -87,7 +87,7 @@
"hw_ver": "1.0",
"latitude_i": 0,
"led_off": 0,
- "longitude_i": -0,
+ "longitude_i": 0,
"mac": "00:00:00:00:00:00",
"mic_type": "IOT.SMARTPLUGSWITCH",
"model": "HS300(US)",
diff --git a/tests/fixtures/iot/HS300(US)_1.0_1.0.21.json b/tests/fixtures/iot/HS300(US)_1.0_1.0.21.json
index 388fadf3..a174027c 100644
--- a/tests/fixtures/iot/HS300(US)_1.0_1.0.21.json
+++ b/tests/fixtures/iot/HS300(US)_1.0_1.0.21.json
@@ -10,12 +10,12 @@
},
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Power Strip_2CA9",
+ "alias": "#MASKED_NAME#",
"child_num": 6,
"children": [
{
- "alias": "Home CameraPC",
- "id": "800623145DFF1AA096363EFD161C2E661A9D8DED00",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -23,8 +23,8 @@
"state": 1
},
{
- "alias": "Home Firewalla",
- "id": "800623145DFF1AA096363EFD161C2E661A9D8DED01",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
@@ -32,8 +32,8 @@
"state": 1
},
{
- "alias": "Home Cox modem",
- "id": "800623145DFF1AA096363EFD161C2E661A9D8DED02",
+ "alias": "#MASKED_NAME# 3",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_3",
"next_action": {
"type": -1
},
@@ -41,8 +41,8 @@
"state": 1
},
{
- "alias": "Home rpi3-2",
- "id": "800623145DFF1AA096363EFD161C2E661A9D8DED03",
+ "alias": "#MASKED_NAME# 4",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_4",
"next_action": {
"type": -1
},
@@ -50,8 +50,8 @@
"state": 1
},
{
- "alias": "Home Camera Switch",
- "id": "800623145DFF1AA096363EFD161C2E661A9D8DED05",
+ "alias": "#MASKED_NAME# 5",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_5",
"next_action": {
"type": -1
},
@@ -59,8 +59,8 @@
"state": 1
},
{
- "alias": "Home Network Switch",
- "id": "800623145DFF1AA096363EFD161C2E661A9D8DED04",
+ "alias": "#MASKED_NAME# 6",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_6",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/HS300(US)_2.0_1.0.12.json b/tests/fixtures/iot/HS300(US)_2.0_1.0.12.json
index bdab432e..bca72089 100644
--- a/tests/fixtures/iot/HS300(US)_2.0_1.0.12.json
+++ b/tests/fixtures/iot/HS300(US)_2.0_1.0.12.json
@@ -15,8 +15,8 @@
"child_num": 6,
"children": [
{
- "alias": "#MASKED_NAME#",
- "id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D00",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -24,8 +24,8 @@
"state": 1
},
{
- "alias": "#MASKED_NAME#",
- "id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D01",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
@@ -33,8 +33,8 @@
"state": 1
},
{
- "alias": "#MASKED_NAME#",
- "id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D02",
+ "alias": "#MASKED_NAME# 3",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_3",
"next_action": {
"type": -1
},
@@ -42,8 +42,8 @@
"state": 1
},
{
- "alias": "#MASKED_NAME#",
- "id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D03",
+ "alias": "#MASKED_NAME# 4",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_4",
"next_action": {
"type": -1
},
@@ -51,8 +51,8 @@
"state": 1
},
{
- "alias": "#MASKED_NAME#",
- "id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D04",
+ "alias": "#MASKED_NAME# 5",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_5",
"next_action": {
"type": -1
},
@@ -60,8 +60,8 @@
"state": 1
},
{
- "alias": "#MASKED_NAME#",
- "id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D05",
+ "alias": "#MASKED_NAME# 6",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_6",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/HS300(US)_2.0_1.0.3.json b/tests/fixtures/iot/HS300(US)_2.0_1.0.3.json
index 3b99cf36..8a5b22c4 100644
--- a/tests/fixtures/iot/HS300(US)_2.0_1.0.3.json
+++ b/tests/fixtures/iot/HS300(US)_2.0_1.0.3.json
@@ -11,12 +11,12 @@
},
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Power Strip_5C33",
+ "alias": "#MASKED_NAME#",
"child_num": 6,
"children": [
{
- "alias": "Plug 1",
- "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031900",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -24,8 +24,8 @@
"state": 0
},
{
- "alias": "Plug 2",
- "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031901",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
@@ -33,8 +33,8 @@
"state": 0
},
{
- "alias": "Plug 3",
- "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031902",
+ "alias": "#MASKED_NAME# 3",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_3",
"next_action": {
"type": -1
},
@@ -42,8 +42,8 @@
"state": 0
},
{
- "alias": "Plug 4",
- "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031903",
+ "alias": "#MASKED_NAME# 4",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_4",
"next_action": {
"type": -1
},
@@ -51,8 +51,8 @@
"state": 0
},
{
- "alias": "Plug 5",
- "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031904",
+ "alias": "#MASKED_NAME# 5",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_5",
"next_action": {
"type": -1
},
@@ -60,8 +60,8 @@
"state": 0
},
{
- "alias": "Plug 6",
- "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031905",
+ "alias": "#MASKED_NAME# 6",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_6",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KL110(US)_1.0_1.8.11.json b/tests/fixtures/iot/KL110(US)_1.0_1.8.11.json
index 94c38858..89b623bd 100644
--- a/tests/fixtures/iot/KL110(US)_1.0_1.8.11.json
+++ b/tests/fixtures/iot/KL110(US)_1.0_1.8.11.json
@@ -21,7 +21,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Bulb3",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL120(US)_1.0_1.8.11.json b/tests/fixtures/iot/KL120(US)_1.0_1.8.11.json
index 1d8e1fce..0bbc9886 100644
--- a/tests/fixtures/iot/KL120(US)_1.0_1.8.11.json
+++ b/tests/fixtures/iot/KL120(US)_1.0_1.8.11.json
@@ -19,7 +19,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Home Family Room Table",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL120(US)_1.0_1.8.6.json b/tests/fixtures/iot/KL120(US)_1.0_1.8.6.json
index c251f2fa..50bd202e 100644
--- a/tests/fixtures/iot/KL120(US)_1.0_1.8.6.json
+++ b/tests/fixtures/iot/KL120(US)_1.0_1.8.6.json
@@ -34,11 +34,11 @@
},
"description": "Smart Wi-Fi LED Bulb with Tunable White Light",
"dev_state": "normal",
- "deviceId": "801200814AD69370AC59DE5501319C051AF409C3",
+ "deviceId": "0000000000000000000000000000000000000000",
"disco_ver": "1.0",
"err_code": 0,
"heapsize": 290784,
- "hwId": "111E35908497A05512E259BB76801E10",
+ "hwId": "00000000000000000000000000000000",
"hw_ver": "1.0",
"is_color": 0,
"is_dimmable": 1,
@@ -52,10 +52,10 @@
"on_off": 1,
"saturation": 0
},
- "mic_mac": "D80D17150474",
+ "mic_mac": "D80D17000000",
"mic_type": "IOT.SMARTBULB",
"model": "KL120(US)",
- "oemId": "1210657CD7FBDC72895644388EEFAE8B",
+ "oemId": "00000000000000000000000000000000",
"preferred_state": [
{
"brightness": 100,
diff --git a/tests/fixtures/iot/KL125(US)_1.20_1.0.5.json b/tests/fixtures/iot/KL125(US)_1.20_1.0.5.json
index 1fca6924..aedcb1f6 100644
--- a/tests/fixtures/iot/KL125(US)_1.20_1.0.5.json
+++ b/tests/fixtures/iot/KL125(US)_1.20_1.0.5.json
@@ -20,7 +20,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "kasa-bc01",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL125(US)_2.0_1.0.7.json b/tests/fixtures/iot/KL125(US)_2.0_1.0.7.json
index b7fa640b..9d19ca57 100644
--- a/tests/fixtures/iot/KL125(US)_2.0_1.0.7.json
+++ b/tests/fixtures/iot/KL125(US)_2.0_1.0.7.json
@@ -22,7 +22,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Test bulb 6",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL130(EU)_1.0_1.8.8.json b/tests/fixtures/iot/KL130(EU)_1.0_1.8.8.json
index f15e3602..ce303462 100644
--- a/tests/fixtures/iot/KL130(EU)_1.0_1.8.8.json
+++ b/tests/fixtures/iot/KL130(EU)_1.0_1.8.8.json
@@ -11,7 +11,7 @@
"stopConnect": 0,
"tcspInfo": "",
"tcspStatus": 1,
- "username": "#MASKED_NAME#"
+ "username": "user@example.com"
},
"get_intl_fw_list": {
"err_code": 0,
diff --git a/tests/fixtures/iot/KL130(US)_1.0_1.8.11.json b/tests/fixtures/iot/KL130(US)_1.0_1.8.11.json
index 3ee4cb2e..d9eaaca1 100644
--- a/tests/fixtures/iot/KL130(US)_1.0_1.8.11.json
+++ b/tests/fixtures/iot/KL130(US)_1.0_1.8.11.json
@@ -21,7 +21,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Bulb2",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL135(US)_1.0_1.0.15.json b/tests/fixtures/iot/KL135(US)_1.0_1.0.15.json
index b6670a7a..38a8805d 100644
--- a/tests/fixtures/iot/KL135(US)_1.0_1.0.15.json
+++ b/tests/fixtures/iot/KL135(US)_1.0_1.0.15.json
@@ -11,7 +11,7 @@
"stopConnect": 0,
"tcspInfo": "",
"tcspStatus": 1,
- "username": "#MASKED_NAME#"
+ "username": "user@example.com"
},
"get_intl_fw_list": {
"err_code": 0,
diff --git a/tests/fixtures/iot/KL135(US)_1.0_1.0.6.json b/tests/fixtures/iot/KL135(US)_1.0_1.0.6.json
index dc0ef45a..be34f9c5 100644
--- a/tests/fixtures/iot/KL135(US)_1.0_1.0.6.json
+++ b/tests/fixtures/iot/KL135(US)_1.0_1.0.6.json
@@ -20,7 +20,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "KL135 Bulb",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL400L5(US)_1.0_1.0.5.json b/tests/fixtures/iot/KL400L5(US)_1.0_1.0.5.json
index 64adf555..1bcd088b 100644
--- a/tests/fixtures/iot/KL400L5(US)_1.0_1.0.5.json
+++ b/tests/fixtures/iot/KL400L5(US)_1.0_1.0.5.json
@@ -10,7 +10,7 @@
"get_sysinfo": {
"LEF": 0,
"active_mode": "none",
- "alias": "Kl400",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL400L5(US)_1.0_1.0.8.json b/tests/fixtures/iot/KL400L5(US)_1.0_1.0.8.json
index a737cd2a..6a15c16c 100644
--- a/tests/fixtures/iot/KL400L5(US)_1.0_1.0.8.json
+++ b/tests/fixtures/iot/KL400L5(US)_1.0_1.0.8.json
@@ -10,7 +10,7 @@
"get_sysinfo": {
"LEF": 0,
"active_mode": "none",
- "alias": "Kl400",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL420L5(US)_1.0_1.0.2.json b/tests/fixtures/iot/KL420L5(US)_1.0_1.0.2.json
index 0d19e794..2d16adba 100644
--- a/tests/fixtures/iot/KL420L5(US)_1.0_1.0.2.json
+++ b/tests/fixtures/iot/KL420L5(US)_1.0_1.0.2.json
@@ -10,7 +10,7 @@
"get_sysinfo": {
"LEF": 1,
"active_mode": "none",
- "alias": "Kl420 test",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL430(UN)_2.0_1.0.8.json b/tests/fixtures/iot/KL430(UN)_2.0_1.0.8.json
index a956575b..8a924c19 100644
--- a/tests/fixtures/iot/KL430(UN)_2.0_1.0.8.json
+++ b/tests/fixtures/iot/KL430(UN)_2.0_1.0.8.json
@@ -10,7 +10,7 @@
"get_sysinfo": {
"LEF": 1,
"active_mode": "none",
- "alias": "Bedroom light strip",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL430(US)_1.0_1.0.10.json b/tests/fixtures/iot/KL430(US)_1.0_1.0.10.json
index 9b6d8413..5bda5762 100644
--- a/tests/fixtures/iot/KL430(US)_1.0_1.0.10.json
+++ b/tests/fixtures/iot/KL430(US)_1.0_1.0.10.json
@@ -23,7 +23,7 @@
"system": {
"get_sysinfo": {
"active_mode": "schedule",
- "alias": "Bedroom Lightstrip",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL430(US)_2.0_1.0.11.json b/tests/fixtures/iot/KL430(US)_2.0_1.0.11.json
index f39c5519..380250ff 100644
--- a/tests/fixtures/iot/KL430(US)_2.0_1.0.11.json
+++ b/tests/fixtures/iot/KL430(US)_2.0_1.0.11.json
@@ -11,7 +11,7 @@
"stopConnect": 0,
"tcspInfo": "",
"tcspStatus": 1,
- "username": "#MASKED_NAME#"
+ "username": "user@example.com"
},
"get_intl_fw_list": {
"err_code": 0,
diff --git a/tests/fixtures/iot/KL430(US)_2.0_1.0.8.json b/tests/fixtures/iot/KL430(US)_2.0_1.0.8.json
index e69a9dc1..c5cf550b 100644
--- a/tests/fixtures/iot/KL430(US)_2.0_1.0.8.json
+++ b/tests/fixtures/iot/KL430(US)_2.0_1.0.8.json
@@ -10,7 +10,7 @@
"get_sysinfo": {
"LEF": 1,
"active_mode": "none",
- "alias": "89 strip",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL430(US)_2.0_1.0.9.json b/tests/fixtures/iot/KL430(US)_2.0_1.0.9.json
index d5f2eafb..2d9f7535 100644
--- a/tests/fixtures/iot/KL430(US)_2.0_1.0.9.json
+++ b/tests/fixtures/iot/KL430(US)_2.0_1.0.9.json
@@ -10,7 +10,7 @@
"get_sysinfo": {
"LEF": 1,
"active_mode": "none",
- "alias": "kl430 updated",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL50(US)_1.0_1.1.13.json b/tests/fixtures/iot/KL50(US)_1.0_1.1.13.json
index f3e43c9a..6e30c136 100644
--- a/tests/fixtures/iot/KL50(US)_1.0_1.1.13.json
+++ b/tests/fixtures/iot/KL50(US)_1.0_1.1.13.json
@@ -22,7 +22,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Kl50",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KL60(UN)_1.0_1.1.4.json b/tests/fixtures/iot/KL60(UN)_1.0_1.1.4.json
index fa842b47..22dadaee 100644
--- a/tests/fixtures/iot/KL60(UN)_1.0_1.1.4.json
+++ b/tests/fixtures/iot/KL60(UN)_1.0_1.1.4.json
@@ -32,7 +32,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "TP-LINK_Smart Bulb_9179",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
@@ -60,7 +60,7 @@
"on_off": 0
},
"longitude_i": 0,
- "mic_mac": "74DA88C89179",
+ "mic_mac": "74DA88000000",
"mic_type": "IOT.SMARTBULB",
"model": "KL60(UN)",
"oemId": "00000000000000000000000000000000",
diff --git a/tests/fixtures/iot/KL60(US)_1.0_1.1.13.json b/tests/fixtures/iot/KL60(US)_1.0_1.1.13.json
index e52cb85c..6834d925 100644
--- a/tests/fixtures/iot/KL60(US)_1.0_1.1.13.json
+++ b/tests/fixtures/iot/KL60(US)_1.0_1.1.13.json
@@ -22,7 +22,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Gold fil",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iot/KP100(US)_3.0_1.0.1.json b/tests/fixtures/iot/KP100(US)_3.0_1.0.1.json
index fb62654b..46e9ec4e 100644
--- a/tests/fixtures/iot/KP100(US)_3.0_1.0.1.json
+++ b/tests/fixtures/iot/KP100(US)_3.0_1.0.1.json
@@ -2,7 +2,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Kasa",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug Mini",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/KP105(UK)_1.0_1.0.5.json b/tests/fixtures/iot/KP105(UK)_1.0_1.0.5.json
index ce194375..91e310d3 100644
--- a/tests/fixtures/iot/KP105(UK)_1.0_1.0.5.json
+++ b/tests/fixtures/iot/KP105(UK)_1.0_1.0.5.json
@@ -11,7 +11,7 @@
"stopConnect": 0,
"tcspInfo": "",
"tcspStatus": 1,
- "username": "#MASKED_NAME#"
+ "username": "user@example.com"
},
"get_intl_fw_list": {
"err_code": -7,
diff --git a/tests/fixtures/iot/KP115(US)_1.0_1.0.17.json b/tests/fixtures/iot/KP115(US)_1.0_1.0.17.json
index afb5a5fe..fb5efac8 100644
--- a/tests/fixtures/iot/KP115(US)_1.0_1.0.17.json
+++ b/tests/fixtures/iot/KP115(US)_1.0_1.0.17.json
@@ -11,7 +11,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Test plug",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug Mini",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/KP125(US)_1.0_1.0.6.json b/tests/fixtures/iot/KP125(US)_1.0_1.0.6.json
index cb32e7c6..2bb0d21e 100644
--- a/tests/fixtures/iot/KP125(US)_1.0_1.0.6.json
+++ b/tests/fixtures/iot/KP125(US)_1.0_1.0.6.json
@@ -11,7 +11,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Test plug",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Wi-Fi Plug Mini",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/KP200(US)_3.0_1.0.3.json b/tests/fixtures/iot/KP200(US)_3.0_1.0.3.json
index fef495d6..40a57fd5 100644
--- a/tests/fixtures/iot/KP200(US)_3.0_1.0.3.json
+++ b/tests/fixtures/iot/KP200(US)_3.0_1.0.3.json
@@ -1,12 +1,12 @@
{
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Smart Plug_C2D6",
+ "alias": "#MASKED_NAME#",
"child_num": 2,
"children": [
{
- "alias": "One ",
- "id": "80066788DFFFD572D9F2E4A5A6847669213E039F00",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -14,8 +14,8 @@
"state": 1
},
{
- "alias": "Two ",
- "id": "80066788DFFFD572D9F2E4A5A6847669213E039F01",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KP303(UK)_1.0_1.0.3.json b/tests/fixtures/iot/KP303(UK)_1.0_1.0.3.json
index d02d766b..b5c6a105 100644
--- a/tests/fixtures/iot/KP303(UK)_1.0_1.0.3.json
+++ b/tests/fixtures/iot/KP303(UK)_1.0_1.0.3.json
@@ -1,12 +1,12 @@
{
"system": {
"get_sysinfo": {
- "alias": "Bedroom Power Strip",
+ "alias": "#MASKED_NAME#",
"child_num": 3,
"children": [
{
- "alias": "Plug 1",
- "id": "8006E9854025B67C3F9D99BA1E66223D1C9A8A7700",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -14,8 +14,8 @@
"state": 1
},
{
- "alias": "Plug 2",
- "id": "8006E9854025B67C3F9D99BA1E66223D1C9A8A7701",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
@@ -23,8 +23,8 @@
"state": 0
},
{
- "alias": "Plug 3",
- "id": "8006E9854025B67C3F9D99BA1E66223D1C9A8A7702",
+ "alias": "#MASKED_NAME# 3",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_3",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KP303(US)_2.0_1.0.3.json b/tests/fixtures/iot/KP303(US)_2.0_1.0.3.json
index 96c2f8c9..a9590557 100644
--- a/tests/fixtures/iot/KP303(US)_2.0_1.0.3.json
+++ b/tests/fixtures/iot/KP303(US)_2.0_1.0.3.json
@@ -1,12 +1,12 @@
{
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Power Strip_BDF6",
+ "alias": "#MASKED_NAME#",
"child_num": 3,
"children": [
{
- "alias": "Plug 1",
- "id": "800681855E0E9AEF096F4891B3DC88C71E59F42E00",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -14,8 +14,8 @@
"state": 0
},
{
- "alias": "Plug 2",
- "id": "800681855E0E9AEF096F4891B3DC88C71E59F42E01",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
@@ -23,8 +23,8 @@
"state": 0
},
{
- "alias": "Plug 3",
- "id": "800681855E0E9AEF096F4891B3DC88C71E59F42E02",
+ "alias": "#MASKED_NAME# 3",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_3",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KP303(US)_2.0_1.0.9.json b/tests/fixtures/iot/KP303(US)_2.0_1.0.9.json
index d500ebb8..333df3f6 100644
--- a/tests/fixtures/iot/KP303(US)_2.0_1.0.9.json
+++ b/tests/fixtures/iot/KP303(US)_2.0_1.0.9.json
@@ -5,8 +5,8 @@
"child_num": 3,
"children": [
{
- "alias": "#MASKED_NAME#",
- "id": "800639AA097730E58235162FCDA301CE1F038F9101",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -14,8 +14,8 @@
"state": 1
},
{
- "alias": "#MASKED_NAME#",
- "id": "800639AA097730E58235162FCDA301CE1F038F9102",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
@@ -23,8 +23,8 @@
"state": 0
},
{
- "alias": "#MASKED_NAME#",
- "id": "800639AA097730E58235162FCDA301CE1F038F9100",
+ "alias": "#MASKED_NAME# 3",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_3",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KP400(US)_1.0_1.0.10.json b/tests/fixtures/iot/KP400(US)_1.0_1.0.10.json
index afdb7bfc..cd09a434 100644
--- a/tests/fixtures/iot/KP400(US)_1.0_1.0.10.json
+++ b/tests/fixtures/iot/KP400(US)_1.0_1.0.10.json
@@ -17,12 +17,12 @@
},
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Smart Plug_2ECE",
+ "alias": "#MASKED_NAME#",
"child_num": 2,
"children": [
{
- "alias": "Rope",
- "id": "00",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"action": 1,
"schd_sec": 69240,
@@ -32,8 +32,8 @@
"state": 0
},
{
- "alias": "Plug 2",
- "id": "01",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KP400(US)_2.0_1.0.6.json b/tests/fixtures/iot/KP400(US)_2.0_1.0.6.json
index 23cd22d1..3f838a91 100644
--- a/tests/fixtures/iot/KP400(US)_2.0_1.0.6.json
+++ b/tests/fixtures/iot/KP400(US)_2.0_1.0.6.json
@@ -1,12 +1,12 @@
{
"system": {
"get_sysinfo": {
- "alias": "TP-LINK_Smart Plug_DC2A",
+ "alias": "#MASKED_NAME#",
"child_num": 2,
"children": [
{
- "alias": "Anc ",
- "id": "8006B8E953CC4149E2B13AA27E0D18EF1DCFBF3400",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -14,8 +14,8 @@
"state": 1
},
{
- "alias": "Plug 2",
- "id": "8006B8E953CC4149E2B13AA27E0D18EF1DCFBF3401",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KP400(US)_3.0_1.0.3.json b/tests/fixtures/iot/KP400(US)_3.0_1.0.3.json
index e93eea8f..ec1c37f3 100644
--- a/tests/fixtures/iot/KP400(US)_3.0_1.0.3.json
+++ b/tests/fixtures/iot/KP400(US)_3.0_1.0.3.json
@@ -5,8 +5,8 @@
"child_num": 2,
"children": [
{
- "alias": "#MASKED_NAME#",
- "id": "8006521377E30159055A751347B5A5E321A8D0A100",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -14,8 +14,8 @@
"state": 1
},
{
- "alias": "#MASKED_NAME#",
- "id": "8006521377E30159055A751347B5A5E321A8D0A101",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KP400(US)_3.0_1.0.4.json b/tests/fixtures/iot/KP400(US)_3.0_1.0.4.json
index 18580f4e..5a60a400 100644
--- a/tests/fixtures/iot/KP400(US)_3.0_1.0.4.json
+++ b/tests/fixtures/iot/KP400(US)_3.0_1.0.4.json
@@ -5,8 +5,8 @@
"child_num": 2,
"children": [
{
- "alias": "#MASKED_NAME#",
- "id": "8006521377E30159055A751347B5A5E321A8D0A100",
+ "alias": "#MASKED_NAME# 1",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_1",
"next_action": {
"type": -1
},
@@ -14,8 +14,8 @@
"state": 0
},
{
- "alias": "#MASKED_NAME#",
- "id": "8006521377E30159055A751347B5A5E321A8D0A101",
+ "alias": "#MASKED_NAME# 2",
+ "id": "SCRUBBED_CHILD_DEVICE_ID_2",
"next_action": {
"type": -1
},
diff --git a/tests/fixtures/iot/KP401(US)_1.0_1.0.0.json b/tests/fixtures/iot/KP401(US)_1.0_1.0.0.json
index 644c4e5f..f3006cf4 100644
--- a/tests/fixtures/iot/KP401(US)_1.0_1.0.0.json
+++ b/tests/fixtures/iot/KP401(US)_1.0_1.0.0.json
@@ -2,7 +2,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Kp401",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Outdoor Plug",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/KP405(US)_1.0_1.0.5.json b/tests/fixtures/iot/KP405(US)_1.0_1.0.5.json
index ad6357f3..806bdc27 100644
--- a/tests/fixtures/iot/KP405(US)_1.0_1.0.5.json
+++ b/tests/fixtures/iot/KP405(US)_1.0_1.0.5.json
@@ -15,7 +15,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Porch Lights",
+ "alias": "#MASKED_NAME#",
"brightness": 50,
"dev_name": "Kasa Smart Wi-Fi Outdoor Plug-In Dimmer",
"deviceId": "0000000000000000000000000000000000000000",
diff --git a/tests/fixtures/iot/KS200(US)_1.0_1.0.8.json b/tests/fixtures/iot/KS200(US)_1.0_1.0.8.json
new file mode 100644
index 00000000..4fc94890
--- /dev/null
+++ b/tests/fixtures/iot/KS200(US)_1.0_1.0.8.json
@@ -0,0 +1,63 @@
+{
+ "cnCloud": {
+ "get_info": {
+ "binded": 1,
+ "cld_connection": 1,
+ "err_code": 0,
+ "fwDlPage": "",
+ "fwNotifyType": -1,
+ "illegalType": 0,
+ "server": "n-devs.tplinkcloud.com",
+ "stopConnect": 0,
+ "tcspInfo": "",
+ "tcspStatus": 1,
+ "username": "user@example.com"
+ },
+ "get_intl_fw_list": {
+ "err_code": 0,
+ "fw_list": []
+ }
+ },
+ "schedule": {
+ "get_next_action": {
+ "err_code": 0,
+ "type": -1
+ },
+ "get_rules": {
+ "enable": 1,
+ "err_code": 0,
+ "rule_list": [],
+ "version": 2
+ }
+ },
+ "system": {
+ "get_sysinfo": {
+ "active_mode": "none",
+ "alias": "#MASKED_NAME#",
+ "dev_name": "Smart Wi-Fi Light Switch",
+ "deviceId": "0000000000000000000000000000000000000000",
+ "err_code": 0,
+ "feature": "TIM",
+ "hwId": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "icon_hash": "",
+ "latitude_i": 0,
+ "led_off": 0,
+ "longitude_i": 0,
+ "mac": "A8:42:A1:00:00:00",
+ "mic_type": "IOT.SMARTPLUGSWITCH",
+ "model": "KS200(US)",
+ "next_action": {
+ "type": -1
+ },
+ "obd_src": "tplink",
+ "oemId": "00000000000000000000000000000000",
+ "on_time": 0,
+ "relay_state": 0,
+ "rssi": -46,
+ "status": "new",
+ "sw_ver": "1.0.8 Build 240424 Rel.101842",
+ "updating": 0
+ }
+ }
+}
diff --git a/tests/fixtures/iot/KS200M(US)_1.0_1.0.10.json b/tests/fixtures/iot/KS200M(US)_1.0_1.0.10.json
index 24acdb97..f9498ae9 100644
--- a/tests/fixtures/iot/KS200M(US)_1.0_1.0.10.json
+++ b/tests/fixtures/iot/KS200M(US)_1.0_1.0.10.json
@@ -11,7 +11,7 @@
"stopConnect": 0,
"tcspInfo": "",
"tcspStatus": 1,
- "username": "#MASKED_NAME#"
+ "username": "user@example.com"
},
"get_intl_fw_list": {
"err_code": 0,
diff --git a/tests/fixtures/iot/KS200M(US)_1.0_1.0.8.json b/tests/fixtures/iot/KS200M(US)_1.0_1.0.8.json
index 3806895b..719dab2e 100644
--- a/tests/fixtures/iot/KS200M(US)_1.0_1.0.8.json
+++ b/tests/fixtures/iot/KS200M(US)_1.0_1.0.8.json
@@ -66,7 +66,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Test KS200M",
+ "alias": "#MASKED_NAME#",
"dev_name": "Smart Light Switch with PIR",
"deviceId": "0000000000000000000000000000000000000000",
"err_code": 0,
diff --git a/tests/fixtures/iot/KS220(US)_1.0_1.0.13.json b/tests/fixtures/iot/KS220(US)_1.0_1.0.13.json
index f5c8c1dd..debdd722 100644
--- a/tests/fixtures/iot/KS220(US)_1.0_1.0.13.json
+++ b/tests/fixtures/iot/KS220(US)_1.0_1.0.13.json
@@ -11,7 +11,7 @@
"stopConnect": 0,
"tcspInfo": "",
"tcspStatus": 1,
- "username": "#MASKED_NAME#"
+ "username": "user@example.com"
},
"get_intl_fw_list": {
"err_code": 0,
diff --git a/tests/fixtures/iot/KS220M(US)_1.0_1.0.4.json b/tests/fixtures/iot/KS220M(US)_1.0_1.0.4.json
index 40da46fd..3dceb322 100644
--- a/tests/fixtures/iot/KS220M(US)_1.0_1.0.4.json
+++ b/tests/fixtures/iot/KS220M(US)_1.0_1.0.4.json
@@ -78,7 +78,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Garage Entryway Lights",
+ "alias": "#MASKED_NAME#",
"brightness": 100,
"dev_name": "Wi-Fi Smart Dimmer with sensor",
"deviceId": "0000000000000000000000000000000000000000",
diff --git a/tests/fixtures/iot/KS230(US)_1.0_1.0.14.json b/tests/fixtures/iot/KS230(US)_1.0_1.0.14.json
index a9e529bc..8876a1af 100644
--- a/tests/fixtures/iot/KS230(US)_1.0_1.0.14.json
+++ b/tests/fixtures/iot/KS230(US)_1.0_1.0.14.json
@@ -14,7 +14,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "Test KS230",
+ "alias": "#MASKED_NAME#",
"brightness": 60,
"dc_state": 0,
"dev_name": "Wi-Fi Smart 3-Way Dimmer",
diff --git a/tests/fixtures/iot/KS230(US)_2.0_1.0.11.json b/tests/fixtures/iot/KS230(US)_2.0_1.0.11.json
new file mode 100644
index 00000000..213f2460
--- /dev/null
+++ b/tests/fixtures/iot/KS230(US)_2.0_1.0.11.json
@@ -0,0 +1,112 @@
+{
+ "cnCloud": {
+ "get_info": {
+ "binded": 1,
+ "cld_connection": 1,
+ "err_code": 0,
+ "fwDlPage": "",
+ "fwNotifyType": -1,
+ "illegalType": 0,
+ "server": "n-devs.tplinkcloud.com",
+ "stopConnect": 0,
+ "tcspInfo": "",
+ "tcspStatus": 1,
+ "username": "user@example.com"
+ },
+ "get_intl_fw_list": {
+ "err_code": 0,
+ "fw_list": []
+ }
+ },
+ "schedule": {
+ "get_next_action": {
+ "err_code": 0,
+ "type": -1
+ },
+ "get_rules": {
+ "enable": 1,
+ "err_code": 0,
+ "rule_list": [],
+ "version": 2
+ }
+ },
+ "smartlife.iot.dimmer": {
+ "get_default_behavior": {
+ "double_click": {
+ "mode": "none"
+ },
+ "err_code": 0,
+ "hard_on": {
+ "mode": "last_status"
+ },
+ "long_press": {
+ "mode": "instant_on_off"
+ },
+ "soft_on": {
+ "mode": "last_status"
+ }
+ },
+ "get_dimmer_parameters": {
+ "bulb_type": 1,
+ "calibration_type": 0,
+ "err_code": 0,
+ "fadeOffTime": 1000,
+ "fadeOnTime": 1000,
+ "gentleOffTime": 10000,
+ "gentleOnTime": 3000,
+ "minThreshold": 11,
+ "rampRate": 30
+ }
+ },
+ "system": {
+ "get_sysinfo": {
+ "active_mode": "none",
+ "alias": "#MASKED_NAME#",
+ "brightness": 100,
+ "dc_state": 0,
+ "dev_name": "Wi-Fi Smart 3-Way Dimmer",
+ "deviceId": "0000000000000000000000000000000000000000",
+ "err_code": 0,
+ "feature": "TIM",
+ "hwId": "00000000000000000000000000000000",
+ "hw_ver": "2.0",
+ "icon_hash": "",
+ "latitude_i": 0,
+ "led_off": 0,
+ "longitude_i": 0,
+ "mac": "5C:E9:31:00:00:00",
+ "mic_type": "IOT.SMARTPLUGSWITCH",
+ "model": "KS230(US)",
+ "next_action": {
+ "type": -1
+ },
+ "ntc_state": 0,
+ "obd_src": "tplink",
+ "oemId": "00000000000000000000000000000000",
+ "on_time": 0,
+ "preferred_state": [
+ {
+ "brightness": 100,
+ "index": 0
+ },
+ {
+ "brightness": 75,
+ "index": 1
+ },
+ {
+ "brightness": 50,
+ "index": 2
+ },
+ {
+ "brightness": 25,
+ "index": 3
+ }
+ ],
+ "relay_state": 0,
+ "rssi": -41,
+ "status": "new",
+ "sw_ver": "1.0.11 Build 240516 Rel.104458",
+ "updating": 0
+ }
+ }
+}
diff --git a/tests/fixtures/iot/LB110(US)_1.0_1.8.11.json b/tests/fixtures/iot/LB110(US)_1.0_1.8.11.json
index ec49e91b..8df62f23 100644
--- a/tests/fixtures/iot/LB110(US)_1.0_1.8.11.json
+++ b/tests/fixtures/iot/LB110(US)_1.0_1.8.11.json
@@ -21,7 +21,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
- "alias": "TP-LINK_Smart Bulb_43EC",
+ "alias": "#MASKED_NAME#",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
diff --git a/tests/fixtures/iotcam/EC60(US)_4.0_2.3.22.json b/tests/fixtures/iotcam/EC60(US)_4.0_2.3.22.json
new file mode 100644
index 00000000..2da0d5f3
--- /dev/null
+++ b/tests/fixtures/iotcam/EC60(US)_4.0_2.3.22.json
@@ -0,0 +1,86 @@
+{
+ "emeter": {
+ "get_realtime": {
+ "err_code": -10008,
+ "err_msg": "Unsupported API call."
+ }
+ },
+ "smartlife.iot.LAS": {
+ "get_current_brt": {
+ "err_code": -10008,
+ "err_msg": "Unsupported API call."
+ }
+ },
+ "smartlife.iot.PIR": {
+ "get_config": {
+ "err_code": -10008,
+ "err_msg": "Unsupported API call."
+ }
+ },
+ "smartlife.iot.common.emeter": {
+ "get_realtime": {
+ "err_code": -10008,
+ "err_msg": "Unsupported API call."
+ }
+ },
+ "smartlife.iot.dimmer": {
+ "get_dimmer_parameters": {
+ "err_code": -10008,
+ "err_msg": "Unsupported API call."
+ }
+ },
+ "smartlife.iot.smartbulb.lightingservice": {
+ "get_light_state": {
+ "err_code": -10008,
+ "err_msg": "Unsupported API call."
+ }
+ },
+ "system": {
+ "get_sysinfo": {
+ "err_code": 0,
+ "system": {
+ "a_type": 2,
+ "alias": "#MASKED_NAME#",
+ "bind_status": false,
+ "c_opt": [
+ 0,
+ 1
+ ],
+ "camera_switch": "on",
+ "dev_name": "Kasa Spot, 24/7 Recording",
+ "deviceId": "0000000000000000000000000000000000000000",
+ "f_list": [
+ 1,
+ 2
+ ],
+ "hwId": "00000000000000000000000000000000",
+ "hw_ver": "4.0",
+ "is_cal": 1,
+ "last_activity_timestamp": 0,
+ "latitude": 0,
+ "led_status": "on",
+ "longitude": 0,
+ "mac": "74:FE:CE:00:00:00",
+ "mic_mac": "74FECE000000",
+ "model": "EC60(US)",
+ "new_feature": [
+ 2,
+ 3,
+ 4,
+ 5,
+ 7,
+ 9
+ ],
+ "oemId": "00000000000000000000000000000000",
+ "resolution": "720P",
+ "rssi": -28,
+ "status": "new",
+ "stream_version": 2,
+ "sw_ver": "2.3.22 Build 20230731 rel.69808",
+ "system_time": 1690827820,
+ "type": "IOT.IPCAMERA",
+ "updating": false
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/serialization/deviceconfig_camera-aes-https.json b/tests/fixtures/serialization/deviceconfig_camera-aes-https.json
index 559e834b..40543d2d 100644
--- a/tests/fixtures/serialization/deviceconfig_camera-aes-https.json
+++ b/tests/fixtures/serialization/deviceconfig_camera-aes-https.json
@@ -1,10 +1,9 @@
{
- "host": "127.0.0.1",
- "timeout": 5,
"connection_type": {
"device_family": "SMART.IPCAMERA",
"encryption_type": "AES",
"https": true
},
- "uses_http": false
+ "host": "127.0.0.1",
+ "timeout": 5
}
diff --git a/tests/fixtures/serialization/deviceconfig_plug-klap.json b/tests/fixtures/serialization/deviceconfig_plug-klap.json
index ef42bb2f..f7891802 100644
--- a/tests/fixtures/serialization/deviceconfig_plug-klap.json
+++ b/tests/fixtures/serialization/deviceconfig_plug-klap.json
@@ -1,11 +1,10 @@
{
- "host": "127.0.0.1",
- "timeout": 5,
"connection_type": {
"device_family": "SMART.TAPOPLUG",
"encryption_type": "KLAP",
"https": false,
"login_version": 2
},
- "uses_http": false
+ "host": "127.0.0.1",
+ "timeout": 5
}
diff --git a/tests/fixtures/serialization/deviceconfig_plug-xor.json b/tests/fixtures/serialization/deviceconfig_plug-xor.json
index 78cc05a9..04e43639 100644
--- a/tests/fixtures/serialization/deviceconfig_plug-xor.json
+++ b/tests/fixtures/serialization/deviceconfig_plug-xor.json
@@ -1,10 +1,9 @@
{
- "host": "127.0.0.1",
- "timeout": 5,
"connection_type": {
"device_family": "IOT.SMARTPLUGSWITCH",
"encryption_type": "XOR",
"https": false
},
- "uses_http": false
+ "host": "127.0.0.1",
+ "timeout": 5
}
diff --git a/tests/fixtures/smart/D100C(US)_1.0_1.1.3.json b/tests/fixtures/smart/D100C(US)_1.0_1.1.3.json
new file mode 100644
index 00000000..25d59860
--- /dev/null
+++ b/tests/fixtures/smart/D100C(US)_1.0_1.1.3.json
@@ -0,0 +1,258 @@
+{
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "wireless",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "led",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ }
+ ]
+ },
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "D100C(US)",
+ "device_type": "SMART.TAPOCHIME",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "40-AE-30-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
+ },
+ "get_auto_update_info": {
+ "enable": true,
+ "random_range": 120,
+ "time": 180
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_device_info": {
+ "avatar": "",
+ "device_id": "0000000000000000000000000000000000000000",
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.3 Build 231221 Rel.154700",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "ip": "127.0.0.123",
+ "lang": "en_US",
+ "latitude": 0,
+ "led_off": 0,
+ "longitude": 0,
+ "mac": "40-AE-30-00-00-00",
+ "model": "D100C",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "region": "America/Chicago",
+ "rssi": -24,
+ "signal_level": 3,
+ "specs": "",
+ "ssid": "I01BU0tFRF9TU0lEIw==",
+ "time_diff": -360,
+ "type": "SMART.TAPOCHIME"
+ },
+ "get_device_time": {
+ "region": "America/Chicago",
+ "time_diff": -360,
+ "timestamp": 1736433406
+ },
+ "get_device_usage": {},
+ "get_fw_download_state": {
+ "auto_upgrade": false,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_latest_fw": {
+ "fw_size": 0,
+ "fw_ver": "1.1.3 Build 231221 Rel.154700",
+ "hw_id": "",
+ "need_to_upgrade": false,
+ "oem_id": "",
+ "release_date": "",
+ "release_note": "",
+ "type": 0
+ },
+ "get_wireless_scan_info": {
+ "ap_list": [
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "start_index": 0,
+ "sum": 9,
+ "wep_supported": false
+ },
+ "qs_component_nego": {
+ "component_list": [
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ }
+ ],
+ "extra_info": {
+ "device_model": "D100C",
+ "device_type": "SMART.TAPOCHIME",
+ "is_klap": true
+ }
+ }
+}
diff --git a/tests/fixtures/smart/EP25(US)_2.6_1.0.1.json b/tests/fixtures/smart/EP25(US)_2.6_1.0.1.json
index 61e12b25..e83c6221 100644
--- a/tests/fixtures/smart/EP25(US)_2.6_1.0.1.json
+++ b/tests/fixtures/smart/EP25(US)_2.6_1.0.1.json
@@ -84,21 +84,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "EP25(US)",
- "device_type": "SMART.KASAPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "00-00-00-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "EP25(US)",
+ "device_type": "SMART.KASAPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "00-00-00-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/EP25(US)_2.6_1.0.2.json b/tests/fixtures/smart/EP25(US)_2.6_1.0.2.json
index 2d3e2e5e..4aebbe0e 100644
--- a/tests/fixtures/smart/EP25(US)_2.6_1.0.2.json
+++ b/tests/fixtures/smart/EP25(US)_2.6_1.0.2.json
@@ -88,21 +88,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "EP25(US)",
- "device_type": "SMART.KASAPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "EP25(US)",
+ "device_type": "SMART.KASAPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/EP40M(US)_1.0_1.1.0.json b/tests/fixtures/smart/EP40M(US)_1.0_1.1.0.json
index 1126fad5..9eef29dc 100644
--- a/tests/fixtures/smart/EP40M(US)_1.0_1.1.0.json
+++ b/tests/fixtures/smart/EP40M(US)_1.0_1.1.0.json
@@ -379,21 +379,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "EP40M(US)",
- "device_type": "SMART.KASAPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-09-0D-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "matter",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "EP40M(US)",
+ "device_type": "SMART.KASAPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-09-0D-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "matter",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": true,
diff --git a/tests/fixtures/smart/H100(EU)_1.0_1.2.3.json b/tests/fixtures/smart/H100(EU)_1.0_1.2.3.json
index 4d4936c6..ba09016a 100644
--- a/tests/fixtures/smart/H100(EU)_1.0_1.2.3.json
+++ b/tests/fixtures/smart/H100(EU)_1.0_1.2.3.json
@@ -84,21 +84,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "H100(EU)",
- "device_type": "SMART.TAPOHUB",
- "factory_default": true,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": ""
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "H100(EU)",
+ "device_type": "SMART.TAPOHUB",
+ "factory_default": true,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": ""
+ }
},
"get_auto_update_info": {
"enable": true,
diff --git a/tests/fixtures/smart/H100(EU)_1.0_1.5.10.json b/tests/fixtures/smart/H100(EU)_1.0_1.5.10.json
index 021309c7..4e0e5258 100644
--- a/tests/fixtures/smart/H100(EU)_1.0_1.5.10.json
+++ b/tests/fixtures/smart/H100(EU)_1.0_1.5.10.json
@@ -96,21 +96,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "H100(EU)",
- "device_type": "SMART.TAPOHUB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "H100(EU)",
+ "device_type": "SMART.TAPOHUB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_alarm_configure": {
"duration": 10,
@@ -469,6 +472,24 @@
"setup_code": "00000000000",
"setup_payload": "00:0000000000000000000"
},
+ "get_scan_child_device_list": {
+ "child_device_list": [
+ {
+ "category": "subg.trigger.temp-hmdt-sensor",
+ "device_id": "REDACTED_1",
+ "device_model": "T315",
+ "name": "REDACTED_1"
+ },
+ {
+ "category": "subg.trigger.contact-sensor",
+ "device_id": "REDACTED_2",
+ "device_model": "T110",
+ "name": "REDACTED_2"
+ }
+ ],
+ "scan_status": "scanning",
+ "scan_wait_time": 28
+ },
"get_support_alarm_type_list": {
"alarm_type_list": [
"Doorbell Ring 1",
diff --git a/tests/fixtures/smart/H100(EU)_1.0_1.5.5.json b/tests/fixtures/smart/H100(EU)_1.0_1.5.5.json
index 639122bd..fadb35d2 100644
--- a/tests/fixtures/smart/H100(EU)_1.0_1.5.5.json
+++ b/tests/fixtures/smart/H100(EU)_1.0_1.5.5.json
@@ -92,21 +92,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "H100(EU)",
- "device_type": "SMART.TAPOHUB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "H100(EU)",
+ "device_type": "SMART.TAPOHUB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_alarm_configure": {
"duration": 10,
@@ -195,7 +198,7 @@
"ver_code": 1
}
],
- "device_id": "0000000000000000000000000000000000000000"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
}
],
"start_index": 0,
@@ -213,7 +216,7 @@
"current_humidity_exception": -34,
"current_temp": 22.2,
"current_temp_exception": 0,
- "device_id": "0000000000000000000000000000000000000000",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
"fw_ver": "1.7.0 Build 230424 Rel.170332",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
diff --git a/tests/fixtures/smart/HS200(US)_5.26_1.0.3.json b/tests/fixtures/smart/HS200(US)_5.26_1.0.3.json
index e67435a9..f17269cc 100644
--- a/tests/fixtures/smart/HS200(US)_5.26_1.0.3.json
+++ b/tests/fixtures/smart/HS200(US)_5.26_1.0.3.json
@@ -80,21 +80,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "HS200(US)",
- "device_type": "SMART.KASASWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "74-FE-CE-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "HS200(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "74-FE-CE-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/HS220(US)_3.26_1.0.1.json b/tests/fixtures/smart/HS220(US)_3.26_1.0.1.json
index 63ec680b..99818984 100644
--- a/tests/fixtures/smart/HS220(US)_3.26_1.0.1.json
+++ b/tests/fixtures/smart/HS220(US)_3.26_1.0.1.json
@@ -100,20 +100,23 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "owner": "00000000000000000000000000000000",
- "device_type": "SMART.KASASWITCH",
- "device_model": "HS220(US)",
- "ip": "127.0.0.123",
- "mac": "24-2F-D0-00-00-00",
- "is_support_iot_cloud": true,
- "obd_src": "tplink",
- "factory_default": false,
- "mgt_encrypt_schm": {
- "is_support_https": false,
- "encrypt_type": "AES",
- "http_port": 80,
- "lv": 2
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "HS220(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "24-2F-D0-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
}
},
"get_antitheft_rules": {
diff --git a/tests/fixtures/smart/KH100(EU)_1.0_1.2.3.json b/tests/fixtures/smart/KH100(EU)_1.0_1.2.3.json
index 4ef13a07..0f24be14 100644
--- a/tests/fixtures/smart/KH100(EU)_1.0_1.2.3.json
+++ b/tests/fixtures/smart/KH100(EU)_1.0_1.2.3.json
@@ -84,21 +84,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KH100(EU)",
- "device_type": "SMART.KASAHUB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "A8-42-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KH100(EU)",
+ "device_type": "SMART.KASAHUB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-42-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_alarm_configure": {
"duration": 300,
diff --git a/tests/fixtures/smart/KH100(EU)_1.0_1.5.12.json b/tests/fixtures/smart/KH100(EU)_1.0_1.5.12.json
index 937fe36c..53684a58 100644
--- a/tests/fixtures/smart/KH100(EU)_1.0_1.5.12.json
+++ b/tests/fixtures/smart/KH100(EU)_1.0_1.5.12.json
@@ -88,21 +88,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KH100(EU)",
- "device_type": "SMART.KASAHUB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "A8-42-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KH100(EU)",
+ "device_type": "SMART.KASAHUB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-42-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_alarm_configure": {
"duration": 300,
diff --git a/tests/fixtures/smart/KH100(UK)_1.0_1.5.6.json b/tests/fixtures/smart/KH100(UK)_1.0_1.5.6.json
index 33e4cec6..c0eeb89b 100644
--- a/tests/fixtures/smart/KH100(UK)_1.0_1.5.6.json
+++ b/tests/fixtures/smart/KH100(UK)_1.0_1.5.6.json
@@ -1,4 +1,4 @@
- {
+{
"component_nego": {
"component_list": [
{
@@ -88,21 +88,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KH100(UK)",
- "device_type": "SMART.KASAHUB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-A7-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KH100(UK)",
+ "device_type": "SMART.KASAHUB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_alarm_configure": {
"duration": 300,
diff --git a/tests/fixtures/smart/KP125M(US)_1.0_1.1.3.json b/tests/fixtures/smart/KP125M(US)_1.0_1.1.3.json
index c7b6ecb9..41a34cb3 100644
--- a/tests/fixtures/smart/KP125M(US)_1.0_1.1.3.json
+++ b/tests/fixtures/smart/KP125M(US)_1.0_1.1.3.json
@@ -84,21 +84,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KP125M(US)",
- "device_type": "SMART.KASAPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "00-00-00-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KP125M(US)",
+ "device_type": "SMART.KASAPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "00-00-00-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_current_power": {
"current_power": 17
@@ -124,7 +127,7 @@
"longitude": 0,
"mac": "00-00-00-00-00-00",
"model": "KP125M",
- "nickname": "IyNNQVNLRUROQU1FIyM=",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"on_time": 5332,
"overheated": false,
@@ -133,7 +136,7 @@
"rssi": -62,
"signal_level": 2,
"specs": "",
- "ssid": "IyNNQVNLRUROQU1FIyM=",
+ "ssid": "I01BU0tFRF9TU0lEIw==",
"time_diff": -360,
"type": "SMART.KASAPLUG"
},
diff --git a/tests/fixtures/smart/KP125M(US)_1.0_1.2.3.json b/tests/fixtures/smart/KP125M(US)_1.0_1.2.3.json
index 710febeb..9878b65b 100644
--- a/tests/fixtures/smart/KP125M(US)_1.0_1.2.3.json
+++ b/tests/fixtures/smart/KP125M(US)_1.0_1.2.3.json
@@ -88,21 +88,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KP125M(US)",
- "device_type": "SMART.KASAPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "78-8C-B5-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KP125M(US)",
+ "device_type": "SMART.KASAPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "78-8C-B5-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/KS205(US)_1.0_1.0.2.json b/tests/fixtures/smart/KS205(US)_1.0_1.0.2.json
index c94d4f2a..60611f33 100644
--- a/tests/fixtures/smart/KS205(US)_1.0_1.0.2.json
+++ b/tests/fixtures/smart/KS205(US)_1.0_1.0.2.json
@@ -80,21 +80,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KS205(US)",
- "device_type": "SMART.KASASWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "00-00-00-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KS205(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "00-00-00-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/KS205(US)_1.0_1.1.0.json b/tests/fixtures/smart/KS205(US)_1.0_1.1.0.json
index f9ac5af9..9f7419ec 100644
--- a/tests/fixtures/smart/KS205(US)_1.0_1.1.0.json
+++ b/tests/fixtures/smart/KS205(US)_1.0_1.1.0.json
@@ -76,21 +76,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KS205(US)",
- "device_type": "SMART.KASASWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "40-ED-00-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": ""
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KS205(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "40-ED-00-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": ""
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/KS225(US)_1.0_1.0.2.json b/tests/fixtures/smart/KS225(US)_1.0_1.0.2.json
index e6945cb8..1f2d9d2b 100644
--- a/tests/fixtures/smart/KS225(US)_1.0_1.0.2.json
+++ b/tests/fixtures/smart/KS225(US)_1.0_1.0.2.json
@@ -96,21 +96,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KS225(US)",
- "device_type": "SMART.KASASWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "00-00-00-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KS225(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "00-00-00-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/KS225(US)_1.0_1.1.0.json b/tests/fixtures/smart/KS225(US)_1.0_1.1.0.json
index 798642d3..61ead929 100644
--- a/tests/fixtures/smart/KS225(US)_1.0_1.1.0.json
+++ b/tests/fixtures/smart/KS225(US)_1.0_1.1.0.json
@@ -96,21 +96,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KS225(US)",
- "device_type": "SMART.KASASWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": ""
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KS225(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": ""
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/KS240(US)_1.0_1.0.4.json b/tests/fixtures/smart/KS240(US)_1.0_1.0.4.json
index 2775ee7c..15092b85 100644
--- a/tests/fixtures/smart/KS240(US)_1.0_1.0.4.json
+++ b/tests/fixtures/smart/KS240(US)_1.0_1.0.4.json
@@ -414,21 +414,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KS240(US)",
- "device_type": "SMART.KASASWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-A7-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KS240(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": false,
diff --git a/tests/fixtures/smart/KS240(US)_1.0_1.0.5.json b/tests/fixtures/smart/KS240(US)_1.0_1.0.5.json
index 6d14f7bf..fb6c667d 100644
--- a/tests/fixtures/smart/KS240(US)_1.0_1.0.5.json
+++ b/tests/fixtures/smart/KS240(US)_1.0_1.0.5.json
@@ -104,21 +104,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KS240(US)",
- "device_type": "SMART.KASASWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-A7-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KS240(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": false,
@@ -206,7 +209,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000001"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
},
{
"component_list": [
@@ -267,7 +270,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000000"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2"
}
],
"start_index": 0,
@@ -279,7 +282,7 @@
"avatar": "switch_ks240",
"bind_count": 1,
"category": "kasa.switch.outlet.sub-fan",
- "device_id": "000000000000000000000000000000000000000000",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
"device_on": true,
"fan_sleep_mode_on": false,
"fan_speed_level": 1,
@@ -317,7 +320,7 @@
],
"type": "last_states"
},
- "device_id": "000000000000000000000000000000000000000001",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
"device_on": false,
"fade_off_time": 1,
"fade_on_time": 1,
diff --git a/tests/fixtures/smart/KS240(US)_1.0_1.0.7.json b/tests/fixtures/smart/KS240(US)_1.0_1.0.7.json
index a3f28309..4630a977 100644
--- a/tests/fixtures/smart/KS240(US)_1.0_1.0.7.json
+++ b/tests/fixtures/smart/KS240(US)_1.0_1.0.7.json
@@ -425,21 +425,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "KS240(US)",
- "device_type": "SMART.KASASWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-A7-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "KS240(US)",
+ "device_type": "SMART.KASASWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": true,
diff --git a/tests/fixtures/smart/L510B(EU)_3.0_1.0.5.json b/tests/fixtures/smart/L510B(EU)_3.0_1.0.5.json
index a53e93bb..f89dfc69 100644
--- a/tests/fixtures/smart/L510B(EU)_3.0_1.0.5.json
+++ b/tests/fixtures/smart/L510B(EU)_3.0_1.0.5.json
@@ -80,21 +80,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L510B(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "5C-E9-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L510B(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "5C-E9-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L510E(US)_3.0_1.0.5.json b/tests/fixtures/smart/L510E(US)_3.0_1.0.5.json
index 9a51ea45..a81222e4 100644
--- a/tests/fixtures/smart/L510E(US)_3.0_1.0.5.json
+++ b/tests/fixtures/smart/L510E(US)_3.0_1.0.5.json
@@ -80,21 +80,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L510E(US)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L510E(US)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L510E(US)_3.0_1.1.2.json b/tests/fixtures/smart/L510E(US)_3.0_1.1.2.json
index 055674d2..523d4992 100644
--- a/tests/fixtures/smart/L510E(US)_3.0_1.1.2.json
+++ b/tests/fixtures/smart/L510E(US)_3.0_1.1.2.json
@@ -88,21 +88,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L510E(US)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L510E(US)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L530E(EU)_3.0_1.0.6.json b/tests/fixtures/smart/L530E(EU)_3.0_1.0.6.json
index 10b9d300..05c04522 100644
--- a/tests/fixtures/smart/L530E(EU)_3.0_1.0.6.json
+++ b/tests/fixtures/smart/L530E(EU)_3.0_1.0.6.json
@@ -100,21 +100,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L530E(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "5C-E9-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L530E(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "5C-E9-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L530E(EU)_3.0_1.1.0.json b/tests/fixtures/smart/L530E(EU)_3.0_1.1.0.json
index b5b90d32..a32c0463 100644
--- a/tests/fixtures/smart/L530E(EU)_3.0_1.1.0.json
+++ b/tests/fixtures/smart/L530E(EU)_3.0_1.1.0.json
@@ -104,21 +104,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L530E(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "5C-E9-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L530E(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "5C-E9-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L530E(EU)_3.0_1.1.6.json b/tests/fixtures/smart/L530E(EU)_3.0_1.1.6.json
index 0e0ad2fa..8da76d78 100644
--- a/tests/fixtures/smart/L530E(EU)_3.0_1.1.6.json
+++ b/tests/fixtures/smart/L530E(EU)_3.0_1.1.6.json
@@ -104,21 +104,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L530E(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "5C-E9-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L530E(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "5C-E9-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
@@ -175,7 +178,7 @@
"longitude": 0,
"mac": "5C-E9-31-00-00-00",
"model": "L530",
- "nickname": "TGl2aW5nIFJvb20gQnVsYg==",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"overheated": false,
"region": "Europe/Berlin",
diff --git a/tests/fixtures/smart/L530E(US)_2.0_1.1.0.json b/tests/fixtures/smart/L530E(US)_2.0_1.1.0.json
index 6dac1048..0c80d3a5 100644
--- a/tests/fixtures/smart/L530E(US)_2.0_1.1.0.json
+++ b/tests/fixtures/smart/L530E(US)_2.0_1.1.0.json
@@ -104,21 +104,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L530E(US)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "5C-62-8B-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L530E(US)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "5C-62-8B-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L630(EU)_1.0_1.1.2.json b/tests/fixtures/smart/L630(EU)_1.0_1.1.2.json
index 4ca91c9b..3fb263be 100644
--- a/tests/fixtures/smart/L630(EU)_1.0_1.1.2.json
+++ b/tests/fixtures/smart/L630(EU)_1.0_1.1.2.json
@@ -104,21 +104,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L630(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "40-AE-30-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L630(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "40-AE-30-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L900-10(EU)_1.0_1.0.17.json b/tests/fixtures/smart/L900-10(EU)_1.0_1.0.17.json
index 5d05bc94..816cf896 100644
--- a/tests/fixtures/smart/L900-10(EU)_1.0_1.0.17.json
+++ b/tests/fixtures/smart/L900-10(EU)_1.0_1.0.17.json
@@ -104,21 +104,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L900-10(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-A7-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L900-10(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L900-10(US)_1.0_1.0.11.json b/tests/fixtures/smart/L900-10(US)_1.0_1.0.11.json
index 8665c8f3..5c81fd32 100644
--- a/tests/fixtures/smart/L900-10(US)_1.0_1.0.11.json
+++ b/tests/fixtures/smart/L900-10(US)_1.0_1.0.11.json
@@ -100,20 +100,23 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L900-10(US)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "54-AF-97-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L900-10(US)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "54-AF-97-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L900-5(EU)_1.0_1.0.17.json b/tests/fixtures/smart/L900-5(EU)_1.0_1.0.17.json
index a281f2ec..7c7ac420 100644
--- a/tests/fixtures/smart/L900-5(EU)_1.0_1.0.17.json
+++ b/tests/fixtures/smart/L900-5(EU)_1.0_1.0.17.json
@@ -104,21 +104,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L900-5(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "A8-42-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L900-5(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-42-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L900-5(EU)_1.0_1.1.0.json b/tests/fixtures/smart/L900-5(EU)_1.0_1.1.0.json
index 136d3a0f..98980a4c 100644
--- a/tests/fixtures/smart/L900-5(EU)_1.0_1.1.0.json
+++ b/tests/fixtures/smart/L900-5(EU)_1.0_1.1.0.json
@@ -108,21 +108,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L900-5(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "A8-42-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L900-5(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-42-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L920-5(EU)_1.0_1.0.7.json b/tests/fixtures/smart/L920-5(EU)_1.0_1.0.7.json
index a55707ae..3315b19b 100644
--- a/tests/fixtures/smart/L920-5(EU)_1.0_1.0.7.json
+++ b/tests/fixtures/smart/L920-5(EU)_1.0_1.0.7.json
@@ -104,20 +104,23 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L920-5(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": true,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "1C-61-B4-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false
- },
- "obd_src": "tplink",
- "owner": ""
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L920-5(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": true,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "1C-61-B4-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false
+ },
+ "obd_src": "tplink",
+ "owner": ""
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L920-5(EU)_1.0_1.1.3.json b/tests/fixtures/smart/L920-5(EU)_1.0_1.1.3.json
index 5f03b5b6..0f845bf3 100644
--- a/tests/fixtures/smart/L920-5(EU)_1.0_1.1.3.json
+++ b/tests/fixtures/smart/L920-5(EU)_1.0_1.1.3.json
@@ -116,21 +116,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L920-5(EU)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "B4-B0-24-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L920-5(EU)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "B4-B0-24-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L920-5(US)_1.0_1.1.0.json b/tests/fixtures/smart/L920-5(US)_1.0_1.1.0.json
index 2ea0c69f..95e8f969 100644
--- a/tests/fixtures/smart/L920-5(US)_1.0_1.1.0.json
+++ b/tests/fixtures/smart/L920-5(US)_1.0_1.1.0.json
@@ -112,21 +112,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L920-5(US)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "00-00-00-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L920-5(US)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "00-00-00-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L920-5(US)_1.0_1.1.3.json b/tests/fixtures/smart/L920-5(US)_1.0_1.1.3.json
index 5463944d..992f6399 100644
--- a/tests/fixtures/smart/L920-5(US)_1.0_1.1.3.json
+++ b/tests/fixtures/smart/L920-5(US)_1.0_1.1.3.json
@@ -116,21 +116,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L920-5(US)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "34-60-F9-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L920-5(US)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "34-60-F9-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/L930-5(US)_1.0_1.1.2.json b/tests/fixtures/smart/L930-5(US)_1.0_1.1.2.json
index de7ae2c7..c374ebc5 100644
--- a/tests/fixtures/smart/L930-5(US)_1.0_1.1.2.json
+++ b/tests/fixtures/smart/L930-5(US)_1.0_1.1.2.json
@@ -124,21 +124,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "L930-5(US)",
- "device_type": "SMART.TAPOBULB",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "L930-5(US)",
+ "device_type": "SMART.TAPOBULB",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P100(US)_1.0.0_1.1.3.json b/tests/fixtures/smart/P100(US)_1.0.0_1.1.3.json
index 337c6f2c..2ae738cd 100644
--- a/tests/fixtures/smart/P100(US)_1.0.0_1.1.3.json
+++ b/tests/fixtures/smart/P100(US)_1.0.0_1.1.3.json
@@ -56,18 +56,21 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P100",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "mac": "1C-3B-F3-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false
- },
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P100",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "mac": "1C-3B-F3-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false
+ },
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
@@ -93,7 +96,7 @@
"hw_ver": "1.0.0",
"ip": "127.0.0.123",
"latitude": 0,
- "location": "hallway",
+ "location": "#MASKED_NAME#",
"longitude": 0,
"mac": "1C-3B-F3-00-00-00",
"model": "P100",
diff --git a/tests/fixtures/smart/P100(US)_1.0.0_1.3.7.json b/tests/fixtures/smart/P100(US)_1.0.0_1.3.7.json
index cdddc72e..5347d070 100644
--- a/tests/fixtures/smart/P100(US)_1.0.0_1.3.7.json
+++ b/tests/fixtures/smart/P100(US)_1.0.0_1.3.7.json
@@ -64,20 +64,23 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P100",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "CC-32-E5-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P100",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "CC-32-E5-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
@@ -108,7 +111,7 @@
"ip": "127.0.0.123",
"lang": "en_US",
"latitude": 0,
- "location": "bedroom",
+ "location": "#MASKED_NAME#",
"longitude": 0,
"mac": "CC-32-E5-00-00-00",
"model": "P100",
diff --git a/tests/fixtures/smart/P100(US)_1.0.0_1.4.0.json b/tests/fixtures/smart/P100(US)_1.0.0_1.4.0.json
index 5ec33343..ab75faf5 100644
--- a/tests/fixtures/smart/P100(US)_1.0.0_1.4.0.json
+++ b/tests/fixtures/smart/P100(US)_1.0.0_1.4.0.json
@@ -64,20 +64,23 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P100",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "74-DA-88-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P100",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "74-DA-88-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P110(EU)_1.0_1.0.7.json b/tests/fixtures/smart/P110(EU)_1.0_1.0.7.json
index 6332f259..dd7a0360 100644
--- a/tests/fixtures/smart/P110(EU)_1.0_1.0.7.json
+++ b/tests/fixtures/smart/P110(EU)_1.0_1.0.7.json
@@ -72,19 +72,22 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P110(EU)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "34-60-F9-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false
- },
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P110(EU)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "34-60-F9-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false
+ },
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P110(EU)_1.0_1.2.3.json b/tests/fixtures/smart/P110(EU)_1.0_1.2.3.json
index 415e8ce6..62e580fc 100644
--- a/tests/fixtures/smart/P110(EU)_1.0_1.2.3.json
+++ b/tests/fixtures/smart/P110(EU)_1.0_1.2.3.json
@@ -80,21 +80,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P110(EU)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "48-22-54-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P110(EU)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "48-22-54-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P110(UK)_1.0_1.3.0.json b/tests/fixtures/smart/P110(UK)_1.0_1.3.0.json
index 339c5fb2..0c7f6e83 100644
--- a/tests/fixtures/smart/P110(UK)_1.0_1.3.0.json
+++ b/tests/fixtures/smart/P110(UK)_1.0_1.3.0.json
@@ -88,21 +88,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P110(UK)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "48-22-54-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P110(UK)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "48-22-54-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P110M(AU)_1.0_1.2.3.json b/tests/fixtures/smart/P110M(AU)_1.0_1.2.3.json
index efb88c85..2fea4379 100644
--- a/tests/fixtures/smart/P110M(AU)_1.0_1.2.3.json
+++ b/tests/fixtures/smart/P110M(AU)_1.0_1.2.3.json
@@ -96,21 +96,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P110M(AU)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-09-0D-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P110M(AU)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-09-0D-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_off_config": {
"delay_min": 120,
@@ -124,19 +127,6 @@
"get_connect_cloud_state": {
"status": 1
},
- "get_energy_usage": {
- "today_runtime": 306,
- "month_runtime": 12572,
- "today_energy": 173,
- "month_energy": 6110,
- "local_time": "2024-11-22 21:03:25",
- "electricity_charge": [
- 0,
- 0,
- 0
- ],
- "current_power": 74116
- },
"get_current_power": {
"current_power": 74
},
@@ -313,6 +303,19 @@
},
"type": "constant"
},
+ "get_energy_usage": {
+ "current_power": 74116,
+ "electricity_charge": [
+ 0,
+ 0,
+ 0
+ ],
+ "local_time": "2024-11-22 21:03:25",
+ "month_energy": 6110,
+ "month_runtime": 12572,
+ "today_energy": 173,
+ "today_runtime": 306
+ },
"get_fw_download_state": {
"auto_upgrade": false,
"download_progress": 0,
diff --git a/tests/fixtures/smart/P110M(EU)_1.0_1.2.3.json b/tests/fixtures/smart/P110M(EU)_1.0_1.2.3.json
index d8453319..81174d7b 100644
--- a/tests/fixtures/smart/P110M(EU)_1.0_1.2.3.json
+++ b/tests/fixtures/smart/P110M(EU)_1.0_1.2.3.json
@@ -96,21 +96,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P110M(EU)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-A7-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "matter",
- "owner": ""
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P110M(EU)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "matter",
+ "owner": ""
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P115(EU)_1.0_1.2.3.json b/tests/fixtures/smart/P115(EU)_1.0_1.2.3.json
index 48cd46f2..33d7465c 100644
--- a/tests/fixtures/smart/P115(EU)_1.0_1.2.3.json
+++ b/tests/fixtures/smart/P115(EU)_1.0_1.2.3.json
@@ -80,21 +80,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P115(EU)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": true,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "A8-42-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": ""
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P115(EU)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": true,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-42-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": ""
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P115(US)_1.0_1.1.3.json b/tests/fixtures/smart/P115(US)_1.0_1.1.3.json
new file mode 100644
index 00000000..151f7300
--- /dev/null
+++ b/tests/fixtures/smart/P115(US)_1.0_1.1.3.json
@@ -0,0 +1,643 @@
+{
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "wireless",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "led",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "energy_monitoring",
+ "ver_code": 2
+ },
+ {
+ "id": "power_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "charging_protection",
+ "ver_code": 2
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "current_protection",
+ "ver_code": 1
+ }
+ ]
+ },
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P115(US)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "B0-19-21-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_auto_off_config": {
+ "delay_min": 120,
+ "enable": false
+ },
+ "get_auto_update_info": {
+ "enable": true,
+ "random_range": 120,
+ "time": 180
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_current_power": {
+ "current_power": 0
+ },
+ "get_device_info": {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "charging_status": "normal",
+ "default_states": {
+ "state": {},
+ "type": "last_states"
+ },
+ "device_id": "0000000000000000000000000000000000000000",
+ "device_on": false,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.3 Build 240523 Rel.175054",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "ip": "127.0.0.123",
+ "lang": "en_US",
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "B0-19-21-00-00-00",
+ "model": "P115",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 0,
+ "overcurrent_status": "normal",
+ "overheat_status": "normal",
+ "power_protection_status": "normal",
+ "region": "America/Indiana/Indianapolis",
+ "rssi": -54,
+ "signal_level": 2,
+ "specs": "US",
+ "ssid": "I01BU0tFRF9TU0lEIw==",
+ "time_diff": -300,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_time": {
+ "region": "America/Indiana/Indianapolis",
+ "time_diff": -300,
+ "timestamp": 1733673137
+ },
+ "get_device_usage": {
+ "power_usage": {
+ "past30": 4376,
+ "past7": 1879,
+ "today": 0
+ },
+ "saved_power": {
+ "past30": 8618,
+ "past7": 69,
+ "today": 0
+ },
+ "time_usage": {
+ "past30": 12994,
+ "past7": 1948,
+ "today": 0
+ }
+ },
+ "get_electricity_price_config": {
+ "constant_price": 0,
+ "time_of_use_config": {
+ "summer": {
+ "midpeak": 0,
+ "offpeak": 0,
+ "onpeak": 0,
+ "period": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "weekday_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "weekend_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ },
+ "winter": {
+ "midpeak": 0,
+ "offpeak": 0,
+ "onpeak": 0,
+ "period": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "weekday_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "weekend_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+ },
+ "type": "constant"
+ },
+ "get_emeter_data": {
+ "current_ma": 30,
+ "energy_wh": 1465,
+ "power_mw": 0,
+ "voltage_mv": 122133
+ },
+ "get_emeter_vgain_igain": {
+ "igain": 11101,
+ "vgain": 125071
+ },
+ "get_energy_usage": {
+ "current_power": 0,
+ "electricity_charge": [
+ 0,
+ 0,
+ 0
+ ],
+ "local_time": "2024-12-08 10:52:19",
+ "month_energy": 2532,
+ "month_runtime": 2630,
+ "today_energy": 0,
+ "today_runtime": 0
+ },
+ "get_fw_download_state": {
+ "auto_upgrade": false,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_latest_fw": {
+ "fw_size": 0,
+ "fw_ver": "1.1.3 Build 240523 Rel.175054",
+ "hw_id": "",
+ "need_to_upgrade": false,
+ "oem_id": "",
+ "release_date": "",
+ "release_note": "",
+ "type": 0
+ },
+ "get_led_info": {
+ "bri_config": {
+ "bri_type": "overall",
+ "overall_bri": 50
+ },
+ "led_rule": "always",
+ "led_status": false,
+ "night_mode": {
+ "end_time": 476,
+ "night_mode_type": "sunrise_sunset",
+ "start_time": 1040,
+ "sunrise_offset": 0,
+ "sunset_offset": 0
+ }
+ },
+ "get_max_power": {
+ "max_power": 1934
+ },
+ "get_next_event": {},
+ "get_protection_power": {
+ "enabled": false,
+ "protection_power": 0
+ },
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ },
+ "get_wireless_scan_info": {
+ "ap_list": [
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 0,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 0,
+ "key_type": "none",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "start_index": 0,
+ "sum": 25,
+ "wep_supported": false
+ },
+ "qs_component_nego": {
+ "component_list": [
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ }
+ ],
+ "extra_info": {
+ "device_model": "P115",
+ "device_type": "SMART.TAPOPLUG",
+ "is_klap": true
+ }
+ }
+}
diff --git a/tests/fixtures/smart/P125M(US)_1.0_1.1.0.json b/tests/fixtures/smart/P125M(US)_1.0_1.1.0.json
index 78e876d7..1e0cf7e2 100644
--- a/tests/fixtures/smart/P125M(US)_1.0_1.1.0.json
+++ b/tests/fixtures/smart/P125M(US)_1.0_1.1.0.json
@@ -80,21 +80,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P125M(US)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "48-22-54-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P125M(US)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "48-22-54-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P135(US)_1.0_1.0.5.json b/tests/fixtures/smart/P135(US)_1.0_1.0.5.json
index 9f6c3b03..f1099cc7 100644
--- a/tests/fixtures/smart/P135(US)_1.0_1.0.5.json
+++ b/tests/fixtures/smart/P135(US)_1.0_1.0.5.json
@@ -96,21 +96,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P135(US)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "F0-A7-31-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P135(US)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/P135(US)_1.0_1.2.0.json b/tests/fixtures/smart/P135(US)_1.0_1.2.0.json
new file mode 100644
index 00000000..ec193037
--- /dev/null
+++ b/tests/fixtures/smart/P135(US)_1.0_1.2.0.json
@@ -0,0 +1,419 @@
+{
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "wireless",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "led",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "brightness",
+ "ver_code": 1
+ },
+ {
+ "id": "preset",
+ "ver_code": 1
+ },
+ {
+ "id": "on_off_gradually",
+ "ver_code": 2
+ },
+ {
+ "id": "dimmer_calibration",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "matter",
+ "ver_code": 2
+ }
+ ]
+ },
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P135(US)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-09-0D-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000",
+ "protocol_version": 1
+ }
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_auto_update_info": {
+ "enable": true,
+ "random_range": 120,
+ "time": 180
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_device_info": {
+ "accessory_at_low_battery": false,
+ "avatar": "plug",
+ "brightness": 100,
+ "default_states": {
+ "re_power_type": "always_off",
+ "re_power_type_capability": [
+ "last_states",
+ "always_on",
+ "always_off"
+ ],
+ "type": "last_states"
+ },
+ "device_id": "0000000000000000000000000000000000000000",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.2.0 Build 240415 Rel.171222",
+ "has_set_location_info": false,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "ip": "127.0.0.123",
+ "lang": "",
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "F0-09-0D-00-00-00",
+ "model": "P135",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 3428,
+ "overheat_status": "normal",
+ "region": "America/Los_Angeles",
+ "rssi": -35,
+ "signal_level": 3,
+ "specs": "",
+ "ssid": "I01BU0tFRF9TU0lEIw==",
+ "time_diff": -480,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_time": {
+ "region": "America/Los_Angeles",
+ "time_diff": -480,
+ "timestamp": 1734735856
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 57,
+ "past7": 57,
+ "today": 57
+ }
+ },
+ "get_fw_download_state": {
+ "auto_upgrade": false,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_inherit_info": null,
+ "get_latest_fw": {
+ "fw_size": 0,
+ "fw_ver": "1.2.0 Build 240415 Rel.171222",
+ "hw_id": "",
+ "need_to_upgrade": false,
+ "oem_id": "",
+ "release_date": "",
+ "release_note": "",
+ "type": 0
+ },
+ "get_led_info": {
+ "bri_config": {
+ "bri_type": "overall",
+ "overall_bri": 50
+ },
+ "led_rule": "always",
+ "led_status": true,
+ "night_mode": {
+ "end_time": 420,
+ "night_mode_type": "custom",
+ "start_time": 1320
+ }
+ },
+ "get_matter_setup_info": {
+ "setup_code": "00000000000",
+ "setup_payload": "00:0000000000000000000"
+ },
+ "get_next_event": {},
+ "get_on_off_gradually_info": {
+ "off_state": {
+ "duration": 1,
+ "enable": true,
+ "max_duration": 60
+ },
+ "on_state": {
+ "duration": 1,
+ "enable": true,
+ "max_duration": 60
+ }
+ },
+ "get_preset_rules": {
+ "brightness": [
+ 100,
+ 75,
+ 50,
+ 25,
+ 1
+ ]
+ },
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ },
+ "get_wireless_scan_info": {
+ "ap_list": [
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "start_index": 0,
+ "sum": 15,
+ "wep_supported": false
+ },
+ "qs_component_nego": {
+ "component_list": [
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "matter",
+ "ver_code": 2
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ }
+ ],
+ "extra_info": {
+ "device_model": "P135",
+ "device_type": "SMART.TAPOPLUG",
+ "is_klap": true
+ }
+ }
+}
diff --git a/tests/fixtures/smart/P210M(US)_1.0_1.0.3.json b/tests/fixtures/smart/P210M(US)_1.0_1.0.3.json
new file mode 100644
index 00000000..61ac4762
--- /dev/null
+++ b/tests/fixtures/smart/P210M(US)_1.0_1.0.3.json
@@ -0,0 +1,1585 @@
+{
+ "child_devices": {
+ "SCRUBBED_CHILD_DEVICE_ID_1": {
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ },
+ {
+ "id": "energy_monitoring",
+ "ver_code": 2
+ },
+ {
+ "id": "current_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "power_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "charging_protection",
+ "ver_code": 2
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_auto_off_config": {
+ "delay_min": 120,
+ "enable": false
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_current_power": {
+ "current_power": 68
+ },
+ "get_device_info": {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "charging_status": "normal",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.0.3 Build 240703 Rel.114246",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "DC6279000000",
+ "model": "P210M",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 170204,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overcurrent_status": "normal",
+ "overheat_status": "normal",
+ "position": 1,
+ "power_protection_status": "normal",
+ "protection_enabled": false,
+ "protection_power": 0,
+ "region": "America/Los_Angeles",
+ "slot_number": 2,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_usage": {
+ "power_usage": {
+ "past30": 275,
+ "past7": 275,
+ "today": 168
+ },
+ "saved_power": {
+ "past30": 3289,
+ "past7": 3289,
+ "today": 745
+ },
+ "time_usage": {
+ "past30": 3564,
+ "past7": 3564,
+ "today": 913
+ }
+ },
+ "get_electricity_price_config": {
+ "constant_price": 0,
+ "time_of_use_config": {
+ "summer": {
+ "midpeak": 0,
+ "offpeak": 0,
+ "onpeak": 0,
+ "period": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "weekday_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "weekend_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ },
+ "winter": {
+ "midpeak": 0,
+ "offpeak": 0,
+ "onpeak": 0,
+ "period": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "weekday_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "weekend_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+ },
+ "type": "constant"
+ },
+ "get_emeter_data": {
+ "current_ma": 589,
+ "energy_wh": 249,
+ "power_mw": 68325,
+ "voltage_mv": 120254
+ },
+ "get_emeter_vgain_igain": {
+ "data": [
+ {
+ "igain": 3788,
+ "slot_id": 0,
+ "vgain": 30382
+ },
+ {
+ "igain": 3833,
+ "slot_id": 1,
+ "vgain": 30253
+ }
+ ]
+ },
+ "get_energy_usage": {
+ "electricity_charge": [
+ 0,
+ 0,
+ 0
+ ],
+ "local_time": "2024-12-20 15:13:58",
+ "month_energy": 275,
+ "month_runtime": 3564,
+ "today_energy": 168,
+ "today_runtime": 913
+ },
+ "get_max_power": {
+ "max_power": 1835
+ },
+ "get_next_event": {},
+ "get_protection_power": {
+ "enabled": false,
+ "protection_power": 0
+ },
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ }
+ },
+ "SCRUBBED_CHILD_DEVICE_ID_2": {
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ },
+ {
+ "id": "energy_monitoring",
+ "ver_code": 2
+ },
+ {
+ "id": "current_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "power_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "charging_protection",
+ "ver_code": 2
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_auto_off_config": {
+ "delay_min": 120,
+ "enable": false
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_current_power": {
+ "current_power": 0
+ },
+ "get_device_info": {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "charging_status": "normal",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.0.3 Build 240703 Rel.114246",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "DC6279000000",
+ "model": "P210M",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 170204,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overcurrent_status": "normal",
+ "overheat_status": "normal",
+ "position": 2,
+ "power_protection_status": "normal",
+ "protection_enabled": false,
+ "protection_power": 0,
+ "region": "America/Los_Angeles",
+ "slot_number": 2,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_usage": {
+ "power_usage": {
+ "past30": 0,
+ "past7": 0,
+ "today": 0
+ },
+ "saved_power": {
+ "past30": 3564,
+ "past7": 3564,
+ "today": 913
+ },
+ "time_usage": {
+ "past30": 3564,
+ "past7": 3564,
+ "today": 913
+ }
+ },
+ "get_electricity_price_config": {
+ "constant_price": 0,
+ "time_of_use_config": {
+ "summer": {
+ "midpeak": 0,
+ "offpeak": 0,
+ "onpeak": 0,
+ "period": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "weekday_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "weekend_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ },
+ "winter": {
+ "midpeak": 0,
+ "offpeak": 0,
+ "onpeak": 0,
+ "period": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "weekday_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "weekend_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+ },
+ "type": "constant"
+ },
+ "get_emeter_data": {
+ "current_ma": 0,
+ "energy_wh": 0,
+ "power_mw": 0,
+ "voltage_mv": 119720
+ },
+ "get_emeter_vgain_igain": {
+ "data": [
+ {
+ "igain": 3788,
+ "slot_id": 0,
+ "vgain": 30382
+ },
+ {
+ "igain": 3833,
+ "slot_id": 1,
+ "vgain": 30253
+ }
+ ]
+ },
+ "get_energy_usage": {
+ "electricity_charge": [
+ 0,
+ 0,
+ 0
+ ],
+ "local_time": "2024-12-20 15:13:58",
+ "month_energy": 0,
+ "month_runtime": 3564,
+ "today_energy": 0,
+ "today_runtime": 913
+ },
+ "get_max_power": {
+ "max_power": 1827
+ },
+ "get_next_event": {},
+ "get_protection_power": {
+ "enabled": false,
+ "protection_power": 0
+ },
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ }
+ }
+ },
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "wireless",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "led",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "control_child",
+ "ver_code": 2
+ },
+ {
+ "id": "child_device",
+ "ver_code": 2
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ },
+ {
+ "id": "matter",
+ "ver_code": 2
+ },
+ {
+ "id": "energy_monitoring",
+ "ver_code": 2
+ },
+ {
+ "id": "current_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "power_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "charging_protection",
+ "ver_code": 2
+ }
+ ]
+ },
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P210M(US)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "DC-62-79-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000",
+ "protocol_version": 1
+ }
+ },
+ "get_auto_update_info": {
+ "enable": true,
+ "random_range": 120,
+ "time": 180
+ },
+ "get_child_device_component_list": {
+ "child_component_list": [
+ {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ },
+ {
+ "id": "energy_monitoring",
+ "ver_code": 2
+ },
+ {
+ "id": "current_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "power_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "charging_protection",
+ "ver_code": 2
+ }
+ ],
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
+ },
+ {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ },
+ {
+ "id": "energy_monitoring",
+ "ver_code": 2
+ },
+ {
+ "id": "current_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "power_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "charging_protection",
+ "ver_code": 2
+ }
+ ],
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2"
+ }
+ ],
+ "start_index": 0,
+ "sum": 2
+ },
+ "get_child_device_list": {
+ "child_device_list": [
+ {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "charging_status": "normal",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.0.3 Build 240703 Rel.114246",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "DC6279000000",
+ "model": "P210M",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 170202,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overcurrent_status": "normal",
+ "overheat_status": "normal",
+ "position": 1,
+ "power_protection_status": "normal",
+ "protection_enabled": false,
+ "protection_power": 0,
+ "region": "America/Los_Angeles",
+ "slot_number": 2,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "charging_status": "normal",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.0.3 Build 240703 Rel.114246",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "DC6279000000",
+ "model": "P210M",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 170202,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overcurrent_status": "normal",
+ "overheat_status": "normal",
+ "position": 2,
+ "power_protection_status": "normal",
+ "protection_enabled": false,
+ "protection_power": 0,
+ "region": "America/Los_Angeles",
+ "slot_number": 2,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ }
+ ],
+ "start_index": 0,
+ "sum": 2
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_device_info": {
+ "avatar": "",
+ "device_id": "0000000000000000000000000000000000000000",
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.0.3 Build 240703 Rel.114246",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "ip": "127.0.0.123",
+ "lang": "en_US",
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "DC-62-79-00-00-00",
+ "model": "P210M",
+ "nickname": "",
+ "oem_id": "00000000000000000000000000000000",
+ "region": "America/Los_Angeles",
+ "rssi": -53,
+ "signal_level": 2,
+ "specs": "",
+ "ssid": "I01BU0tFRF9TU0lEIw==",
+ "time_diff": -480,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_time": {
+ "region": "America/Los_Angeles",
+ "time_diff": -480,
+ "timestamp": 1734736436
+ },
+ "get_device_usage": {
+ "power_usage": {
+ "past30": 275,
+ "past7": 275,
+ "today": 168
+ },
+ "saved_power": {
+ "past30": 3289,
+ "past7": 3289,
+ "today": 745
+ },
+ "time_usage": {
+ "past30": 3564,
+ "past7": 3564,
+ "today": 913
+ }
+ },
+ "get_electricity_price_config": {
+ "constant_price": 0,
+ "time_of_use_config": {
+ "summer": {
+ "midpeak": 0,
+ "offpeak": 0,
+ "onpeak": 0,
+ "period": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "weekday_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "weekend_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ },
+ "winter": {
+ "midpeak": 0,
+ "offpeak": 0,
+ "onpeak": 0,
+ "period": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "weekday_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "weekend_config": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+ },
+ "type": "constant"
+ },
+ "get_emeter_vgain_igain": {
+ "data": [
+ {
+ "igain": 3788,
+ "slot_id": 0,
+ "vgain": 30382
+ },
+ {
+ "igain": 3833,
+ "slot_id": 1,
+ "vgain": 30253
+ }
+ ]
+ },
+ "get_fw_download_state": {
+ "auto_upgrade": false,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_latest_fw": {
+ "fw_size": 0,
+ "fw_ver": "1.0.3 Build 240703 Rel.114246",
+ "hw_id": "",
+ "need_to_upgrade": false,
+ "oem_id": "",
+ "release_date": "",
+ "release_note": "",
+ "type": 0
+ },
+ "get_led_info": {
+ "bri_config": {
+ "bri_type": "overall",
+ "overall_bri": 50
+ },
+ "led_rule": "always",
+ "led_status": true,
+ "night_mode": {
+ "end_time": 420,
+ "night_mode_type": "custom",
+ "start_time": 1320
+ }
+ },
+ "get_matter_setup_info": {
+ "setup_code": "00000000000",
+ "setup_payload": "00:000000000000-000000"
+ },
+ "get_protection_power": {
+ "enabled": false,
+ "protection_power": 0
+ },
+ "get_wireless_scan_info": {
+ "ap_list": [
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "start_index": 0,
+ "sum": 29,
+ "wep_supported": false
+ },
+ "qs_component_nego": {
+ "component_list": [
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "matter",
+ "ver_code": 2
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "control_child",
+ "ver_code": 2
+ },
+ {
+ "id": "child_device",
+ "ver_code": 2
+ }
+ ],
+ "extra_info": {
+ "device_model": "P210M",
+ "device_type": "SMART.TAPOPLUG",
+ "is_klap": true
+ }
+ }
+}
diff --git a/tests/fixtures/smart/P300(EU)_1.0_1.0.13.json b/tests/fixtures/smart/P300(EU)_1.0_1.0.13.json
index 0d7d4a3b..73f76e83 100644
--- a/tests/fixtures/smart/P300(EU)_1.0_1.0.13.json
+++ b/tests/fixtures/smart/P300(EU)_1.0_1.0.13.json
@@ -84,21 +84,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P300(EU)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "78-8C-B5-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P300(EU)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "78-8C-B5-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": false,
@@ -170,7 +173,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000002"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
},
{
"component_list": [
@@ -235,7 +238,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000001"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2"
},
{
"component_list": [
@@ -300,7 +303,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000000"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_3"
}
],
"start_index": 0,
@@ -318,7 +321,7 @@
},
"type": "custom"
},
- "device_id": "000000000000000000000000000000000000000002",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
"device_on": false,
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.0.13 Build 230925 Rel.150200",
@@ -347,7 +350,7 @@
"default_states": {
"type": "last_states"
},
- "device_id": "000000000000000000000000000000000000000001",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
"device_on": false,
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.0.13 Build 230925 Rel.150200",
@@ -379,7 +382,7 @@
},
"type": "custom"
},
- "device_id": "000000000000000000000000000000000000000000",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_3",
"device_on": false,
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.0.13 Build 230925 Rel.150200",
diff --git a/tests/fixtures/smart/P300(EU)_1.0_1.0.15.json b/tests/fixtures/smart/P300(EU)_1.0_1.0.15.json
index dd40708e..e9d4b54f 100644
--- a/tests/fixtures/smart/P300(EU)_1.0_1.0.15.json
+++ b/tests/fixtures/smart/P300(EU)_1.0_1.0.15.json
@@ -495,21 +495,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P300(EU)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "78-8C-B5-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P300(EU)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "78-8C-B5-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": false,
diff --git a/tests/fixtures/smart/P300(EU)_1.0_1.0.7.json b/tests/fixtures/smart/P300(EU)_1.0_1.0.7.json
index 17df5ac5..eaa03a35 100644
--- a/tests/fixtures/smart/P300(EU)_1.0_1.0.7.json
+++ b/tests/fixtures/smart/P300(EU)_1.0_1.0.7.json
@@ -84,21 +84,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P300(EU)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "78-8C-B5-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P300(EU)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "78-8C-B5-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": true,
@@ -170,7 +173,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000001"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
},
{
"component_list": [
@@ -235,7 +238,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000002"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2"
},
{
"component_list": [
@@ -300,7 +303,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000003"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_3"
}
],
"start_index": 0,
@@ -315,7 +318,7 @@
"default_states": {
"type": "last_states"
},
- "device_id": "000000000000000000000000000000000000000001",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
"device_on": true,
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.0.7 Build 220715 Rel.200458",
@@ -329,7 +332,7 @@
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"on_time": 366,
- "original_device_id": "8022852468EC205A8178C7CBE81FC119213BC020",
+ "original_device_id": "0000000000000000000000000000000000000000",
"overheat_status": "normal",
"position": 1,
"region": "Europe/Berlin",
@@ -344,7 +347,7 @@
"default_states": {
"type": "last_states"
},
- "device_id": "000000000000000000000000000000000000000002",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
"device_on": true,
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.0.7 Build 220715 Rel.200458",
@@ -358,7 +361,7 @@
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"on_time": 366,
- "original_device_id": "8022852468EC205A8178C7CBE81FC119213BC020",
+ "original_device_id": "0000000000000000000000000000000000000000",
"overheat_status": "normal",
"position": 2,
"region": "Europe/Berlin",
@@ -373,7 +376,7 @@
"default_states": {
"type": "last_states"
},
- "device_id": "000000000000000000000000000000000000000003",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_3",
"device_on": true,
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.0.7 Build 220715 Rel.200458",
@@ -387,7 +390,7 @@
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"on_time": 366,
- "original_device_id": "8022852468EC205A8178C7CBE81FC119213BC020",
+ "original_device_id": "0000000000000000000000000000000000000000",
"overheat_status": "normal",
"position": 3,
"region": "Europe/Berlin",
diff --git a/tests/fixtures/smart/P304M(UK)_1.0_1.0.3.json b/tests/fixtures/smart/P304M(UK)_1.0_1.0.3.json
index 4e67f482..398977ad 100644
--- a/tests/fixtures/smart/P304M(UK)_1.0_1.0.3.json
+++ b/tests/fixtures/smart/P304M(UK)_1.0_1.0.3.json
@@ -1385,21 +1385,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "P304M(UK)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "A8-6E-84-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P304M(UK)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-6E-84-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": true,
diff --git a/tests/fixtures/smart/P306(US)_1.0_1.1.2.json b/tests/fixtures/smart/P306(US)_1.0_1.1.2.json
new file mode 100644
index 00000000..a5fcb1e8
--- /dev/null
+++ b/tests/fixtures/smart/P306(US)_1.0_1.1.2.json
@@ -0,0 +1,1708 @@
+{
+ "child_devices": {
+ "SCRUBBED_CHILD_DEVICE_ID_1": {
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_auto_off_config": {
+ "delay_min": 120,
+ "enable": false
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_device_info": {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "usb",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": true,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 169807,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 4,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 3561,
+ "past7": 3561,
+ "today": 907
+ }
+ },
+ "get_next_event": {},
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ }
+ },
+ "SCRUBBED_CHILD_DEVICE_ID_2": {
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_auto_off_config": {
+ "delay_min": 120,
+ "enable": false
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_device_info": {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 169808,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 3,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 3561,
+ "past7": 3561,
+ "today": 907
+ }
+ },
+ "get_next_event": {},
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ }
+ },
+ "SCRUBBED_CHILD_DEVICE_ID_3": {
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_auto_off_config": {
+ "delay_min": 120,
+ "enable": false
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_device_info": {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_3",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 169808,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 2,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 3561,
+ "past7": 3561,
+ "today": 907
+ }
+ },
+ "get_next_event": {},
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ }
+ },
+ "SCRUBBED_CHILD_DEVICE_ID_4": {
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_auto_off_config": {
+ "delay_min": 120,
+ "enable": false
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_device_info": {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_4",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 169808,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 1,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 3561,
+ "past7": 3561,
+ "today": 907
+ }
+ },
+ "get_next_event": {},
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ }
+ },
+ "SCRUBBED_CHILD_DEVICE_ID_5": {
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "brightness",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_device_info": {
+ "avatar": "bedside_lamp_1",
+ "bind_count": 1,
+ "brightness": 100,
+ "category": "plug.powerstrip.sub-bulb",
+ "default_states": {
+ "brightness": {
+ "type": "last_states",
+ "value": 100
+ },
+ "re_power_type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_5",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 7169,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 5,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOBULB"
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 2425,
+ "past7": 2425,
+ "today": 758
+ }
+ },
+ "get_next_event": {},
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ }
+ }
+ },
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "wireless",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "led",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "control_child",
+ "ver_code": 2
+ },
+ {
+ "id": "child_device",
+ "ver_code": 2
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ },
+ {
+ "id": "homekit",
+ "ver_code": 2
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ }
+ ]
+ },
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "P306(US)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-6E-84-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000",
+ "protocol_version": 1
+ }
+ },
+ "get_auto_update_info": {
+ "enable": true,
+ "random_range": 120,
+ "time": 180
+ },
+ "get_child_device_component_list": {
+ "child_component_list": [
+ {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ }
+ ],
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
+ },
+ {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ }
+ ],
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2"
+ },
+ {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ }
+ ],
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_3"
+ },
+ {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_off",
+ "ver_code": 2
+ }
+ ],
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_4"
+ },
+ {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "default_states",
+ "ver_code": 1
+ },
+ {
+ "id": "brightness",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "overheat_protection",
+ "ver_code": 1
+ },
+ {
+ "id": "auto_light_control",
+ "ver_code": 1
+ }
+ ],
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_5"
+ }
+ ],
+ "start_index": 0,
+ "sum": 5
+ },
+ "get_child_device_list": {
+ "child_device_list": [
+ {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "usb",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": true,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 169805,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 4,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 169805,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 3,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_3",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 169805,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 2,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ {
+ "auto_off_remain_time": 0,
+ "auto_off_status": "off",
+ "avatar": "plug",
+ "bind_count": 1,
+ "category": "plug.powerstrip.sub-plug",
+ "default_states": {
+ "type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_4",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_usb": false,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 169805,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 1,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOPLUG"
+ },
+ {
+ "avatar": "bedside_lamp_1",
+ "bind_count": 1,
+ "brightness": 100,
+ "category": "plug.powerstrip.sub-bulb",
+ "default_states": {
+ "brightness": {
+ "type": "last_states",
+ "value": 100
+ },
+ "re_power_type": "last_states"
+ },
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_5",
+ "device_on": true,
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A86E84000000",
+ "model": "P306",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "on_time": 7166,
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "overheat_status": "normal",
+ "position": 5,
+ "region": "America/Los_Angeles",
+ "slot_number": 5,
+ "status_follow_edge": true,
+ "type": "SMART.TAPOBULB"
+ }
+ ],
+ "start_index": 0,
+ "sum": 5
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_device_info": {
+ "avatar": "",
+ "device_id": "0000000000000000000000000000000000000000",
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "ip": "127.0.0.123",
+ "lang": "en_US",
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A8-6E-84-00-00-00",
+ "model": "P306",
+ "nickname": "",
+ "oem_id": "00000000000000000000000000000000",
+ "region": "America/Los_Angeles",
+ "rssi": -46,
+ "signal_level": 3,
+ "specs": "",
+ "ssid": "I01BU0tFRF9TU0lEIw==",
+ "time_diff": -480,
+ "type": "SMART.TAPOPLUG"
+ },
+ "get_device_time": {
+ "region": "America/Los_Angeles",
+ "time_diff": -480,
+ "timestamp": 1734736024
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 3561,
+ "past7": 3561,
+ "today": 907
+ }
+ },
+ "get_fw_download_state": {
+ "auto_upgrade": false,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_homekit_info": {
+ "mfi_setup_code": "000-00-000",
+ "mfi_setup_id": "0000",
+ "mfi_token_token": "000000000000000000000000000000000000000000000000/00000000000000000000000000000000000000000000000000000000/0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/0000000000000000000",
+ "mfi_token_uuid": "00000000-0000-0000-0000-000000000000"
+ },
+ "get_latest_fw": {
+ "fw_size": 0,
+ "fw_ver": "1.1.2 Build 240531 Rel.204226",
+ "hw_id": "",
+ "need_to_upgrade": false,
+ "oem_id": "",
+ "release_date": "",
+ "release_note": "",
+ "type": 0
+ },
+ "get_led_info": {
+ "bri_config": {
+ "bri_type": "overall",
+ "overall_bri": 50
+ },
+ "led_rule": "always",
+ "led_status": true,
+ "night_mode": {
+ "end_time": 420,
+ "night_mode_type": "custom",
+ "start_time": 1320
+ }
+ },
+ "get_wireless_scan_info": {
+ "ap_list": [
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "bssid": "000000000000",
+ "channel": 0,
+ "cipher_type": 2,
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "start_index": 0,
+ "sum": 24,
+ "wep_supported": false
+ },
+ "qs_component_nego": {
+ "component_list": [
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 2
+ },
+ {
+ "id": "control_child",
+ "ver_code": 2
+ },
+ {
+ "id": "child_device",
+ "ver_code": 2
+ }
+ ],
+ "extra_info": {
+ "device_model": "P306",
+ "device_type": "SMART.TAPOPLUG",
+ "is_klap": true
+ }
+ }
+}
diff --git a/tests/fixtures/smart/RV20 Max Plus(EU)_1.0_1.0.7.json b/tests/fixtures/smart/RV20 Max Plus(EU)_1.0_1.0.7.json
new file mode 100644
index 00000000..5a09c155
--- /dev/null
+++ b/tests/fixtures/smart/RV20 Max Plus(EU)_1.0_1.0.7.json
@@ -0,0 +1,382 @@
+{
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 1
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "clean",
+ "ver_code": 3
+ },
+ {
+ "id": "battery",
+ "ver_code": 1
+ },
+ {
+ "id": "consumables",
+ "ver_code": 2
+ },
+ {
+ "id": "direction_control",
+ "ver_code": 1
+ },
+ {
+ "id": "button_and_led",
+ "ver_code": 1
+ },
+ {
+ "id": "speaker",
+ "ver_code": 3
+ },
+ {
+ "id": "schedule",
+ "ver_code": 3
+ },
+ {
+ "id": "wireless",
+ "ver_code": 1
+ },
+ {
+ "id": "map",
+ "ver_code": 2
+ },
+ {
+ "id": "auto_change_map",
+ "ver_code": -1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "dust_bucket",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ },
+ {
+ "id": "mop",
+ "ver_code": 1
+ },
+ {
+ "id": "do_not_disturb",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "charge_pose_clean",
+ "ver_code": 1
+ },
+ {
+ "id": "continue_breakpoint_sweep",
+ "ver_code": 1
+ },
+ {
+ "id": "goto_point",
+ "ver_code": 1
+ },
+ {
+ "id": "furniture",
+ "ver_code": 1
+ },
+ {
+ "id": "map_cloud_backup",
+ "ver_code": 1
+ },
+ {
+ "id": "dev_log",
+ "ver_code": 1
+ },
+ {
+ "id": "map_lock",
+ "ver_code": 1
+ },
+ {
+ "id": "carpet_area",
+ "ver_code": 1
+ },
+ {
+ "id": "clean_angle",
+ "ver_code": 1
+ },
+ {
+ "id": "clean_percent",
+ "ver_code": 1
+ },
+ {
+ "id": "no_pose_config",
+ "ver_code": 1
+ }
+ ]
+ },
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "RV20 Max Plus(EU)",
+ "device_type": "SMART.TAPOROBOVAC",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "B0-19-21-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 4433,
+ "is_support_https": true
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
+ },
+ "getAreaUnit": {
+ "area_unit": 0
+ },
+ "getAutoChangeMap": {
+ "auto_change_map": false
+ },
+ "getAutoDustCollection": {
+ "auto_dust_collection": 1
+ },
+ "getBatteryInfo": {
+ "battery_percentage": 75
+ },
+ "getCarpetClean": {
+ "carpet_clean_prefer": "boost"
+ },
+ "getChildLockInfo": {
+ "child_lock_status": false
+ },
+ "getCleanAttr": {
+ "cistern": 2,
+ "clean_number": 1,
+ "suction": 2
+ },
+ "getCleanInfo": {
+ "clean_area": 5,
+ "clean_percent": 1,
+ "clean_time": 5
+ },
+ "getCleanRecords": {
+ "lastest_day_record": [
+ 1736797545,
+ 25,
+ 16,
+ 1
+ ],
+ "record_list": [
+ {
+ "clean_area": 17,
+ "clean_time": 27,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 1,
+ "map_id": 1736598799,
+ "message": 1,
+ "record_index": 0,
+ "start_type": 1,
+ "task_type": 0,
+ "timestamp": 1736601522
+ },
+ {
+ "clean_area": 14,
+ "clean_time": 25,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1736598799,
+ "message": 0,
+ "record_index": 1,
+ "start_type": 1,
+ "task_type": 0,
+ "timestamp": 1736684961
+ },
+ {
+ "clean_area": 16,
+ "clean_time": 25,
+ "dust_collection": true,
+ "error": 0,
+ "info_num": 3,
+ "map_id": 1736598799,
+ "message": 0,
+ "record_index": 2,
+ "start_type": 1,
+ "task_type": 0,
+ "timestamp": 1736797545
+ }
+ ],
+ "record_list_num": 3,
+ "total_area": 47,
+ "total_number": 3,
+ "total_time": 77
+ },
+ "getCleanStatus": {
+ "getCleanStatus": {
+ "clean_status": 0,
+ "is_mapping": false,
+ "is_relocating": false,
+ "is_working": false
+ }
+ },
+ "getConsumablesInfo": {
+ "charge_contact_time": 0,
+ "edge_brush_time": 0,
+ "filter_time": 0,
+ "main_brush_lid_time": 0,
+ "rag_time": 0,
+ "roll_brush_time": 0,
+ "sensor_time": 0
+ },
+ "getCurrentVoiceLanguage": {
+ "name": "2",
+ "version": 1
+ },
+ "getDoNotDisturb": {
+ "do_not_disturb": true,
+ "e_min": 480,
+ "s_min": 1320
+ },
+ "getDustCollectionInfo": {
+ "auto_dust_collection": true,
+ "dust_collection_mode": 0
+ },
+ "getMapInfo": {
+ "auto_change_map": false,
+ "current_map_id": 0,
+ "map_list": [],
+ "map_num": 0,
+ "version": "LDS"
+ },
+ "getMopState": {
+ "mop_state": false
+ },
+ "getVacStatus": {
+ "err_status": [
+ 0
+ ],
+ "errorCode_id": [
+ 0
+ ],
+ "prompt": [],
+ "promptCode_id": [],
+ "status": 5
+ },
+ "getVolume": {
+ "volume": 84
+ },
+ "get_device_info": {
+ "auto_pack_ver": "0.0.1.1771",
+ "avatar": "",
+ "board_sn": "000000000000",
+ "custom_sn": "000000000000",
+ "device_id": "0000000000000000000000000000000000000000",
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.0.7 Build 240828 Rel.205951",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "ip": "127.0.0.123",
+ "lang": "",
+ "latitude": 0,
+ "linux_ver": "V21.198.1708420747",
+ "location": "",
+ "longitude": 0,
+ "mac": "B0-19-21-00-00-00",
+ "mcu_ver": "1.1.2563.5",
+ "model": "RV20 Max Plus",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "overheated": false,
+ "region": "Europe/Berlin",
+ "rssi": -59,
+ "signal_level": 2,
+ "specs": "",
+ "ssid": "I01BU0tFRF9TU0lEIw==",
+ "sub_ver": "0.0.1.1771-1.1.34",
+ "time_diff": 60,
+ "total_ver": "1.1.34",
+ "type": "SMART.TAPOROBOVAC"
+ },
+ "get_device_time": {
+ "region": "Europe/Berlin",
+ "time_diff": 60,
+ "timestamp": 1736598518
+ },
+ "get_fw_download_state": {
+ "auto_upgrade": false,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_inherit_info": null,
+ "get_next_event": {},
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ },
+ "get_wireless_scan_info": {
+ "ap_list": [
+ {
+ "key_type": "wpa2_psk",
+ "signal_level": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "start_index": 0,
+ "sum": 1,
+ "wep_supported": true
+ },
+ "qs_component_nego": {
+ "component_list": [
+ {
+ "id": "quick_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ }
+ ],
+ "extra_info": {
+ "device_model": "RV20 Max Plus",
+ "device_type": "SMART.TAPOROBOVAC"
+ }
+ }
+}
diff --git a/tests/fixtures/smart/RV30 Max(US)_1.0_1.2.0.json b/tests/fixtures/smart/RV30 Max(US)_1.0_1.2.0.json
new file mode 100644
index 00000000..9b6484da
--- /dev/null
+++ b/tests/fixtures/smart/RV30 Max(US)_1.0_1.2.0.json
@@ -0,0 +1,888 @@
+{
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 1
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "clean",
+ "ver_code": 3
+ },
+ {
+ "id": "battery",
+ "ver_code": 1
+ },
+ {
+ "id": "consumables",
+ "ver_code": 2
+ },
+ {
+ "id": "direction_control",
+ "ver_code": 1
+ },
+ {
+ "id": "button_and_led",
+ "ver_code": 1
+ },
+ {
+ "id": "speaker",
+ "ver_code": 3
+ },
+ {
+ "id": "schedule",
+ "ver_code": 3
+ },
+ {
+ "id": "wireless",
+ "ver_code": 1
+ },
+ {
+ "id": "map",
+ "ver_code": 2
+ },
+ {
+ "id": "auto_change_map",
+ "ver_code": 2
+ },
+ {
+ "id": "mop",
+ "ver_code": 1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "do_not_disturb",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "charge_pose_clean",
+ "ver_code": 1
+ },
+ {
+ "id": "continue_breakpoint_sweep",
+ "ver_code": 1
+ },
+ {
+ "id": "goto_point",
+ "ver_code": 1
+ },
+ {
+ "id": "furniture",
+ "ver_code": 1
+ },
+ {
+ "id": "map_cloud_backup",
+ "ver_code": 1
+ },
+ {
+ "id": "dev_log",
+ "ver_code": 1
+ },
+ {
+ "id": "map_lock",
+ "ver_code": 1
+ },
+ {
+ "id": "carpet_area",
+ "ver_code": 1
+ },
+ {
+ "id": "clean_angle",
+ "ver_code": 1
+ },
+ {
+ "id": "clean_percent",
+ "ver_code": 1
+ },
+ {
+ "id": "no_pose_config",
+ "ver_code": 1
+ }
+ ]
+ },
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "RV30 Max(US)",
+ "device_type": "SMART.TAPOROBOVAC",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "7C-F1-7E-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 4433,
+ "is_support_https": true
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000",
+ "protocol_version": 1
+ }
+ },
+ "getAreaUnit": {
+ "area_unit": 1
+ },
+ "getAutoChangeMap": {
+ "auto_change_map": true
+ },
+ "getBatteryInfo": {
+ "battery_percentage": 100
+ },
+ "getCarpetClean": {
+ "carpet_clean_prefer": "boost"
+ },
+ "getChildLockInfo": {
+ "child_lock_status": false
+ },
+ "getCleanAttr": {
+ "cistern": 1,
+ "clean_number": 1,
+ "suction": 2
+ },
+ "getCleanInfo": {
+ "clean_area": 59,
+ "clean_percent": 100,
+ "clean_time": 56
+ },
+ "getCleanRecords": {
+ "lastest_day_record": [
+ 1737387294,
+ 56,
+ 59,
+ 1
+ ],
+ "record_list": [
+ {
+ "clean_area": 59,
+ "clean_time": 57,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 0,
+ "start_type": 4,
+ "task_type": 0,
+ "timestamp": 1737041654
+ },
+ {
+ "clean_area": 39,
+ "clean_time": 58,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 1,
+ "map_id": 1736541042,
+ "message": 0,
+ "record_index": 1,
+ "start_type": 1,
+ "task_type": 0,
+ "timestamp": 1737055944
+ },
+ {
+ "clean_area": 1,
+ "clean_time": 3,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 2,
+ "start_type": 1,
+ "task_type": 4,
+ "timestamp": 1737074472
+ },
+ {
+ "clean_area": 59,
+ "clean_time": 58,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 3,
+ "start_type": 4,
+ "task_type": 0,
+ "timestamp": 1737128195
+ },
+ {
+ "clean_area": 68,
+ "clean_time": 78,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 2,
+ "map_id": 1736541042,
+ "message": 0,
+ "record_index": 4,
+ "start_type": 1,
+ "task_type": 1,
+ "timestamp": 1737216716
+ },
+ {
+ "clean_area": 3,
+ "clean_time": 3,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734742958,
+ "message": 0,
+ "record_index": 5,
+ "start_type": 1,
+ "task_type": 3,
+ "timestamp": 1737300731
+ },
+ {
+ "clean_area": 20,
+ "clean_time": 16,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734742958,
+ "message": 0,
+ "record_index": 6,
+ "start_type": 1,
+ "task_type": 3,
+ "timestamp": 1737304391
+ },
+ {
+ "clean_area": 59,
+ "clean_time": 56,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 7,
+ "start_type": 4,
+ "task_type": 0,
+ "timestamp": 1737387294
+ },
+ {
+ "clean_area": 17,
+ "clean_time": 16,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 8,
+ "start_type": 1,
+ "task_type": 3,
+ "timestamp": 1736707487
+ },
+ {
+ "clean_area": 8,
+ "clean_time": 10,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 9,
+ "start_type": 1,
+ "task_type": 4,
+ "timestamp": 1736708425
+ },
+ {
+ "clean_area": 59,
+ "clean_time": 54,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 10,
+ "start_type": 4,
+ "task_type": 0,
+ "timestamp": 1736782261
+ },
+ {
+ "clean_area": 60,
+ "clean_time": 56,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 11,
+ "start_type": 4,
+ "task_type": 0,
+ "timestamp": 1736868752
+ },
+ {
+ "clean_area": 58,
+ "clean_time": 68,
+ "dust_collection": true,
+ "error": 1,
+ "info_num": 0,
+ "map_id": 1736541042,
+ "message": 0,
+ "record_index": 12,
+ "start_type": 1,
+ "task_type": 1,
+ "timestamp": 1736881428
+ },
+ {
+ "clean_area": 59,
+ "clean_time": 59,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 13,
+ "start_type": 4,
+ "task_type": 0,
+ "timestamp": 1736955682
+ },
+ {
+ "clean_area": 36,
+ "clean_time": 33,
+ "dust_collection": false,
+ "error": 0,
+ "info_num": 0,
+ "map_id": 1734727686,
+ "message": 0,
+ "record_index": 14,
+ "start_type": 1,
+ "task_type": 4,
+ "timestamp": 1736960713
+ }
+ ],
+ "record_list_num": 15,
+ "total_area": 2304,
+ "total_number": 85,
+ "total_time": 2510
+ },
+ "getCleanStatus": {
+ "clean_status": 0,
+ "is_mapping": false,
+ "is_relocating": false,
+ "is_working": false
+ },
+ "getConsumablesInfo": {
+ "charge_contact_time": 660,
+ "edge_brush_time": 2743,
+ "filter_time": 287,
+ "main_brush_lid_time": 2462,
+ "rag_time": 0,
+ "roll_brush_time": 2719,
+ "sensor_time": 935
+ },
+ "getCurrentVoiceLanguage": {
+ "name": "bb053ca2c5605a55090fcdb952f3902b",
+ "version": 2
+ },
+ "getDoNotDisturb": {
+ "do_not_disturb": true,
+ "e_min": 480,
+ "s_min": 1320
+ },
+ "getMapData": {
+ "area_list": [
+ {
+ "cistern": 1,
+ "clean_number": 1,
+ "color": 3,
+ "floor_texture": -1,
+ "id": 5,
+ "name": "I01BU0tFRF9OQU1FIw==",
+ "suction": 2,
+ "type": "room"
+ },
+ {
+ "cistern": 1,
+ "clean_number": 1,
+ "color": 4,
+ "floor_texture": -1,
+ "id": 6,
+ "name": "I01BU0tFRF9OQU1FIw==",
+ "suction": 2,
+ "type": "room"
+ },
+ {
+ "cistern": 1,
+ "clean_number": 1,
+ "color": 1,
+ "floor_texture": 0,
+ "id": 2,
+ "name": "I01BU0tFRF9OQU1FIw==",
+ "suction": 2,
+ "type": "room"
+ },
+ {
+ "cistern": 1,
+ "clean_number": 1,
+ "color": 5,
+ "floor_texture": 90,
+ "id": 3,
+ "name": "I01BU0tFRF9OQU1FIw==",
+ "suction": 2,
+ "type": "room"
+ },
+ {
+ "cistern": 1,
+ "clean_number": 1,
+ "color": 2,
+ "floor_texture": -1,
+ "id": 4,
+ "name": "I01BU0tFRF9OQU1FIw==",
+ "suction": 2,
+ "type": "room"
+ },
+ {
+ "id": 401,
+ "type": "virtual_wall",
+ "vertexs": [
+ [
+ 4711,
+ 985
+ ],
+ [
+ 4717,
+ -404
+ ]
+ ]
+ },
+ {
+ "id": 301,
+ "type": "forbid",
+ "vertexs": [
+ [
+ 3061,
+ -3027
+ ],
+ [
+ 3580,
+ -3027
+ ],
+ [
+ 3580,
+ -3692
+ ],
+ [
+ 3061,
+ -3692
+ ]
+ ]
+ },
+ {
+ "id": 402,
+ "type": "virtual_wall",
+ "vertexs": [
+ [
+ 5302,
+ 6816
+ ],
+ [
+ 5304,
+ 4924
+ ]
+ ]
+ },
+ {
+ "cistern": -1,
+ "clean_number": 1,
+ "id": 501,
+ "suction": -1,
+ "type": "area",
+ "vertexs": [
+ [
+ 2889,
+ 6241
+ ],
+ [
+ 3721,
+ 6241
+ ],
+ [
+ 3721,
+ 4919
+ ],
+ [
+ 2889,
+ 4919
+ ]
+ ]
+ },
+ {
+ "carpet_strategy": 11,
+ "id": 101,
+ "type": "carpet_rectangle",
+ "vertexs": [
+ [
+ 20,
+ -2012
+ ],
+ [
+ 2857,
+ -2012
+ ],
+ [
+ 2857,
+ -4122
+ ],
+ [
+ 20,
+ -4122
+ ]
+ ]
+ },
+ {
+ "carpet_strategy": 11,
+ "id": 102,
+ "type": "carpet_rectangle",
+ "vertexs": [
+ [
+ 1327,
+ 3064
+ ],
+ [
+ 2428,
+ 3064
+ ],
+ [
+ 2428,
+ 2258
+ ],
+ [
+ 1327,
+ 2258
+ ]
+ ]
+ },
+ {
+ "carpet_strategy": 11,
+ "id": 103,
+ "type": "carpet_rectangle",
+ "vertexs": [
+ [
+ 4458,
+ 5974
+ ],
+ [
+ 5336,
+ 5974
+ ],
+ [
+ 5336,
+ 4903
+ ],
+ [
+ 4458,
+ 4903
+ ]
+ ]
+ },
+ {
+ "carpet_strategy": 11,
+ "id": 104,
+ "type": "carpet_rectangle",
+ "vertexs": [
+ [
+ -1383,
+ 2730
+ ],
+ [
+ -761,
+ 2730
+ ],
+ [
+ -761,
+ 1587
+ ],
+ [
+ -1383,
+ 1587
+ ]
+ ]
+ }
+ ],
+ "auto_area_flag": true,
+ "bit_list": {
+ "auto_area": [
+ 0,
+ 100
+ ],
+ "barrier": 0,
+ "clean": 255,
+ "none": 127
+ },
+ "bitnum": 8,
+ "charge_coor": [
+ 65,
+ 134,
+ 272
+ ],
+ "furniture_list": [],
+ "height": 303,
+ "map_data": "#SCRUBBED_MAPDATA#",
+ "map_hash": "A5D8FA4487CC40312EF58D8123F0A4CC",
+ "map_id": 1734727686,
+ "map_locked": 0,
+ "map_name": "I01BU0tFRF9OQU1FIw==",
+ "origin_coor": [
+ -33,
+ -108,
+ 270
+ ],
+ "path_id": 122,
+ "pix_len": 66660,
+ "pix_lz4len": 6826,
+ "real_charge_coor": [
+ 1599,
+ 1295,
+ 272
+ ],
+ "real_origin_coor": [
+ -1674,
+ -5424,
+ 270
+ ],
+ "real_vac_coor": [
+ 1599,
+ 1076,
+ 272
+ ],
+ "resolution": 50,
+ "resolution_unit": "mm",
+ "vac_coor": [
+ 65,
+ 130,
+ 272
+ ],
+ "version": "LDS",
+ "width": 220
+ },
+ "getMapInfo": {
+ "auto_change_map": true,
+ "current_map_id": 1734727686,
+ "map_list": [
+ {
+ "auto_area_flag": true,
+ "global_cleaned": -1,
+ "is_saved": true,
+ "map_id": 1734727686,
+ "map_locked": 0,
+ "map_name": "I01BU0tFRF9OQU1FIw==",
+ "rotate_angle": 270,
+ "update_time": 1737387285
+ },
+ {
+ "auto_area_flag": true,
+ "global_cleaned": -1,
+ "is_saved": true,
+ "map_id": 1734742958,
+ "map_locked": 0,
+ "map_name": "I01BU0tFRF9OQU1FIw==",
+ "rotate_angle": 0,
+ "update_time": 1737304392
+ },
+ {
+ "auto_area_flag": true,
+ "global_cleaned": -1,
+ "is_saved": true,
+ "map_id": 1736541042,
+ "map_locked": 0,
+ "map_name": "I01BU0tFRF9OQU1FIw==",
+ "rotate_angle": 270,
+ "update_time": 1737216718
+ }
+ ],
+ "map_num": 3,
+ "version": "LDS"
+ },
+ "getMopState": {
+ "mop_state": false
+ },
+ "getVacStatus": {
+ "err_status": [
+ 0
+ ],
+ "errorCode_id": [
+ 1144500830
+ ],
+ "prompt": [],
+ "promptCode_id": [],
+ "status": 6
+ },
+ "getVolume": {
+ "volume": 60
+ },
+ "get_device_info": {
+ "auto_pack_ver": "0.0.131.1852",
+ "avatar": "",
+ "board_sn": "000000000000",
+ "cd": "I01BU0tFRF9CSU5BUlkj",
+ "custom_sn": "000000000000",
+ "device_id": "0000000000000000000000000000000000000000",
+ "fw_id": "00000000000000000000000000000000",
+ "fw_ver": "1.2.0 Build 241219 Rel.163928",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "ip": "127.0.0.123",
+ "lang": "",
+ "latitude": 0,
+ "linux_ver": "V21.198.1708420747",
+ "location": "",
+ "longitude": 0,
+ "mac": "7C-F1-7E-00-00-00",
+ "mcu_ver": "1.1.2724.442",
+ "model": "RV30 Max",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "overheated": false,
+ "product_id": "1794",
+ "region": "America/Chicago",
+ "rssi": -38,
+ "signal_level": 3,
+ "specs": "",
+ "ssid": "I01BU0tFRF9TU0lEIw==",
+ "sub_ver": "0.0.131.1852-1.4.40",
+ "time_diff": -360,
+ "total_ver": "1.4.40",
+ "type": "SMART.TAPOROBOVAC"
+ },
+ "get_device_time": {
+ "region": "America/Chicago",
+ "time_diff": -360,
+ "timestamp": 1737399953
+ },
+ "get_fw_download_state": {
+ "auto_upgrade": false,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_inherit_info": {
+ "inherit_status": true
+ },
+ "get_latest_fw": {
+ "fw_size": 0,
+ "fw_ver": "1.2.0 Build 241219 Rel.163928",
+ "hw_id": "",
+ "need_to_upgrade": false,
+ "oem_id": "",
+ "release_date": "",
+ "release_note": "",
+ "type": 0
+ },
+ "get_schedule_rules": {
+ "enable": true,
+ "rule_list": [
+ {
+ "alarm_min": 0,
+ "cancel": false,
+ "clean_attr": {
+ "cistern": 2,
+ "clean_mode": 0,
+ "clean_number": 1,
+ "clean_order": false,
+ "suction": 2
+ },
+ "day": 21,
+ "enable": true,
+ "id": "S1",
+ "invalid": 0,
+ "mode": "repeat",
+ "month": 1,
+ "s_min": 515,
+ "start_remind": true,
+ "week_day": 62,
+ "year": 2025
+ }
+ ],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 1
+ },
+ "get_wireless_scan_info": {
+ "ap_list": [
+ {
+ "key_type": "wpa2_psk",
+ "signal_level": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "key_type": "wpa2_psk",
+ "signal_level": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "start_index": 0,
+ "sum": 5,
+ "wep_supported": true
+ },
+ "qs_component_nego": {
+ "component_list": [
+ {
+ "id": "quick_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 1
+ },
+ {
+ "id": "ble_whole_setup",
+ "ver_code": 1
+ },
+ {
+ "id": "inherit",
+ "ver_code": 1
+ }
+ ],
+ "extra_info": {
+ "device_model": "RV30 Max",
+ "device_type": "SMART.TAPOROBOVAC"
+ }
+ }
+}
diff --git a/tests/fixtures/smart/S500D(US)_1.0_1.0.5.json b/tests/fixtures/smart/S500D(US)_1.0_1.0.5.json
index a141e700..3e6ec48d 100644
--- a/tests/fixtures/smart/S500D(US)_1.0_1.0.5.json
+++ b/tests/fixtures/smart/S500D(US)_1.0_1.0.5.json
@@ -92,21 +92,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "S500D(US)",
- "device_type": "SMART.TAPOSWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "48-22-54-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "S500D(US)",
+ "device_type": "SMART.TAPOSWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "48-22-54-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/S505(US)_1.0_1.0.2.json b/tests/fixtures/smart/S505(US)_1.0_1.0.2.json
index c9c63cd7..340bd3a1 100644
--- a/tests/fixtures/smart/S505(US)_1.0_1.0.2.json
+++ b/tests/fixtures/smart/S505(US)_1.0_1.0.2.json
@@ -80,21 +80,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "S505(US)",
- "device_type": "SMART.TAPOSWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "S505(US)",
+ "device_type": "SMART.TAPOSWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/S505D(US)_1.0_1.1.0.json b/tests/fixtures/smart/S505D(US)_1.0_1.1.0.json
index 6adac986..0c990d75 100644
--- a/tests/fixtures/smart/S505D(US)_1.0_1.1.0.json
+++ b/tests/fixtures/smart/S505D(US)_1.0_1.1.0.json
@@ -100,21 +100,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "S505D(US)",
- "device_type": "SMART.TAPOSWITCH",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "48-22-54-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "KLAP",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "matter",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "S505D(US)",
+ "device_type": "SMART.TAPOSWITCH",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "48-22-54-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "KLAP",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "matter",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/TP15(US)_1.0_1.0.3.json b/tests/fixtures/smart/TP15(US)_1.0_1.0.3.json
index 404bfe2f..8d0964b3 100644
--- a/tests/fixtures/smart/TP15(US)_1.0_1.0.3.json
+++ b/tests/fixtures/smart/TP15(US)_1.0_1.0.3.json
@@ -76,21 +76,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "TP15(US)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "5C-62-8B-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "TP15(US)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "5C-62-8B-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_antitheft_rules": {
"antitheft_rule_max_count": 1,
diff --git a/tests/fixtures/smart/TP25(US)_1.0_1.0.2.json b/tests/fixtures/smart/TP25(US)_1.0_1.0.2.json
index 1e3321f8..b9165414 100644
--- a/tests/fixtures/smart/TP25(US)_1.0_1.0.2.json
+++ b/tests/fixtures/smart/TP25(US)_1.0_1.0.2.json
@@ -84,21 +84,24 @@
]
},
"discovery_result": {
- "device_id": "00000000000000000000000000000000",
- "device_model": "TP25(US)",
- "device_type": "SMART.TAPOPLUG",
- "factory_default": false,
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "3C-52-A1-00-00-00",
- "mgt_encrypt_schm": {
- "encrypt_type": "AES",
- "http_port": 80,
- "is_support_https": false,
- "lv": 2
- },
- "obd_src": "tplink",
- "owner": "00000000000000000000000000000000"
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "TP25(US)",
+ "device_type": "SMART.TAPOPLUG",
+ "factory_default": false,
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "3C-52-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "encrypt_type": "AES",
+ "http_port": 80,
+ "is_support_https": false,
+ "lv": 2
+ },
+ "obd_src": "tplink",
+ "owner": "00000000000000000000000000000000"
+ }
},
"get_auto_update_info": {
"enable": false,
@@ -170,7 +173,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000000"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
},
{
"component_list": [
@@ -235,7 +238,7 @@
"ver_code": 1
}
],
- "device_id": "000000000000000000000000000000000000000001"
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2"
}
],
"start_index": 0,
@@ -250,7 +253,7 @@
"default_states": {
"type": "last_states"
},
- "device_id": "000000000000000000000000000000000000000000",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
"device_on": false,
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.0.2 Build 230206 Rel.095245",
@@ -279,7 +282,7 @@
"default_states": {
"type": "last_states"
},
- "device_id": "000000000000000000000000000000000000000001",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
"device_on": true,
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.0.2 Build 230206 Rel.095245",
diff --git a/tests/fixtures/smart/child/S210(EU)_1.0_1.9.0.json b/tests/fixtures/smart/child/S210(EU)_1.0_1.9.0.json
new file mode 100644
index 00000000..201612cd
--- /dev/null
+++ b/tests/fixtures/smart/child/S210(EU)_1.0_1.9.0.json
@@ -0,0 +1,168 @@
+{
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "trigger_log",
+ "ver_code": 1
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "delay_action",
+ "ver_code": 1
+ },
+ {
+ "id": "battery_detect",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_device_info": {
+ "avatar": "switch_s210",
+ "battery_percentage": 100,
+ "bind_count": 2,
+ "category": "subg.plugswitch.switch",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
+ "device_on": true,
+ "fw_ver": "1.9.0 Build 231106 Rel.164425",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_low": false,
+ "jamming_rssi": -111,
+ "jamming_signal_level": 1,
+ "lastOnboardingTimestamp": 1733332893,
+ "latitude": 0,
+ "led_off": 0,
+ "longitude": 0,
+ "mac": "DC6279000000",
+ "model": "S210",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "parent_device_id": "0000000000000000000000000000000000000000",
+ "position": 1,
+ "region": "Europe/London",
+ "rssi": -34,
+ "signal_level": 3,
+ "slot_number": 1,
+ "specs": "EU",
+ "status": "online",
+ "status_follow_edge": false,
+ "type": "SMART.TAPOSWITCH"
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 12634,
+ "past7": 4388,
+ "today": 17
+ }
+ },
+ "get_fw_download_state": {
+ "cloud_cache_seconds": 1,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_latest_fw": {
+ "fw_size": 0,
+ "fw_ver": "1.9.0 Build 231106 Rel.164425",
+ "hw_id": "",
+ "need_to_upgrade": false,
+ "oem_id": "",
+ "release_date": "",
+ "release_note": "",
+ "type": 0
+ },
+ "get_next_event": {},
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ },
+ "get_trigger_logs": {
+ "logs": [
+ {
+ "event": "singleClick",
+ "eventId": "85caedf6-73b1-50a8-5cae-df673b150a85",
+ "id": 20079,
+ "params": {
+ "on_off": false
+ },
+ "timestamp": 1735898135
+ }
+ ],
+ "start_id": 20079,
+ "sum": 1
+ }
+}
diff --git a/tests/fixtures/smart/child/S220(EU)_1.0_1.9.0.json b/tests/fixtures/smart/child/S220(EU)_1.0_1.9.0.json
new file mode 100644
index 00000000..ee8e63e6
--- /dev/null
+++ b/tests/fixtures/smart/child/S220(EU)_1.0_1.9.0.json
@@ -0,0 +1,158 @@
+{
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "trigger_log",
+ "ver_code": 1
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "schedule",
+ "ver_code": 2
+ },
+ {
+ "id": "countdown",
+ "ver_code": 2
+ },
+ {
+ "id": "antitheft",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "sunrise_sunset",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "delay_action",
+ "ver_code": 1
+ },
+ {
+ "id": "battery_detect",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ }
+ ]
+ },
+ "get_antitheft_rules": {
+ "antitheft_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_countdown_rules": {
+ "countdown_rule_max_count": 1,
+ "enable": false,
+ "rule_list": []
+ },
+ "get_device_info": {
+ "avatar": "switch",
+ "battery_percentage": 100,
+ "bind_count": 2,
+ "category": "subg.plugswitch.switch",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_4",
+ "device_on": false,
+ "fw_ver": "1.9.0 Build 231106 Rel.164353",
+ "has_set_location_info": true,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "is_low": false,
+ "jamming_rssi": -103,
+ "jamming_signal_level": 2,
+ "lastOnboardingTimestamp": 1733332989,
+ "latitude": 0,
+ "led_off": 0,
+ "longitude": 0,
+ "mac": "D84489000000",
+ "model": "S220",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "original_device_id": "0000000000000000000000000000000000000000",
+ "parent_device_id": "0000000000000000000000000000000000000000",
+ "position": 1,
+ "region": "Europe/London",
+ "rssi": -42,
+ "signal_level": 3,
+ "slot_number": 2,
+ "specs": "EU",
+ "status": "online",
+ "status_follow_edge": false,
+ "type": "SMART.TAPOSWITCH"
+ },
+ "get_device_usage": {
+ "time_usage": {
+ "past30": 1124,
+ "past7": 0,
+ "today": 0
+ }
+ },
+ "get_fw_download_state": {
+ "cloud_cache_seconds": 1,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_latest_fw": {
+ "fw_size": 0,
+ "fw_ver": "1.9.0 Build 231106 Rel.164353",
+ "hw_id": "",
+ "need_to_upgrade": false,
+ "oem_id": "",
+ "release_date": "",
+ "release_note": "",
+ "type": 0
+ },
+ "get_next_event": {},
+ "get_schedule_rules": {
+ "enable": false,
+ "rule_list": [],
+ "schedule_rule_max_count": 32,
+ "start_index": 0,
+ "sum": 0
+ },
+ "get_trigger_logs": {
+ "logs": [],
+ "start_id": 0,
+ "sum": 0
+ }
+}
diff --git a/tests/fixtures/smart/child/T310(EU)_1.0_1.5.0.json b/tests/fixtures/smart/child/T310(EU)_1.0_1.5.0.json
index d48875e5..0d9108ee 100644
--- a/tests/fixtures/smart/child/T310(EU)_1.0_1.5.0.json
+++ b/tests/fixtures/smart/child/T310(EU)_1.0_1.5.0.json
@@ -1,5 +1,5 @@
{
- "component_nego" : {
+ "component_nego": {
"component_list": [
{
"id": "device",
diff --git a/tests/fixtures/smart/child/T315(EU)_1.0_1.7.0.json b/tests/fixtures/smart/child/T315(EU)_1.0_1.7.0.json
index 4fc49b0e..a9fd67e3 100644
--- a/tests/fixtures/smart/child/T315(EU)_1.0_1.7.0.json
+++ b/tests/fixtures/smart/child/T315(EU)_1.0_1.7.0.json
@@ -1,537 +1,537 @@
{
- "component_nego" : {
- "component_list" : [
- {
- "id" : "device",
- "ver_code" : 2
- },
- {
- "id" : "quick_setup",
- "ver_code" : 3
- },
- {
- "id" : "trigger_log",
- "ver_code" : 1
- },
- {
- "id" : "time",
- "ver_code" : 1
- },
- {
- "id" : "device_local_time",
- "ver_code" : 1
- },
- {
- "id" : "account",
- "ver_code" : 1
- },
- {
- "id" : "synchronize",
- "ver_code" : 1
- },
- {
- "id" : "cloud_connect",
- "ver_code" : 1
- },
- {
- "id" : "iot_cloud",
- "ver_code" : 1
- },
- {
- "id" : "firmware",
- "ver_code" : 1
- },
- {
- "id" : "localSmart",
- "ver_code" : 1
- },
- {
- "id" : "battery_detect",
- "ver_code" : 1
- },
- {
- "id" : "temperature",
- "ver_code" : 1
- },
- {
- "id" : "humidity",
- "ver_code" : 1
- },
- {
- "id" : "temp_humidity_record",
- "ver_code" : 1
- },
- {
- "id" : "comfort_temperature",
- "ver_code" : 1
- },
- {
- "id" : "comfort_humidity",
- "ver_code" : 1
- },
- {
- "id" : "report_mode",
- "ver_code" : 1
- }
- ]
- },
- "get_connect_cloud_state" : {
- "status" : 0
- },
- "get_device_info" : {
- "at_low_battery" : false,
- "avatar" : "",
- "battery_percentage" : 100,
- "bind_count" : 1,
- "category" : "subg.trigger.temp-hmdt-sensor",
- "current_humidity" : 61,
- "current_humidity_exception" : 1,
- "current_temp" : 21.4,
- "current_temp_exception" : 0,
- "device_id" : "SCRUBBED_CHILD_DEVICE_ID_1",
- "fw_ver" : "1.7.0 Build 230424 Rel.170332",
- "hw_id" : "00000000000000000000000000000000",
- "hw_ver" : "1.0",
- "jamming_rssi" : -122,
- "jamming_signal_level" : 1,
- "lastOnboardingTimestamp" : 1706990901,
- "mac" : "F0A731000000",
- "model" : "T315",
- "nickname" : "I01BU0tFRF9OQU1FIw==",
- "oem_id" : "00000000000000000000000000000000",
- "parent_device_id" : "0000000000000000000000000000000000000000",
- "region" : "Europe/Berlin",
- "report_interval" : 16,
- "rssi" : -56,
- "signal_level" : 3,
- "specs" : "EU",
- "status" : "online",
- "status_follow_edge" : false,
- "temp_unit" : "celsius",
- "type" : "SMART.TAPOSENSOR"
- },
- "get_fw_download_state" : {
- "cloud_cache_seconds" : 1,
- "download_progress" : 0,
- "reboot_time" : 5,
- "status" : 0,
- "upgrade_time" : 5
- },
- "get_latest_fw" : {
- "fw_ver" : "1.8.0 Build 230921 Rel.091446",
- "hw_id" : "00000000000000000000000000000000",
- "need_to_upgrade" : true,
- "oem_id" : "00000000000000000000000000000000",
- "release_date" : "2023-12-01",
- "release_note" : "Modifications and Bug Fixes:\nEnhance the stability of the sensor.",
- "type" : 2
- },
- "get_temp_humidity_records" : {
- "local_time" : 1709061516,
- "past24h_humidity" : [
- 60,
- 60,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 58,
- 59,
- 59,
- 58,
- 59,
- 59,
- 59,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 60,
- 60,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 59,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 60,
- 64,
- 56,
- 53,
- 55,
- 56,
- 57,
- 57,
- 58,
- 59,
- 63,
- 63,
- 62,
- 62,
- 62,
- 62,
- 61,
- 62,
- 62,
- 61,
- 61
- ],
- "past24h_humidity_exception" : [
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 4,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 3,
- 3,
- 2,
- 2,
- 2,
- 2,
- 1,
- 2,
- 2,
- 1,
- 1
- ],
- "past24h_temp" : [
- 217,
- 216,
- 215,
- 214,
- 214,
- 214,
- 214,
- 214,
- 214,
- 213,
- 213,
- 213,
- 213,
- 213,
- 212,
- 212,
- 211,
- 211,
- 211,
- 211,
- 211,
- 211,
- 212,
- 212,
- 212,
- 211,
- 211,
- 211,
- 211,
- 212,
- 212,
- 212,
- 212,
- 212,
- 211,
- 211,
- 211,
- 212,
- 213,
- 214,
- 214,
- 214,
- 213,
- 212,
- 212,
- 212,
- 212,
- 212,
- 212,
- 212,
- 212,
- 212,
- 212,
- 213,
- 213,
- 213,
- 213,
- 213,
- 213,
- 213,
- 213,
- 213,
- 213,
- 214,
- 214,
- 215,
- 215,
- 215,
- 214,
- 215,
- 216,
- 216,
- 216,
- 216,
- 216,
- 216,
- 216,
- 205,
- 196,
- 210,
- 213,
- 213,
- 213,
- 213,
- 213,
- 214,
- 215,
- 214,
- 214,
- 213,
- 213,
- 214,
- 214,
- 214,
- 213,
- 213
- ],
- "past24h_temp_exception" : [
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- -4,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0
- ],
- "temp_unit" : "celsius"
- },
- "get_trigger_logs" : {
- "logs" : [
- {
- "event" : "tooDry",
- "eventId" : "118040a8-5422-1100-0804-0a8542211000",
- "id" : 1,
- "timestamp" : 1706996915
- }
- ],
- "start_id" : 1,
- "sum" : 1
- }
+ "component_nego": {
+ "component_list": [
+ {
+ "id": "device",
+ "ver_code": 2
+ },
+ {
+ "id": "quick_setup",
+ "ver_code": 3
+ },
+ {
+ "id": "trigger_log",
+ "ver_code": 1
+ },
+ {
+ "id": "time",
+ "ver_code": 1
+ },
+ {
+ "id": "device_local_time",
+ "ver_code": 1
+ },
+ {
+ "id": "account",
+ "ver_code": 1
+ },
+ {
+ "id": "synchronize",
+ "ver_code": 1
+ },
+ {
+ "id": "cloud_connect",
+ "ver_code": 1
+ },
+ {
+ "id": "iot_cloud",
+ "ver_code": 1
+ },
+ {
+ "id": "firmware",
+ "ver_code": 1
+ },
+ {
+ "id": "localSmart",
+ "ver_code": 1
+ },
+ {
+ "id": "battery_detect",
+ "ver_code": 1
+ },
+ {
+ "id": "temperature",
+ "ver_code": 1
+ },
+ {
+ "id": "humidity",
+ "ver_code": 1
+ },
+ {
+ "id": "temp_humidity_record",
+ "ver_code": 1
+ },
+ {
+ "id": "comfort_temperature",
+ "ver_code": 1
+ },
+ {
+ "id": "comfort_humidity",
+ "ver_code": 1
+ },
+ {
+ "id": "report_mode",
+ "ver_code": 1
+ }
+ ]
+ },
+ "get_connect_cloud_state": {
+ "status": 0
+ },
+ "get_device_info": {
+ "at_low_battery": false,
+ "avatar": "",
+ "battery_percentage": 100,
+ "bind_count": 1,
+ "category": "subg.trigger.temp-hmdt-sensor",
+ "current_humidity": 61,
+ "current_humidity_exception": 1,
+ "current_temp": 21.4,
+ "current_temp_exception": 0,
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
+ "fw_ver": "1.7.0 Build 230424 Rel.170332",
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "jamming_rssi": -122,
+ "jamming_signal_level": 1,
+ "lastOnboardingTimestamp": 1706990901,
+ "mac": "F0A731000000",
+ "model": "T315",
+ "nickname": "I01BU0tFRF9OQU1FIw==",
+ "oem_id": "00000000000000000000000000000000",
+ "parent_device_id": "0000000000000000000000000000000000000000",
+ "region": "Europe/Berlin",
+ "report_interval": 16,
+ "rssi": -56,
+ "signal_level": 3,
+ "specs": "EU",
+ "status": "online",
+ "status_follow_edge": false,
+ "temp_unit": "celsius",
+ "type": "SMART.TAPOSENSOR"
+ },
+ "get_fw_download_state": {
+ "cloud_cache_seconds": 1,
+ "download_progress": 0,
+ "reboot_time": 5,
+ "status": 0,
+ "upgrade_time": 5
+ },
+ "get_latest_fw": {
+ "fw_ver": "1.8.0 Build 230921 Rel.091446",
+ "hw_id": "00000000000000000000000000000000",
+ "need_to_upgrade": true,
+ "oem_id": "00000000000000000000000000000000",
+ "release_date": "2023-12-01",
+ "release_note": "Modifications and Bug Fixes:\nEnhance the stability of the sensor.",
+ "type": 2
+ },
+ "get_temp_humidity_records": {
+ "local_time": 1709061516,
+ "past24h_humidity": [
+ 60,
+ 60,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 58,
+ 59,
+ 59,
+ 58,
+ 59,
+ 59,
+ 59,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 60,
+ 60,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 59,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 60,
+ 64,
+ 56,
+ 53,
+ 55,
+ 56,
+ 57,
+ 57,
+ 58,
+ 59,
+ 63,
+ 63,
+ 62,
+ 62,
+ 62,
+ 62,
+ 61,
+ 62,
+ 62,
+ 61,
+ 61
+ ],
+ "past24h_humidity_exception": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 4,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 3,
+ 3,
+ 2,
+ 2,
+ 2,
+ 2,
+ 1,
+ 2,
+ 2,
+ 1,
+ 1
+ ],
+ "past24h_temp": [
+ 217,
+ 216,
+ 215,
+ 214,
+ 214,
+ 214,
+ 214,
+ 214,
+ 214,
+ 213,
+ 213,
+ 213,
+ 213,
+ 213,
+ 212,
+ 212,
+ 211,
+ 211,
+ 211,
+ 211,
+ 211,
+ 211,
+ 212,
+ 212,
+ 212,
+ 211,
+ 211,
+ 211,
+ 211,
+ 212,
+ 212,
+ 212,
+ 212,
+ 212,
+ 211,
+ 211,
+ 211,
+ 212,
+ 213,
+ 214,
+ 214,
+ 214,
+ 213,
+ 212,
+ 212,
+ 212,
+ 212,
+ 212,
+ 212,
+ 212,
+ 212,
+ 212,
+ 212,
+ 213,
+ 213,
+ 213,
+ 213,
+ 213,
+ 213,
+ 213,
+ 213,
+ 213,
+ 213,
+ 214,
+ 214,
+ 215,
+ 215,
+ 215,
+ 214,
+ 215,
+ 216,
+ 216,
+ 216,
+ 216,
+ 216,
+ 216,
+ 216,
+ 205,
+ 196,
+ 210,
+ 213,
+ 213,
+ 213,
+ 213,
+ 213,
+ 214,
+ 215,
+ 214,
+ 214,
+ 213,
+ 213,
+ 214,
+ 214,
+ 214,
+ 213,
+ 213
+ ],
+ "past24h_temp_exception": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ -4,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "temp_unit": "celsius"
+ },
+ "get_trigger_logs": {
+ "logs": [
+ {
+ "event": "tooDry",
+ "eventId": "118040a8-5422-1100-0804-0a8542211000",
+ "id": 1,
+ "timestamp": 1706996915
+ }
+ ],
+ "start_id": 1,
+ "sum": 1
+ }
}
diff --git a/tests/fixtures/smartcam/C100_4.0_1.3.14.json b/tests/fixtures/smartcam/C100_4.0_1.3.14.json
new file mode 100644
index 00000000..144cf5f6
--- /dev/null
+++ b/tests/fixtures/smartcam/C100_4.0_1.3.14.json
@@ -0,0 +1,779 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "C100",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.3.14 Build 240513 Rel.43631n(5553)",
+ "hardware_version": "4.0",
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ },
+ "protocol_version": 1
+ }
+ },
+ "getAlertPlan": {
+ "msg_alarm_plan": {
+ "chn1_msg_alarm_plan": {
+ ".name": "chn1_msg_alarm_plan",
+ ".type": "plan",
+ "alarm_plan_1": "0000-0000,127",
+ "enabled": "off"
+ }
+ }
+ },
+ "getAppComponentList": {
+ "app_component": {
+ "app_component_list": [
+ {
+ "name": "sdCard",
+ "version": 1
+ },
+ {
+ "name": "timezone",
+ "version": 1
+ },
+ {
+ "name": "system",
+ "version": 3
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 4
+ },
+ {
+ "name": "detection",
+ "version": 3
+ },
+ {
+ "name": "alert",
+ "version": 1
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 2
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 2
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "dayNightMode",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 2
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 2
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "babyCryDetection",
+ "version": 1
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ },
+ {
+ "name": "staticIp",
+ "version": 1
+ }
+ ]
+ }
+ },
+ "getAudioConfig": {
+ "audio_config": {
+ "microphone": {
+ ".name": "microphone",
+ ".type": "audio_config",
+ "channels": "1",
+ "encode_type": "G711alaw",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "speaker": {
+ ".name": "speaker",
+ ".type": "audio_config",
+ "volume": "40"
+ }
+ }
+ },
+ "getBCDConfig": {
+ "sound_detection": {
+ "bcd": {
+ ".name": "bcd",
+ ".type": "on_off",
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ ".name": "harddisk",
+ ".type": "storage",
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2024-12-15 11:11:55",
+ "seconds_from_1970": 1734279115
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "4",
+ "rssiValue": -15,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ ".name": "motion_det",
+ ".type": "on_off",
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera c100",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "C100 4.0 IPC",
+ "device_model": "C100",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "features": "3",
+ "ffs": false,
+ "has_set_location_info": 1,
+ "hw_desc": "00000000000000000000000000000000",
+ "hw_version": "4.0",
+ "is_cal": true,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "F0-A7-31-00-00-00",
+ "oem_id": "00000000000000000000000000000000",
+ "sw_version": "1.3.14 Build 240513 Rel.43631n(5553)"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ ".name": "common",
+ ".type": "on_off",
+ "enabled": "off",
+ "random_range": "120",
+ "time": "03:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": true,
+ "state": "normal"
+ }
+ }
+ },
+ "getLastAlarmInfo": {
+ "system": {
+ "last_alarm_info": {
+ "last_alarm_time": "",
+ "last_alarm_type": ""
+ }
+ }
+ },
+ "getLdc": {
+ "image": {
+ "common": {
+ ".name": "common",
+ ".type": "para",
+ "area_compensation": "default",
+ "chroma": "50",
+ "contrast": "50",
+ "dehaze": "off",
+ "exp_gain": "0",
+ "exp_type": "auto",
+ "focus_limited": "600",
+ "focus_type": "semi_auto",
+ "high_light_compensation": "off",
+ "inf_delay": "10",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "1",
+ "inf_start_time": "64800",
+ "inf_type": "auto",
+ "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": "off",
+ "smartir_level": "100",
+ "wb_B_gain": "50",
+ "wb_G_gain": "50",
+ "wb_R_gain": "50",
+ "wb_type": "auto",
+ "wd_gain": "50",
+ "wide_dynamic": "off"
+ },
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getLedStatus": {
+ "led": {
+ "config": {
+ ".name": "config",
+ ".type": "led",
+ "enabled": "on"
+ }
+ }
+ },
+ "getLensMaskConfig": {
+ "lens_mask": {
+ "lens_mask_info": {
+ ".name": "lens_mask_info",
+ ".type": "lens_mask_info",
+ "enabled": "off"
+ }
+ }
+ },
+ "getLightFrequencyInfo": {
+ "image": {
+ "common": {
+ ".name": "common",
+ ".type": "para",
+ "area_compensation": "default",
+ "chroma": "50",
+ "contrast": "50",
+ "dehaze": "off",
+ "exp_gain": "0",
+ "exp_type": "auto",
+ "focus_limited": "600",
+ "focus_type": "semi_auto",
+ "high_light_compensation": "off",
+ "inf_delay": "10",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "1",
+ "inf_start_time": "64800",
+ "inf_type": "auto",
+ "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": "off",
+ "smartir_level": "100",
+ "wb_B_gain": "50",
+ "wb_G_gain": "50",
+ "wb_R_gain": "50",
+ "wb_type": "auto",
+ "wd_gain": "50",
+ "wide_dynamic": "off"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ ".name": "media_encrypt",
+ ".type": "on_off",
+ "enabled": "on"
+ }
+ }
+ },
+ "getMsgPushConfig": {
+ "msg_push": {
+ "chn1_msg_push_info": {
+ ".name": "chn1_msg_push_info",
+ ".type": "on_off",
+ "notification_enabled": "off",
+ "rich_notification_enabled": "off"
+ }
+ }
+ },
+ "getNightVisionModeConfig": {
+ "image": {
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getPersonDetectionConfig": {
+ "people_detection": {
+ "detection": {
+ ".name": "detection",
+ ".type": "on_off",
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getRecordPlan": {
+ "record_plan": {
+ "chn1_channel": {
+ ".name": "chn1_channel",
+ ".type": "plan",
+ "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": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getSdCardStatus": {
+ "harddisk_manage": {
+ "hd_info": [
+ {
+ "hd_info_1": {
+ "detect_status": "offline",
+ "disk_name": "1",
+ "free_space": "0B",
+ "loop_record_status": "0",
+ "msg_push_free_space": "0B",
+ "msg_push_total_space": "0B",
+ "percent": "0",
+ "picture_free_space": "0B",
+ "picture_total_space": "0B",
+ "record_duration": "0",
+ "record_free_duration": "0",
+ "record_start_time": "0",
+ "rw_attr": "r",
+ "status": "offline",
+ "total_space": "0B",
+ "type": "local",
+ "video_free_space": "0B",
+ "video_total_space": "0B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ ".name": "tamper_det",
+ ".type": "on_off",
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ ".name": "basic",
+ ".type": "setting",
+ "timezone": "UTC-05:00",
+ "timing_mode": "manual",
+ "zone_id": "America/New_York"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ ".name": "main",
+ ".type": "capability",
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "2048"
+ ],
+ "encode_types": [
+ "H264"
+ ],
+ "frame_rates": [
+ "65537",
+ "65546",
+ "65551"
+ ],
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "1920*1080",
+ "1280*720",
+ "640*360"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ ".name": "main",
+ ".type": "stream",
+ "bitrate": "1024",
+ "bitrate_type": "vbr",
+ "encode_type": "H264",
+ "frame_rate": "65551",
+ "gop_factor": "2",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "1920*1080",
+ "stream_type": "general"
+ }
+ }
+ },
+ "getWhitelampConfig": {
+ "image": {
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getWhitelampStatus": {
+ "rest_time": 0,
+ "status": 0
+ },
+ "get_audio_capability": {
+ "get": {
+ "audio_capability": {
+ "device_microphone": {
+ ".name": "device_microphone",
+ ".type": "capability",
+ "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": {
+ ".name": "device_speaker",
+ ".type": "capability",
+ "channels": "1",
+ "decode_type": [
+ "G711"
+ ],
+ "mute": "0",
+ "sampling_rate": [
+ "8"
+ ],
+ "volume": "1"
+ }
+ }
+ }
+ },
+ "get_audio_config": {
+ "get": {
+ "audio_config": {
+ "microphone": {
+ ".name": "microphone",
+ ".type": "audio_config",
+ "channels": "1",
+ "encode_type": "G711alaw",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "speaker": {
+ ".name": "speaker",
+ ".type": "audio_config",
+ "volume": "40"
+ }
+ }
+ }
+ },
+ "get_cet": {
+ "get": {
+ "cet": {
+ "vhttpd": {
+ ".name": "vhttpd",
+ ".type": "server",
+ "port": "8800"
+ }
+ }
+ }
+ },
+ "get_function": {
+ "get": {
+ "function": {
+ "module_spec": {
+ ".name": "module_spec",
+ ".type": "module-spec",
+ "ae_weighting_table_resolution": "5*5",
+ "ai_enhance_capability": "1",
+ "app_version": "1.0.0",
+ "audio": [
+ "speaker",
+ "microphone"
+ ],
+ "audioexception_detection": "0",
+ "auth_encrypt": "1",
+ "backlight_coexistence": "1",
+ "change_password": "1",
+ "client_info": "1",
+ "cloud_storage_version": "1.0",
+ "custom_area_compensation": "1",
+ "custom_auto_mode_exposure_level": "0",
+ "device_share": [
+ "preview",
+ "playback",
+ "voice",
+ "cloud_storage"
+ ],
+ "download": [
+ "video"
+ ],
+ "events": [
+ "motion",
+ "tamper"
+ ],
+ "greeter": "1.0",
+ "http_system_state_audio_support": "1",
+ "intrusion_detection": "1",
+ "led": "1",
+ "lens_mask": "1",
+ "linecrossing_detection": "1",
+ "linkage_capability": "1",
+ "local_storage": "1",
+ "media_encrypt": "1",
+ "msg_alarm": "1",
+ "msg_alarm_list": [
+ "sound",
+ "light"
+ ],
+ "msg_alarm_separate_list": [
+ "light",
+ "sound"
+ ],
+ "msg_push": "1",
+ "multi_user": "0",
+ "multicast": "0",
+ "network": [
+ "wifi"
+ ],
+ "ota_upgrade": "1",
+ "p2p_support_versions": [
+ "1.1"
+ ],
+ "playback": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "playback_scale": "1",
+ "preview": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "privacy_mask_api_version": "1.0",
+ "record_max_slot_cnt": "10",
+ "record_type": [
+ "timing",
+ "motion"
+ ],
+ "relay_support_versions": [
+ "1.3"
+ ],
+ "reonboarding": "1",
+ "smart_detection": "1",
+ "smart_msg_push_capability": "1",
+ "ssl_cer_version": "1.0",
+ "storage_api_version": "2.2",
+ "stream_max_sessions": "10",
+ "streaming_support_versions": [
+ "1.0"
+ ],
+ "target_track": "0",
+ "timing_reboot": "1",
+ "verification_change_password": "1",
+ "video_codec": [
+ "h264"
+ ],
+ "video_detection_digital_sensitivity": "1",
+ "wifi_cascade_connection": "1",
+ "wifi_connection_info": "1",
+ "wireless_hotspot": "1"
+ }
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/C210(EU)_2.0_1.4.2.json b/tests/fixtures/smartcam/C210(EU)_2.0_1.4.2.json
index ba2e0010..609c46be 100644
--- a/tests/fixtures/smartcam/C210(EU)_2.0_1.4.2.json
+++ b/tests/fixtures/smartcam/C210(EU)_2.0_1.4.2.json
@@ -1,35 +1,38 @@
{
"discovery_result": {
- "decrypted_data": {
- "connect_ssid": "0000000000",
- "connect_type": "wireless",
- "device_id": "0000000000000000000000000000000000000000",
- "http_port": 443,
- "last_alarm_time": "1729264456",
- "last_alarm_type": "motion",
- "owner": "00000000000000000000000000000000",
- "sd_status": "offline"
- },
- "device_id": "00000000000000000000000000000000",
- "device_model": "C210",
- "device_name": "#MASKED_NAME#",
- "device_type": "SMART.IPCAMERA",
- "encrypt_info": {
- "data": "",
- "key": "",
- "sym_schm": "AES"
- },
- "encrypt_type": [
- "3"
- ],
- "factory_default": false,
- "firmware_version": "1.4.2 Build 240829 Rel.54953n",
- "hardware_version": "2.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
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "1729264456",
+ "last_alarm_type": "motion",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "C210",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.4.2 Build 240829 Rel.54953n",
+ "hardware_version": "2.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": {
diff --git a/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json b/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json
index a2f7666e..d4de5b9f 100644
--- a/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json
+++ b/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json
@@ -1,35 +1,39 @@
{
"discovery_result": {
- "decrypted_data": {
- "connect_ssid": "0000000000",
- "connect_type": "wireless",
- "device_id": "0000000000000000000000000000000000000000",
- "http_port": 443,
- "last_alarm_time": "0",
- "last_alarm_type": "",
- "owner": "00000000000000000000000000000000",
- "sd_status": "offline"
- },
- "device_id": "00000000000000000000000000000000",
- "device_model": "C210",
- "device_name": "#MASKED_NAME#",
- "device_type": "SMART.IPCAMERA",
- "encrypt_info": {
- "data": "",
- "key": "",
- "sym_schm": "AES"
- },
- "encrypt_type": [
- "3"
- ],
- "factory_default": false,
- "firmware_version": "1.4.3 Build 241010 Rel.33858n",
- "hardware_version": "2.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
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "1733422805",
+ "last_alarm_type": "motion",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "C210",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.4.3 Build 241010 Rel.33858n",
+ "hardware_version": "2.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
+ },
+ "protocol_version": 1
}
},
"getAlertConfig": {
@@ -263,15 +267,22 @@
"getClockStatus": {
"system": {
"clock_status": {
- "local_time": "2024-11-01 13:58:50",
- "seconds_from_1970": 1730469530
+ "local_time": "2024-12-15 11:28:40",
+ "seconds_from_1970": 1734262120
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {
+ "status": 0
}
}
},
"getConnectionType": {
"link_type": "wifi",
"rssi": "3",
- "rssiValue": -57,
+ "rssiValue": -61,
"ssid": "I01BU0tFRF9TU0lEIw=="
},
"getDetectionConfig": {
@@ -318,7 +329,7 @@
"getFirmwareAutoUpgradeConfig": {
"auto_upgrade": {
"common": {
- "enabled": "on",
+ "enabled": "off",
"random_range": "120",
"time": "03:00"
}
@@ -335,8 +346,8 @@
"getLastAlarmInfo": {
"system": {
"last_alarm_info": {
- "last_alarm_time": "0",
- "last_alarm_type": ""
+ "last_alarm_time": "1733422805",
+ "last_alarm_type": "motion"
}
}
},
@@ -958,5 +969,35 @@
}
}
}
+ },
+ "scanApList": {
+ "onboarding": {
+ "scan": {
+ "ap_list": [
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "wpa3_supported": "false"
+ }
+ }
}
}
diff --git a/tests/fixtures/smartcam/C210_2.0_1.3.11.json b/tests/fixtures/smartcam/C210_2.0_1.3.11.json
new file mode 100644
index 00000000..9e53bf05
--- /dev/null
+++ b/tests/fixtures/smartcam/C210_2.0_1.3.11.json
@@ -0,0 +1,870 @@
+{
+ "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": "C210",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.3.11 Build 240110 Rel.64341n(4555)",
+ "hardware_version": "2.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
+ }
+ }
+ },
+ "getAlertPlan": {
+ "msg_alarm_plan": {
+ "chn1_msg_alarm_plan": {
+ ".name": "chn1_msg_alarm_plan",
+ ".type": "plan",
+ "alarm_plan_1": "0000-0000,127",
+ "enabled": "off"
+ }
+ }
+ },
+ "getAppComponentList": {
+ "app_component": {
+ "app_component_list": [
+ {
+ "name": "sdCard",
+ "version": 1
+ },
+ {
+ "name": "timezone",
+ "version": 1
+ },
+ {
+ "name": "system",
+ "version": 3
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 4
+ },
+ {
+ "name": "detection",
+ "version": 1
+ },
+ {
+ "name": "alert",
+ "version": 1
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 1
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "ptz",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 2
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "dayNightMode",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 2
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 2
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 1
+ },
+ {
+ "name": "targetTrack",
+ "version": 1
+ },
+ {
+ "name": "babyCryDetection",
+ "version": 1
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ }
+ ]
+ }
+ },
+ "getAudioConfig": {
+ "audio_config": {
+ "microphone": {
+ ".name": "microphone",
+ ".type": "audio_config",
+ "channels": "1",
+ "encode_type": "G711alaw",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "speaker": {
+ ".name": "speaker",
+ ".type": "audio_config",
+ "volume": "100"
+ }
+ }
+ },
+ "getBCDConfig": {
+ "sound_detection": {
+ "bcd": {
+ ".name": "bcd",
+ ".type": "on_off",
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ ".name": "harddisk",
+ ".type": "storage",
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2024-12-24 00:19:08",
+ "seconds_from_1970": 1734999548
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "4",
+ "rssiValue": -39,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ ".name": "motion_det",
+ ".type": "on_off",
+ "digital_sensitivity": "60",
+ "enabled": "on",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera c212",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "C210 2.0 IPC",
+ "device_model": "C210",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "features": "3",
+ "ffs": false,
+ "has_set_location_info": 1,
+ "hw_desc": "00000000000000000000000000000000",
+ "hw_version": "2.0",
+ "is_cal": true,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "40-AE-30-00-00-00",
+ "oem_id": "00000000000000000000000000000000",
+ "sw_version": "1.3.11 Build 240110 Rel.64341n(4555)"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ ".name": "common",
+ ".type": "on_off",
+ "enabled": "off",
+ "random_range": "120",
+ "time": "03:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": false,
+ "state": "normal"
+ }
+ }
+ },
+ "getLastAlarmInfo": {
+ "system": {
+ "last_alarm_info": {
+ "last_alarm_time": "1734967724",
+ "last_alarm_type": "motion"
+ }
+ }
+ },
+ "getLdc": {
+ "image": {
+ "common": {
+ ".name": "common",
+ ".type": "para",
+ "area_compensation": "default",
+ "chroma": "50",
+ "contrast": "50",
+ "dehaze": "off",
+ "exp_gain": "0",
+ "exp_type": "auto",
+ "focus_limited": "600",
+ "focus_type": "semi_auto",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "1",
+ "inf_start_time": "64800",
+ "inf_type": "auto",
+ "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": "off",
+ "smartir_level": "100",
+ "wb_B_gain": "50",
+ "wb_G_gain": "50",
+ "wb_R_gain": "50",
+ "wb_type": "auto",
+ "wd_gain": "50",
+ "wide_dynamic": "off"
+ },
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getLedStatus": {
+ "led": {
+ "config": {
+ ".name": "config",
+ ".type": "led",
+ "enabled": "on"
+ }
+ }
+ },
+ "getLensMaskConfig": {
+ "lens_mask": {
+ "lens_mask_info": {
+ ".name": "lens_mask_info",
+ ".type": "lens_mask_info",
+ "enabled": "on"
+ }
+ }
+ },
+ "getLightFrequencyInfo": {
+ "image": {
+ "common": {
+ ".name": "common",
+ ".type": "para",
+ "area_compensation": "default",
+ "chroma": "50",
+ "contrast": "50",
+ "dehaze": "off",
+ "exp_gain": "0",
+ "exp_type": "auto",
+ "focus_limited": "600",
+ "focus_type": "semi_auto",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "1",
+ "inf_start_time": "64800",
+ "inf_type": "auto",
+ "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": "off",
+ "smartir_level": "100",
+ "wb_B_gain": "50",
+ "wb_G_gain": "50",
+ "wb_R_gain": "50",
+ "wb_type": "auto",
+ "wd_gain": "50",
+ "wide_dynamic": "off"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ ".name": "media_encrypt",
+ ".type": "on_off",
+ "enabled": "on"
+ }
+ }
+ },
+ "getMsgPushConfig": {
+ "msg_push": {
+ "chn1_msg_push_info": {
+ ".name": "chn1_msg_push_info",
+ ".type": "on_off",
+ "notification_enabled": "on",
+ "rich_notification_enabled": "off"
+ }
+ }
+ },
+ "getNightVisionModeConfig": {
+ "image": {
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getPersonDetectionConfig": {
+ "people_detection": {
+ "detection": {
+ ".name": "detection",
+ ".type": "on_off",
+ "enabled": "on"
+ }
+ }
+ },
+ "getPresetConfig": {
+ "preset": {
+ "preset": {
+ "id": [
+ "1"
+ ],
+ "name": [
+ "Viewpoint 1"
+ ],
+ "position_pan": [
+ "-0.176836"
+ ],
+ "position_tilt": [
+ "-0.859297"
+ ],
+ "read_only": [
+ "0"
+ ]
+ }
+ }
+ },
+ "getRecordPlan": {
+ "record_plan": {
+ "chn1_channel": {
+ ".name": "chn1_channel",
+ ".type": "plan",
+ "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": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getSdCardStatus": {
+ "harddisk_manage": {
+ "hd_info": [
+ {
+ "hd_info_1": {
+ "detect_status": "offline",
+ "disk_name": "1",
+ "free_space": "0B",
+ "loop_record_status": "0",
+ "msg_push_free_space": "0B",
+ "msg_push_total_space": "0B",
+ "percent": "0",
+ "picture_free_space": "0B",
+ "picture_total_space": "0B",
+ "record_duration": "0",
+ "record_free_duration": "0",
+ "record_start_time": "0",
+ "rw_attr": "r",
+ "status": "offline",
+ "total_space": "0B",
+ "type": "local",
+ "video_free_space": "0B",
+ "video_total_space": "0B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ ".name": "tamper_det",
+ ".type": "on_off",
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTargetTrackConfig": {
+ "target_track": {
+ "target_track_info": {
+ ".name": "target_track_info",
+ ".type": "target_track_info",
+ "enabled": "off"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ ".name": "basic",
+ ".type": "setting",
+ "timezone": "UTC-00:00",
+ "timing_mode": "ntp",
+ "zone_id": "Europe/London"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ ".name": "main",
+ ".type": "capability",
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "2048"
+ ],
+ "encode_types": [
+ "H264"
+ ],
+ "frame_rates": [
+ "65537",
+ "65546",
+ "65551"
+ ],
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "2304*1296",
+ "1920*1080",
+ "1280*720"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ ".name": "main",
+ ".type": "stream",
+ "bitrate": "2048",
+ "bitrate_type": "vbr",
+ "encode_type": "H264",
+ "frame_rate": "65551",
+ "gop_factor": "2",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "1920*1080",
+ "stream_type": "general"
+ }
+ }
+ },
+ "getWhitelampConfig": {
+ "image": {
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getWhitelampStatus": {
+ "rest_time": 0,
+ "status": 0
+ },
+ "get_audio_capability": {
+ "get": {
+ "audio_capability": {
+ "device_microphone": {
+ ".name": "device_microphone",
+ ".type": "capability",
+ "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": {
+ ".name": "device_speaker",
+ ".type": "capability",
+ "channels": "1",
+ "decode_type": [
+ "G711"
+ ],
+ "mute": "0",
+ "sampling_rate": [
+ "8"
+ ],
+ "volume": "1"
+ }
+ }
+ }
+ },
+ "get_audio_config": {
+ "get": {
+ "audio_config": {
+ "microphone": {
+ ".name": "microphone",
+ ".type": "audio_config",
+ "channels": "1",
+ "encode_type": "G711alaw",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "speaker": {
+ ".name": "speaker",
+ ".type": "audio_config",
+ "volume": "100"
+ }
+ }
+ }
+ },
+ "get_cet": {
+ "get": {
+ "cet": {
+ "vhttpd": {
+ ".name": "vhttpd",
+ ".type": "server",
+ "port": "8800"
+ }
+ }
+ }
+ },
+ "get_function": {
+ "get": {
+ "function": {
+ "module_spec": {
+ ".name": "module_spec",
+ ".type": "module-spec",
+ "ae_weighting_table_resolution": "5*5",
+ "ai_enhance_capability": "1",
+ "app_version": "1.0.0",
+ "audio": [
+ "speaker",
+ "microphone"
+ ],
+ "audioexception_detection": "0",
+ "auth_encrypt": "1",
+ "backlight_coexistence": "1",
+ "change_password": "1",
+ "client_info": "1",
+ "cloud_storage_version": "1.0",
+ "custom_area_compensation": "1",
+ "custom_auto_mode_exposure_level": "0",
+ "device_share": [
+ "preview",
+ "playback",
+ "voice",
+ "motor",
+ "cloud_storage"
+ ],
+ "download": [
+ "video"
+ ],
+ "events": [
+ "motion",
+ "tamper"
+ ],
+ "greeter": "1.0",
+ "http_system_state_audio_support": "1",
+ "intrusion_detection": "1",
+ "led": "1",
+ "lens_mask": "1",
+ "linecrossing_detection": "1",
+ "linkage_capability": "1",
+ "local_storage": "1",
+ "media_encrypt": "1",
+ "msg_alarm": "1",
+ "msg_alarm_list": [
+ "sound",
+ "light"
+ ],
+ "msg_alarm_separate_list": [
+ "light",
+ "sound"
+ ],
+ "msg_push": "1",
+ "multi_user": "0",
+ "multicast": "0",
+ "network": [
+ "wifi"
+ ],
+ "ota_upgrade": "1",
+ "p2p_support_versions": [
+ "1.1"
+ ],
+ "playback": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "playback_scale": "1",
+ "preview": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "privacy_mask_api_version": "1.0",
+ "ptz": "1",
+ "record_max_slot_cnt": "10",
+ "record_type": [
+ "timing",
+ "motion"
+ ],
+ "relay_support_versions": [
+ "1.3"
+ ],
+ "reonboarding": "1",
+ "smart_detection": "1",
+ "smart_msg_push_capability": "1",
+ "ssl_cer_version": "1.0",
+ "storage_api_version": "2.2",
+ "stream_max_sessions": "10",
+ "streaming_support_versions": [
+ "1.0"
+ ],
+ "target_track": "1.0",
+ "timing_reboot": "1",
+ "verification_change_password": "1",
+ "video_codec": [
+ "h264"
+ ],
+ "video_detection_digital_sensitivity": "1",
+ "wifi_cascade_connection": "1",
+ "wifi_connection_info": "1",
+ "wireless_hotspot": "1"
+ }
+ }
+ }
+ },
+ "get_motor": {
+ "get": {
+ "motor": {
+ "capability": {
+ ".name": "capability",
+ ".type": "ptz",
+ "absolute_move_supported": "1",
+ "calibrate_supported": "1",
+ "continuous_move_supported": "1",
+ "eflip_mode": [
+ "off",
+ "on"
+ ],
+ "home_position_mode": "none",
+ "limit_supported": "0",
+ "manual_control_level": [
+ "low",
+ "normal",
+ "high"
+ ],
+ "manual_control_mode": [
+ "compatible",
+ "pedestrian",
+ "motor_vehicle",
+ "non_motor_vehicle",
+ "self_adaptive"
+ ],
+ "park_supported": "0",
+ "pattern_supported": "0",
+ "plan_supported": "0",
+ "position_pan_range": [
+ "-1.000000",
+ "1.000000"
+ ],
+ "position_tilt_range": [
+ "-1.000000",
+ "1.000000"
+ ],
+ "poweroff_save_supported": "1",
+ "poweroff_save_time_range": [
+ "10",
+ "600"
+ ],
+ "preset_number_max": "8",
+ "preset_supported": "1",
+ "relative_move_supported": "1",
+ "reverse_mode": [
+ "off",
+ "on",
+ "auto"
+ ],
+ "scan_supported": "0",
+ "speed_pan_max": "1.00000",
+ "speed_tilt_max": "1.000000",
+ "tour_supported": "0"
+ }
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/C220(EU)_1.0_1.2.2.json b/tests/fixtures/smartcam/C220(EU)_1.0_1.2.2.json
new file mode 100644
index 00000000..617acd74
--- /dev/null
+++ b/tests/fixtures/smartcam/C220(EU)_1.0_1.2.2.json
@@ -0,0 +1,1234 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "0",
+ "last_alarm_type": "",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "C220",
+ "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.2 Build 240914 Rel.55174n",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "isResetWiFi": false,
+ "is_support_iot_cloud": true,
+ "mac": "B0-19-21-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ },
+ "protocol_version": 1
+ }
+ },
+ "getAlertConfig": {
+ "msg_alarm": {
+ "capability": {
+ "alarm_duration_support": "1",
+ "alarm_volume_support": "1",
+ "alert_event_type_support": "1",
+ "usr_def_audio_alarm_max_num": "2",
+ "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": [
+ "light",
+ "sound"
+ ],
+ "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": 3
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 6
+ },
+ {
+ "name": "detection",
+ "version": 3
+ },
+ {
+ "name": "alert",
+ "version": 2
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 2
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "ptz",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 3
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "dayNightMode",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 2
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 3
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "linecrossingDetection",
+ "version": 2
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tamperDetection",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "targetTrack",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "babyCryDetection",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "patrol",
+ "version": 1
+ },
+ {
+ "name": "vehicleDetection",
+ "version": 1
+ },
+ {
+ "name": "petDetection",
+ "version": 1
+ },
+ {
+ "name": "meowDetection",
+ "version": 1
+ },
+ {
+ "name": "barkDetection",
+ "version": 1
+ },
+ {
+ "name": "glassDetection",
+ "version": 1
+ },
+ {
+ "name": "markerBox",
+ "version": 1
+ },
+ {
+ "name": "nvmp",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "panoramicView",
+ "version": 1
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "smartTrack",
+ "version": 1
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ },
+ {
+ "name": "staticIp",
+ "version": 2
+ },
+ {
+ "name": "snapshot",
+ "version": 2
+ },
+ {
+ "name": "timeFormat",
+ "version": 1
+ },
+ {
+ "name": "upnpc",
+ "version": 2
+ }
+ ]
+ }
+ },
+ "getAudioConfig": {
+ "audio_config": {
+ "microphone": {
+ "bitrate": "64",
+ "channels": "1",
+ "echo_cancelling": "off",
+ "encode_type": "G711alaw",
+ "factory_noise_cancelling": "off",
+ "input_device_type": "MicIn",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "speaker": {
+ "mute": "off",
+ "output_device_type": "SpeakerOut",
+ "volume": "100"
+ }
+ }
+ },
+ "getBCDConfig": {
+ "sound_detection": {
+ "bcd": {
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getBarkDetectionConfig": {
+ "bark_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "50"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2025-01-18 13:54:46",
+ "seconds_from_1970": 1737204886
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {
+ "status": 0
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "4",
+ "rssiValue": -37,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ "digital_sensitivity": "60",
+ "enabled": "on",
+ "non_vehicle_enabled": "off",
+ "people_enabled": "off",
+ "sensitivity": "high",
+ "vehicle_enabled": "off"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera c212",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "C220 1.0 IPC",
+ "device_model": "C220",
+ "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,
+ "mac": "B0-19-21-00-00-00",
+ "manufacturer_name": "TP-LINK",
+ "mobile_access": "0",
+ "no_rtsp_constrain": 1,
+ "oem_id": "00000000000000000000000000000000",
+ "region": "EU",
+ "sw_version": "1.2.2 Build 240914 Rel.55174n"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ "enabled": "on",
+ "random_range": "120",
+ "time": "03:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": true,
+ "state": "normal"
+ }
+ }
+ },
+ "getGlassDetectionConfig": {
+ "glass_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "50"
+ }
+ }
+ },
+ "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": "100",
+ "exp_level": "0",
+ "exp_type": "auto",
+ "focus_limited": "10",
+ "focus_type": "manual",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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": "50",
+ "smartwtl_level": "3",
+ "style": "standard",
+ "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_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": "30",
+ "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": "1"
+ }
+ }
+ },
+ "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": "100",
+ "exp_level": "0",
+ "exp_type": "auto",
+ "focus_limited": "10",
+ "focus_type": "manual",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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": "50",
+ "smartwtl_level": "3",
+ "style": "standard",
+ "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_day2night": "1400",
+ "wtl_sensitivity_night2day": "9100",
+ "wtl_start_time": "64800",
+ "wtl_type": "auto"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ "enabled": "on"
+ }
+ }
+ },
+ "getMeowDetectionConfig": {
+ "meow_detection": {
+ "detection": {
+ "enabled": "on",
+ "sensitivity": "50"
+ }
+ }
+ },
+ "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"
+ ],
+ "supplement_lamp_type": [
+ "infrared_lamp"
+ ]
+ }
+ }
+ },
+ "getNightVisionModeConfig": {
+ "image": {
+ "switch": {
+ "best_view_distance": "0",
+ "clear_licence_plate_mode": "off",
+ "flip_type": "off",
+ "full_color_min_keep_time": "30",
+ "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": "1"
+ }
+ }
+ },
+ "getPersonDetectionConfig": {
+ "people_detection": {
+ "detection": {
+ "enabled": "on",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getPetDetectionConfig": {
+ "pet_detection": {
+ "detection": {
+ "enabled": "on",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getPresetConfig": {
+ "preset": {
+ "preset": {
+ "id": [
+ "1",
+ "2"
+ ],
+ "name": [
+ "Viewpoint 1",
+ "Viewpoint 2"
+ ],
+ "position_pan": [
+ "-0.122544",
+ "0.172182"
+ ],
+ "position_tilt": [
+ "1.000000",
+ "1.000000"
+ ],
+ "position_zoom": [],
+ "read_only": [
+ "0",
+ "0"
+ ]
+ }
+ }
+ },
+ "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": "30",
+ "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": "1"
+ }
+ }
+ },
+ "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": "offline",
+ "disk_name": "1",
+ "free_space": "0B",
+ "free_space_accurate": "0B",
+ "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",
+ "percent": "0",
+ "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": "0",
+ "rw_attr": "r",
+ "status": "offline",
+ "total_space": "0B",
+ "total_space_accurate": "0B",
+ "type": "local",
+ "video_free_space": "0B",
+ "video_free_space_accurate": "0B",
+ "video_total_space": "0B",
+ "video_total_space_accurate": "0B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTargetTrackConfig": {
+ "target_track": {
+ "target_track_info": {
+ "back_time": "30",
+ "enabled": "off",
+ "track_mode": "pantilt",
+ "track_time": "0"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ "timezone": "UTC+01:00",
+ "timing_mode": "manual",
+ "zone_id": "Europe/Sarajevo"
+ }
+ }
+ },
+ "getVehicleDetectionConfig": {
+ "vehicle_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "1536",
+ "2048",
+ "2560"
+ ],
+ "change_fps_support": "1",
+ "encode_types": [
+ "H264",
+ "H265"
+ ],
+ "frame_rates": [
+ "65551",
+ "65556",
+ "65561"
+ ],
+ "minor_stream_support": "1",
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "2560*1440",
+ "1920*1080"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ "bitrate": "2560",
+ "bitrate_type": "vbr",
+ "default_bitrate": "2560",
+ "encode_type": "H264",
+ "frame_rate": "65561",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "2560*1440",
+ "smart_codec": "off"
+ }
+ }
+ },
+ "getWhitelampConfig": {
+ "image": {
+ "switch": {
+ "best_view_distance": "0",
+ "clear_licence_plate_mode": "off",
+ "flip_type": "off",
+ "full_color_min_keep_time": "30",
+ "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": "1"
+ }
+ }
+ },
+ "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": "80",
+ "volume": "1"
+ }
+ }
+ }
+ },
+ "get_audio_config": {
+ "get": {
+ "audio_config": {
+ "microphone": {
+ "bitrate": "64",
+ "channels": "1",
+ "echo_cancelling": "off",
+ "encode_type": "G711alaw",
+ "factory_noise_cancelling": "off",
+ "input_device_type": "MicIn",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "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",
+ "motor"
+ ],
+ "download": [
+ "video"
+ ],
+ "events": [
+ "motion",
+ "tamper"
+ ],
+ "force_iframe_support": "1",
+ "http_system_state_audio_support": "1",
+ "image_capability": "1",
+ "image_list": [
+ "supplement_lamp",
+ "expose"
+ ],
+ "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": "1",
+ "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": "1",
+ "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": 2,
+ "rssi": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "wpa3_supported": "true"
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/C225(US)_2.0_1.0.11.json b/tests/fixtures/smartcam/C225(US)_2.0_1.0.11.json
new file mode 100644
index 00000000..24227c41
--- /dev/null
+++ b/tests/fixtures/smartcam/C225(US)_2.0_1.0.11.json
@@ -0,0 +1,1283 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "1734729039",
+ "last_alarm_type": "motion",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "normal"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "C225",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.0.11 Build 240826 Rel.62730n",
+ "hardware_version": "2.0",
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-42-A1-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ },
+ "obd_src": "tplink"
+ }
+ },
+ "getAlertConfig": {
+ "msg_alarm": {
+ "capability": {
+ "alarm_duration_support": "1",
+ "alarm_volume_support": "1",
+ "alert_event_type_support": "1",
+ "usr_def_audio_alarm_max_num": "15",
+ "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": [
+ "light",
+ "sound"
+ ],
+ "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": 3
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 6
+ },
+ {
+ "name": "detection",
+ "version": 3
+ },
+ {
+ "name": "alert",
+ "version": 2
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 2
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "ptz",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 3
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "dayNightMode",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 2
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 3
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "linecrossingDetection",
+ "version": 2
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tamperDetection",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "targetTrack",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "babyCryDetection",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "patrol",
+ "version": 1
+ },
+ {
+ "name": "panoramicView",
+ "version": 1
+ },
+ {
+ "name": "vehicleDetection",
+ "version": 1
+ },
+ {
+ "name": "petDetection",
+ "version": 1
+ },
+ {
+ "name": "meowDetection",
+ "version": 1
+ },
+ {
+ "name": "barkDetection",
+ "version": 1
+ },
+ {
+ "name": "glassDetection",
+ "version": 1
+ },
+ {
+ "name": "alarmDetection",
+ "version": 1
+ },
+ {
+ "name": "infLamp",
+ "version": 1
+ },
+ {
+ "name": "markerBox",
+ "version": 1
+ },
+ {
+ "name": "nvmp",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "hdr",
+ "version": 1
+ },
+ {
+ "name": "homekit",
+ "version": 1
+ },
+ {
+ "name": "upnpc",
+ "version": 2
+ },
+ {
+ "name": "bleOnboarding",
+ "version": 2
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "smartTrack",
+ "version": 1
+ },
+ {
+ "name": "encryption",
+ "version": 3
+ },
+ {
+ "name": "staticIp",
+ "version": 2
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ }
+ ]
+ }
+ },
+ "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": "80"
+ }
+ }
+ },
+ "getBCDConfig": {
+ "sound_detection": {
+ "bcd": {
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getBarkDetectionConfig": {
+ "bark_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "50"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2024-12-20 15:15:46",
+ "seconds_from_1970": 1734736546
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {
+ "status": 0
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "4",
+ "rssiValue": -9,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ "digital_sensitivity": "60",
+ "enabled": "off",
+ "non_vehicle_enabled": "off",
+ "people_enabled": "off",
+ "sensitivity": "medium",
+ "vehicle_enabled": "off"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera c225",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "C225 2.0 IPC",
+ "device_model": "C225",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "features": 3,
+ "ffs": false,
+ "has_set_location_info": 0,
+ "hw_desc": "00000000000000000000000000000000",
+ "hw_id": "00000000000000000000000000000000",
+ "hw_version": "2.0",
+ "is_cal": true,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "A8-42-A1-00-00-00",
+ "manufacturer_name": "TP-LINK",
+ "mobile_access": "0",
+ "no_rtsp_constrain": 1,
+ "oem_id": "00000000000000000000000000000000",
+ "region": "US",
+ "sw_version": "1.0.11 Build 240826 Rel.62730n"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ "enabled": "on",
+ "random_range": "120",
+ "time": "03:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": true,
+ "state": "normal"
+ }
+ }
+ },
+ "getGlassDetectionConfig": {
+ "glass_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "50"
+ }
+ }
+ },
+ "getLastAlarmInfo": {
+ "system": {
+ "last_alarm_info": {
+ "last_alarm_time": "1734729039",
+ "last_alarm_type": "motion"
+ }
+ }
+ },
+ "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": "100",
+ "exp_level": "0",
+ "exp_type": "auto",
+ "focus_limited": "10",
+ "focus_type": "manual",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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": "50",
+ "smartwtl_level": "3",
+ "style": "standard",
+ "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_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": "30",
+ "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": "5",
+ "wtl_manual_start_flag": "off"
+ }
+ }
+ },
+ "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": "100",
+ "exp_level": "0",
+ "exp_type": "auto",
+ "focus_limited": "10",
+ "focus_type": "manual",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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": "50",
+ "smartwtl_level": "3",
+ "style": "standard",
+ "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_day2night": "1400",
+ "wtl_sensitivity_night2day": "9100",
+ "wtl_start_time": "64800",
+ "wtl_type": "auto"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ "enabled": "on"
+ }
+ }
+ },
+ "getMeowDetectionConfig": {
+ "meow_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "50"
+ }
+ }
+ },
+ "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"
+ ],
+ "supplement_lamp_type": [
+ "infrared_lamp"
+ ]
+ }
+ }
+ },
+ "getNightVisionModeConfig": {
+ "image": {
+ "switch": {
+ "best_view_distance": "0",
+ "clear_licence_plate_mode": "off",
+ "flip_type": "off",
+ "full_color_min_keep_time": "30",
+ "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": "5",
+ "wtl_manual_start_flag": "off"
+ }
+ }
+ },
+ "getPersonDetectionConfig": {
+ "people_detection": {
+ "detection": {
+ "enabled": "on",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getPetDetectionConfig": {
+ "pet_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getPresetConfig": {
+ "preset": {
+ "preset": {
+ "id": [],
+ "name": [],
+ "position_pan": [],
+ "position_tilt": [],
+ "position_zoom": [],
+ "read_only": []
+ }
+ }
+ },
+ "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": "30",
+ "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": "5",
+ "wtl_manual_start_flag": "off"
+ }
+ }
+ },
+ "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": "98.6GB",
+ "free_space_accurate": "105903970616B",
+ "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",
+ "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": "1729454840",
+ "rw_attr": "rw",
+ "status": "normal",
+ "total_space": "118.8GB",
+ "total_space_accurate": "127531646976B",
+ "type": "local",
+ "video_free_space": "98.6GB",
+ "video_free_space_accurate": "105903970616B",
+ "video_total_space": "114.0GB",
+ "video_total_space_accurate": "122406567936B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTargetTrackConfig": {
+ "target_track": {
+ "target_track_info": {
+ "back_time": "30",
+ "enabled": "off",
+ "track_mode": "pantilt",
+ "track_time": "0"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ "timezone": "UTC-08:00",
+ "timing_mode": "ntp",
+ "zone_id": "America/Los_Angeles"
+ }
+ }
+ },
+ "getVehicleDetectionConfig": {
+ "vehicle_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "1536",
+ "2048"
+ ],
+ "change_fps_support": "1",
+ "encode_types": [
+ "H264",
+ "H265"
+ ],
+ "frame_rates": [
+ "65551",
+ "65556",
+ "65561",
+ "65566"
+ ],
+ "hdrs": [
+ "0",
+ "1"
+ ],
+ "minor_stream_support": "1",
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "2688*1520",
+ "1920*1080"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ "bitrate": "2048",
+ "bitrate_type": "vbr",
+ "default_bitrate": "2048",
+ "encode_type": "H264",
+ "frame_rate": "65551",
+ "hdr": "0",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "2688*1520",
+ "smart_codec": "off"
+ }
+ }
+ },
+ "getWhitelampConfig": {
+ "image": {
+ "switch": {
+ "best_view_distance": "0",
+ "clear_licence_plate_mode": "off",
+ "flip_type": "off",
+ "full_color_min_keep_time": "30",
+ "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": "5",
+ "wtl_manual_start_flag": "off"
+ }
+ }
+ },
+ "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": "80",
+ "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": "80"
+ }
+ }
+ }
+ },
+ "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",
+ "motor"
+ ],
+ "download": [
+ "video"
+ ],
+ "events": [
+ "motion",
+ "tamper"
+ ],
+ "force_iframe_support": "1",
+ "http_system_state_audio_support": "1",
+ "image_capability": "1",
+ "image_list": [
+ "supplement_lamp",
+ "expose"
+ ],
+ "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": "1",
+ "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": "1",
+ "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": 2,
+ "rssi": 4,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 4,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 4,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 4,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 4,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 4,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 4,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 4,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 5,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "wpa3_supported": "true"
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/C325WB(EU)_1.0_1.1.17.json b/tests/fixtures/smartcam/C325WB(EU)_1.0_1.1.17.json
new file mode 100644
index 00000000..b04cbd06
--- /dev/null
+++ b/tests/fixtures/smartcam/C325WB(EU)_1.0_1.1.17.json
@@ -0,0 +1,1065 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "1734490369",
+ "last_alarm_type": "motion",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "normal"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "C325WB",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.1.17 Build 240529 Rel.57938n",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "isResetWiFi": false,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ }
+ }
+ },
+ "getAlertConfig": {
+ "msg_alarm": {
+ "capability": {
+ "alarm_duration_support": "1",
+ "alarm_volume_support": "1",
+ "alert_event_type_support": "1",
+ "usr_def_audio_alarm_max_num": "2",
+ "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": "account",
+ "version": 1
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 3
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 2
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 3
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "linecrossingDetection",
+ "version": 2
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tamperDetection",
+ "version": 1
+ },
+ {
+ "name": "darkLightNightVision",
+ "version": 3
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "vehicleDetection",
+ "version": 1
+ },
+ {
+ "name": "petDetection",
+ "version": 1
+ },
+ {
+ "name": "whiteLamp",
+ "version": 1
+ },
+ {
+ "name": "markerBox",
+ "version": 1
+ },
+ {
+ "name": "nvmp",
+ "version": 1
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "snapshot",
+ "version": 2
+ },
+ {
+ "name": "upnpc",
+ "version": 2
+ }
+ ]
+ }
+ },
+ "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": "0"
+ },
+ "speaker": {
+ "mute": "off",
+ "output_device_type": "SpeakerOut",
+ "volume": "100"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2024-12-18 12:59:13",
+ "seconds_from_1970": 1734490753
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {
+ "status": 0
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "2",
+ "rssiValue": -63,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ "digital_sensitivity": "60",
+ "enabled": "on",
+ "non_vehicle_enabled": "off",
+ "people_enabled": "off",
+ "sensitivity": "medium",
+ "vehicle_enabled": "off"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera c100",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "C325WB 1.0 IPC",
+ "device_model": "C325WB",
+ "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,
+ "mac": "F0-A7-31-00-00-00",
+ "manufacturer_name": "TP-LINK",
+ "mobile_access": "0",
+ "oem_id": "00000000000000000000000000000000",
+ "region": "EU",
+ "sw_version": "1.1.17 Build 240529 Rel.57938n"
+ }
+ }
+ },
+ "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": "1734490369",
+ "last_alarm_type": "motion"
+ }
+ }
+ },
+ "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": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "inf_start_time": "64800",
+ "inf_type": "off",
+ "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": "standard",
+ "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_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": "wtl_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": "30"
+ }
+ }
+ },
+ "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": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "inf_start_time": "64800",
+ "inf_type": "off",
+ "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": "standard",
+ "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_day2night": "1400",
+ "wtl_sensitivity_night2day": "9100",
+ "wtl_start_time": "64800",
+ "wtl_type": "auto"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ "enabled": "on"
+ }
+ }
+ },
+ "getMsgPushConfig": {
+ "msg_push": {
+ "chn1_msg_push_info": {
+ "notification_enabled": "off",
+ "rich_notification_enabled": "off"
+ }
+ }
+ },
+ "getNightVisionCapability": {
+ "image_capability": {
+ "supplement_lamp": {
+ "night_vision_mode_range": [
+ "wtl_night_vision",
+ "md_night_vision",
+ "shed_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": "wtl_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": "30"
+ }
+ }
+ },
+ "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": "wtl_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": "30"
+ }
+ }
+ },
+ "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": "0B",
+ "free_space_accurate": "0B",
+ "loop_record_status": "1",
+ "msg_push_free_space": "0B",
+ "msg_push_free_space_accurate": "0B",
+ "msg_push_total_space": "0B",
+ "msg_push_total_space_accurate": "0B",
+ "percent": "0",
+ "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": "1733281333",
+ "rw_attr": "rw",
+ "status": "normal",
+ "total_space": "118.8GB",
+ "total_space_accurate": "127565725696B",
+ "type": "local",
+ "video_free_space": "0B",
+ "video_free_space_accurate": "0B",
+ "video_total_space": "114.0GB",
+ "video_total_space_accurate": "122406567936B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ "timezone": "UTC+10:00",
+ "timing_mode": "ntp",
+ "zone_id": "Australia/Brisbane"
+ }
+ }
+ },
+ "getVehicleDetectionConfig": {
+ "vehicle_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "1536",
+ "3072"
+ ],
+ "encode_types": [
+ "H264",
+ "H265"
+ ],
+ "frame_rates": [
+ "65537",
+ "65546",
+ "65551",
+ "65556"
+ ],
+ "hdrs": [
+ "0",
+ "1"
+ ],
+ "minor_stream_support": "1",
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "2688*1520",
+ "1920*1080",
+ "1280*720"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ "bitrate": "1536",
+ "bitrate_type": "vbr",
+ "default_bitrate": "3072",
+ "encode_type": "H264",
+ "frame_rate": "65556",
+ "hdr": "0",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "2688*1520",
+ "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": "wtl_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": "30"
+ }
+ }
+ },
+ "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",
+ "G711ulaw"
+ ],
+ "mute": "0",
+ "output_device_type": "0",
+ "sampling_rate": [
+ "8",
+ "16"
+ ],
+ "system_volume": "95",
+ "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": "0"
+ },
+ "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"
+ ],
+ "alarm_out_num": "0",
+ "app_version": "1.0.0",
+ "audio": [
+ "speaker",
+ "microphone"
+ ],
+ "audio_alarm_clock": "1",
+ "audio_lib": "1",
+ "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",
+ "image",
+ "OSD",
+ "video"
+ ],
+ "custom_area_compensation": "1",
+ "daynight_subdivision": "1",
+ "device_share": [
+ "preview",
+ "playback",
+ "voice",
+ "cloud_storage",
+ "motor"
+ ],
+ "diagnose": "1",
+ "diagnose_capability": "1",
+ "download": [
+ "video"
+ ],
+ "events": [
+ "motion",
+ "tamper"
+ ],
+ "gb28181": "1",
+ "greeter": "1.0",
+ "http_system_state_audio_support": "1",
+ "image_capability": "1",
+ "image_list": [
+ "supplement_lamp",
+ "expose"
+ ],
+ "led": "1",
+ "lens_mask": "1",
+ "linkage_capability": "1",
+ "local_storage": "1",
+ "media_encrypt": "1",
+ "msg_alarm_list": [
+ "sound",
+ "light"
+ ],
+ "msg_push": "1",
+ "multi_user": "0",
+ "network": [
+ "wifi",
+ "ethernet"
+ ],
+ "osd_capability": "1",
+ "ota_upgrade": "1",
+ "p2p_support_versions": [
+ "2.0"
+ ],
+ "playback": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "playback_scale": "1",
+ "playback_version": "1.1",
+ "preview": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "privacy_mask_api_version": "1.0",
+ "ptz": "1",
+ "record_max_slot_cnt": "10",
+ "record_type": [
+ "timing",
+ "motion"
+ ],
+ "relay_support_versions": [
+ "2.0"
+ ],
+ "remote_upgrade": "1",
+ "reonboarding": "1",
+ "smart_codec": "0",
+ "smart_detection": "1",
+ "smart_msg_push_capability": "1",
+ "ssl_cer_version": "1.0",
+ "storage_api_version": "2.2",
+ "stream_max_sessions": "10",
+ "streaming_support_versions": [
+ "2.0"
+ ],
+ "tapo_care_version": "1.0.0",
+ "target_track": "1",
+ "timing_reboot": "1",
+ "tums": "1",
+ "tums_config": "1",
+ "tums_msg_push": "1",
+ "verification_change_password": "1",
+ "video_codec": [
+ "h264",
+ "h265"
+ ],
+ "video_detection_digital_sensitivity": "1",
+ "video_message": "1",
+ "wide_range_inf_sensitivity": "1",
+ "wifi_cascade_connection": "0",
+ "wifi_connection_info": "1",
+ "wireless_hotspot": "0"
+ }
+ }
+ }
+ },
+ "scanApList": {
+ "onboarding": {
+ "scan": {
+ "ap_list": [
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 0,
+ "bssid": "000000000000",
+ "encryption": 0,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 0,
+ "bssid": "000000000000",
+ "encryption": 0,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 5,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 0,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "wpa3_supported": "false"
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/C520WS(US)_1.0_1.2.8.json b/tests/fixtures/smartcam/C520WS(US)_1.0_1.2.8.json
new file mode 100644
index 00000000..c425da79
--- /dev/null
+++ b/tests/fixtures/smartcam/C520WS(US)_1.0_1.2.8.json
@@ -0,0 +1,1150 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "1734386954",
+ "last_alarm_type": "motion",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "C520WS",
+ "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.8 Build 240606 Rel.39146n",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "isResetWiFi": false,
+ "is_support_iot_cloud": true,
+ "mac": "F0-A7-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ }
+ }
+ },
+ "getAlertConfig": {
+ "msg_alarm": {
+ "capability": {
+ "alarm_duration_support": "1",
+ "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": 3
+ },
+ {
+ "name": "detection",
+ "version": 3
+ },
+ {
+ "name": "alert",
+ "version": 2
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 2
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "ptz",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 2
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "dayNightMode",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 2
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 3
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "linecrossingDetection",
+ "version": 2
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tamperDetection",
+ "version": 1
+ },
+ {
+ "name": "patrol",
+ "version": 1
+ },
+ {
+ "name": "nightVisionMode",
+ "version": 3
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "targetTrack",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "vehicleDetection",
+ "version": 1
+ },
+ {
+ "name": "petDetection",
+ "version": 1
+ },
+ {
+ "name": "whiteLamp",
+ "version": 1
+ },
+ {
+ "name": "markerBox",
+ "version": 1
+ },
+ {
+ "name": "nvmp",
+ "version": 1
+ },
+ {
+ "name": "panoramicView",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "smartTrack",
+ "version": 1
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ }
+ ]
+ }
+ },
+ "getAudioConfig": {
+ "audio_config": {
+ "microphone": {
+ "bitrate": "64",
+ "channels": "1",
+ "echo_cancelling": "off",
+ "encode_type": "G711alaw",
+ "input_device_type": "MicIn",
+ "mute": "off",
+ "noise_cancelling": "off",
+ "sampling_rate": "8",
+ "volume": "81"
+ },
+ "speaker": {
+ "mute": "off",
+ "output_device_type": "SpeakerOut",
+ "volume": "100"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2024-12-16 17:09:43",
+ "seconds_from_1970": 1734386983
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {
+ "status": 0
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "4",
+ "rssiValue": -45,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ "digital_sensitivity": "60",
+ "enabled": "on",
+ "non_vehicle_enabled": "off",
+ "people_enabled": "off",
+ "sensitivity": "medium",
+ "vehicle_enabled": "off"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera c520ws",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "C520WS 1.0 IPC",
+ "device_model": "C520WS",
+ "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,
+ "mac": "F0-A7-31-00-00-00",
+ "manufacturer_name": "TP-LINK",
+ "mobile_access": "0",
+ "oem_id": "00000000000000000000000000000000",
+ "region": "US",
+ "sw_version": "1.2.8 Build 240606 Rel.39146n"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ "enabled": "off",
+ "random_range": "120",
+ "time": "03:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": true,
+ "state": "normal"
+ }
+ }
+ },
+ "getLastAlarmInfo": {
+ "system": {
+ "last_alarm_info": {
+ "last_alarm_time": "1734386954",
+ "last_alarm_type": "motion"
+ }
+ }
+ },
+ "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": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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": "standard",
+ "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_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": "50",
+ "wtl_manual_start_flag": "off"
+ }
+ }
+ },
+ "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": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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": "standard",
+ "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_day2night": "1400",
+ "wtl_sensitivity_night2day": "9100",
+ "wtl_start_time": "64800",
+ "wtl_type": "auto"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ "enabled": "on"
+ }
+ }
+ },
+ "getMsgPushConfig": {
+ "msg_push": {
+ "chn1_msg_push_info": {
+ "notification_enabled": "off",
+ "rich_notification_enabled": "off"
+ }
+ }
+ },
+ "getNightVisionCapability": {
+ "image_capability": {
+ "supplement_lamp": {
+ "night_vision_mode_range": [
+ "inf_night_vision",
+ "wtl_night_vision",
+ "md_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": "50",
+ "wtl_manual_start_flag": "off"
+ }
+ }
+ },
+ "getPersonDetectionConfig": {
+ "people_detection": {
+ "detection": {
+ "enabled": "on",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getPetDetectionConfig": {
+ "pet_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getPresetConfig": {
+ "preset": {
+ "preset": {
+ "id": [
+ "1",
+ "2",
+ "3",
+ "4"
+ ],
+ "name": [
+ "Doorbell",
+ "Packages",
+ "Street",
+ "Arm"
+ ],
+ "position_pan": [
+ "-0.328380",
+ "0.010401",
+ "0.010401",
+ "0.066865"
+ ],
+ "position_tilt": [
+ "-0.062500",
+ "0.828125",
+ "-0.285156",
+ "0.160156"
+ ],
+ "position_zoom": [],
+ "read_only": [
+ "0",
+ "0",
+ "0",
+ "0"
+ ]
+ }
+ }
+ },
+ "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": "50",
+ "wtl_manual_start_flag": "off"
+ }
+ }
+ },
+ "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": "offline",
+ "disk_name": "1",
+ "free_space": "0B",
+ "free_space_accurate": "0B",
+ "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",
+ "percent": "0",
+ "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": "0",
+ "rw_attr": "r",
+ "status": "offline",
+ "total_space": "0B",
+ "total_space_accurate": "0B",
+ "type": "local",
+ "video_free_space": "0B",
+ "video_free_space_accurate": "0B",
+ "video_total_space": "0B",
+ "video_total_space_accurate": "0B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTargetTrackConfig": {
+ "target_track": {
+ "target_track_info": {
+ "back_time": "30",
+ "enabled": "off",
+ "track_mode": "pantilt",
+ "track_time": "0"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ "timezone": "UTC-05:00",
+ "timing_mode": "manual",
+ "zone_id": "America/New_York"
+ }
+ }
+ },
+ "getVehicleDetectionConfig": {
+ "vehicle_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "1536",
+ "2048",
+ "2560"
+ ],
+ "change_fps_support": "1",
+ "encode_types": [
+ "H264",
+ "H265"
+ ],
+ "frame_rates": [
+ "65551",
+ "65556",
+ "65561"
+ ],
+ "minor_stream_support": "0",
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "2560*1440",
+ "1920*1080",
+ "1280*720"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ "bitrate": "2560",
+ "bitrate_type": "vbr",
+ "default_bitrate": "2560",
+ "encode_type": "H264",
+ "frame_rate": "65551",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "2560*1440",
+ "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": "50",
+ "wtl_manual_start_flag": "off"
+ }
+ }
+ },
+ "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",
+ "G711ulaw"
+ ],
+ "mute": "0",
+ "output_device_type": "0",
+ "sampling_rate": [
+ "8",
+ "16"
+ ],
+ "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": "off",
+ "sampling_rate": "8",
+ "volume": "81"
+ },
+ "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"
+ ],
+ "alarm_out_num": "0",
+ "app_version": "1.0.0",
+ "audio": [
+ "speaker",
+ "microphone"
+ ],
+ "audio_alarm_clock": "1",
+ "audio_lib": "1",
+ "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",
+ "image",
+ "OSD",
+ "video"
+ ],
+ "custom_area_compensation": "1",
+ "daynight_subdivision": "1",
+ "device_share": [
+ "preview",
+ "playback",
+ "voice",
+ "cloud_storage",
+ "motor"
+ ],
+ "diagnose": "1",
+ "diagnose_capability": "1",
+ "download": [
+ "video"
+ ],
+ "events": [
+ "motion",
+ "tamper"
+ ],
+ "gb28181": "1",
+ "greeter": "1.0",
+ "http_system_state_audio_support": "1",
+ "image_capability": "1",
+ "image_list": [
+ "supplement_lamp",
+ "expose"
+ ],
+ "led": "1",
+ "lens_mask": "1",
+ "linkage_capability": "1",
+ "local_storage": "1",
+ "media_encrypt": "1",
+ "msg_alarm_list": [
+ "sound",
+ "light"
+ ],
+ "msg_push": "1",
+ "multi_user": "0",
+ "network": [
+ "wifi",
+ "ethernet"
+ ],
+ "osd_capability": "1",
+ "ota_upgrade": "1",
+ "p2p_support_versions": [
+ "2.0"
+ ],
+ "playback": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "playback_scale": "1",
+ "playback_version": "1.1",
+ "preview": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "privacy_mask_api_version": "1.0",
+ "ptz": "1",
+ "record_max_slot_cnt": "10",
+ "record_type": [
+ "timing",
+ "motion"
+ ],
+ "relay_support_versions": [
+ "2.0"
+ ],
+ "remote_upgrade": "1",
+ "reonboarding": "1",
+ "smart_codec": "0",
+ "smart_detection": "1",
+ "smart_msg_push_capability": "1",
+ "ssl_cer_version": "1.0",
+ "storage_api_version": "2.2",
+ "stream_max_sessions": "10",
+ "streaming_support_versions": [
+ "2.0"
+ ],
+ "tapo_care_version": "1.0.0",
+ "target_track": "1",
+ "timing_reboot": "1",
+ "tums": "1",
+ "tums_config": "1",
+ "tums_msg_push": "1",
+ "verification_change_password": "1",
+ "video_codec": [
+ "h264",
+ "h265"
+ ],
+ "video_detection_digital_sensitivity": "1",
+ "video_message": "1",
+ "wide_range_inf_sensitivity": "1",
+ "wifi_cascade_connection": "0",
+ "wifi_connection_info": "1",
+ "wireless_hotspot": "0"
+ }
+ }
+ }
+ },
+ "scanApList": {
+ "onboarding": {
+ "scan": {
+ "ap_list": [
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "wpa3_supported": "false"
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/C720(US)_1.0_1.2.3.json b/tests/fixtures/smartcam/C720(US)_1.0_1.2.3.json
new file mode 100644
index 00000000..e31bee02
--- /dev/null
+++ b/tests/fixtures/smartcam/C720(US)_1.0_1.2.3.json
@@ -0,0 +1,1039 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "1736360289",
+ "last_alarm_type": "motion",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "normal"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "C720",
+ "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.3 Build 240823 Rel.40327n",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "98-25-4A-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ },
+ "protocol_version": 1
+ }
+ },
+ "getAlertConfig": {
+ "msg_alarm": {
+ "capability": {
+ "alarm_duration_support": "1",
+ "alarm_volume_support": "1",
+ "alert_event_type_support": "1",
+ "usr_def_audio_alarm_max_num": "15",
+ "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"
+ ],
+ "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": 3
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 6
+ },
+ {
+ "name": "detection",
+ "version": 3
+ },
+ {
+ "name": "alert",
+ "version": 2
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 2
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 3
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "dayNightMode",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 2
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 3
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "linecrossingDetection",
+ "version": 2
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tamperDetection",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "vehicleDetection",
+ "version": 1
+ },
+ {
+ "name": "petDetection",
+ "version": 1
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ },
+ {
+ "name": "markerBox",
+ "version": 1
+ },
+ {
+ "name": "nvmp",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "pirDetection",
+ "version": 1
+ },
+ {
+ "name": "lightsensor",
+ "version": 1
+ },
+ {
+ "name": "floodlight",
+ "version": 2
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "upnpc",
+ "version": 2
+ },
+ {
+ "name": "staticIp",
+ "version": 2
+ },
+ {
+ "name": "manualAlarm",
+ "version": 1
+ },
+ {
+ "name": "snapshot",
+ "version": 2
+ },
+ {
+ "name": "timeFormat",
+ "version": 1
+ }
+ ]
+ }
+ },
+ "getAudioConfig": {
+ "audio_config": {
+ "microphone": {
+ "bitrate": "64",
+ "channels": "1",
+ "echo_cancelling": "off",
+ "encode_type": "G711alaw",
+ "factory_noise_cancelling": "off",
+ "input_device_type": "MicIn",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "speaker": {
+ "mute": "off",
+ "output_device_type": "SpeakerOut",
+ "volume": "100"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2025-01-08 12:24:34",
+ "seconds_from_1970": 1736360674
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {
+ "status": 0
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "3",
+ "rssiValue": -55,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ "digital_sensitivity": "60",
+ "enabled": "on",
+ "non_vehicle_enabled": "off",
+ "people_enabled": "off",
+ "sensitivity": "medium",
+ "vehicle_enabled": "off"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera c720",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "C720 1.0 IPC",
+ "device_model": "C720",
+ "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,
+ "mac": "98-25-4A-00-00-00",
+ "manufacturer_name": "TP-LINK",
+ "mobile_access": "0",
+ "no_rtsp_constrain": 1,
+ "oem_id": "00000000000000000000000000000000",
+ "region": "US",
+ "sw_version": "1.2.3 Build 240823 Rel.40327n"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ "enabled": "off",
+ "random_range": "120",
+ "time": "03:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": true,
+ "state": "normal"
+ }
+ }
+ },
+ "getLastAlarmInfo": {
+ "system": {
+ "last_alarm_info": {
+ "last_alarm_time": "1736360661",
+ "last_alarm_type": "motion"
+ }
+ }
+ },
+ "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": "100",
+ "exp_level": "0",
+ "exp_type": "auto",
+ "focus_limited": "10",
+ "focus_type": "manual",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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",
+ "manual_exp_iso_gain": "0",
+ "manual_exp_us": "50",
+ "saturation": "50",
+ "sharpness": "50",
+ "shutter": "1/25",
+ "smartir": "auto_ir",
+ "smartir_level": "0",
+ "smartwtl": "auto_wtl",
+ "smartwtl_digital_level": "50",
+ "smartwtl_level": "3",
+ "style": "standard",
+ "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_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": "center",
+ "full_color_min_keep_time": "30",
+ "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": "5"
+ }
+ }
+ },
+ "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": "100",
+ "exp_level": "0",
+ "exp_type": "auto",
+ "focus_limited": "10",
+ "focus_type": "manual",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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",
+ "manual_exp_iso_gain": "0",
+ "manual_exp_us": "50",
+ "saturation": "50",
+ "sharpness": "50",
+ "shutter": "1/25",
+ "smartir": "auto_ir",
+ "smartir_level": "0",
+ "smartwtl": "auto_wtl",
+ "smartwtl_digital_level": "50",
+ "smartwtl_level": "3",
+ "style": "standard",
+ "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_day2night": "1400",
+ "wtl_sensitivity_night2day": "9100",
+ "wtl_start_time": "64800",
+ "wtl_type": "auto"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ "enabled": "on"
+ }
+ }
+ },
+ "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"
+ ],
+ "supplement_lamp_type": [
+ "infrared_lamp",
+ "white_lamp"
+ ]
+ }
+ }
+ },
+ "getNightVisionModeConfig": {
+ "image": {
+ "switch": {
+ "best_view_distance": "0",
+ "clear_licence_plate_mode": "off",
+ "flip_type": "center",
+ "full_color_min_keep_time": "30",
+ "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": "5"
+ }
+ }
+ },
+ "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": "center",
+ "full_color_min_keep_time": "30",
+ "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": "5"
+ }
+ }
+ },
+ "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": "6.5GB",
+ "free_space_accurate": "6945154936B",
+ "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",
+ "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": "1706216554",
+ "rw_attr": "rw",
+ "status": "normal",
+ "total_space": "119.1GB",
+ "total_space_accurate": "127878135808B",
+ "type": "local",
+ "video_free_space": "6.5GB",
+ "video_free_space_accurate": "6945154936B",
+ "video_total_space": "114.2GB",
+ "video_total_space_accurate": "122675003392B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ "digital_sensitivity": "50",
+ "enabled": "on",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ "timezone": "UTC-06:00",
+ "timing_mode": "ntp",
+ "zone_id": "America/Chicago"
+ }
+ }
+ },
+ "getVehicleDetectionConfig": {
+ "vehicle_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "1536",
+ "2048"
+ ],
+ "change_fps_support": "1",
+ "encode_types": [
+ "H264",
+ "H265"
+ ],
+ "frame_rates": [
+ "65551",
+ "65556",
+ "65561"
+ ],
+ "minor_stream_support": "1",
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "2560*1440",
+ "1920*1080"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ "bitrate": "2048",
+ "bitrate_type": "vbr",
+ "default_bitrate": "2048",
+ "encode_type": "H264",
+ "frame_rate": "65561",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "2560*1440",
+ "smart_codec": "off"
+ }
+ }
+ },
+ "getWhitelampConfig": {
+ "image": {
+ "switch": {
+ "best_view_distance": "0",
+ "clear_licence_plate_mode": "off",
+ "flip_type": "center",
+ "full_color_min_keep_time": "30",
+ "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": "5"
+ }
+ }
+ },
+ "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": "80",
+ "volume": "1"
+ }
+ }
+ }
+ },
+ "get_audio_config": {
+ "get": {
+ "audio_config": {
+ "microphone": {
+ "bitrate": "64",
+ "channels": "1",
+ "echo_cancelling": "off",
+ "encode_type": "G711alaw",
+ "factory_noise_cancelling": "off",
+ "input_device_type": "MicIn",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "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"
+ ],
+ "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"
+ ],
+ "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": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 0,
+ "bssid": "000000000000",
+ "encryption": 0,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "wpa3_supported": "true"
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/D130(US)_1.0_1.1.9.json b/tests/fixtures/smartcam/D130(US)_1.0_1.1.9.json
new file mode 100644
index 00000000..7cd498f7
--- /dev/null
+++ b/tests/fixtures/smartcam/D130(US)_1.0_1.1.9.json
@@ -0,0 +1,986 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "D130",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPODOORBELL",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.1.9 Build 240716 Rel.51615n",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "isResetWiFi": false,
+ "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_volume_support": "1",
+ "alert_event_type_support": "1",
+ "usr_def_audio_alarm_max_num": "15",
+ "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": [
+ "light",
+ "sound"
+ ],
+ "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",
+ "Tone"
+ ]
+ }
+ }
+ },
+ "getAppComponentList": {
+ "app_component": {
+ "app_component_list": [
+ {
+ "name": "sdCard",
+ "version": 1
+ },
+ {
+ "name": "timezone",
+ "version": 1
+ },
+ {
+ "name": "system",
+ "version": 3
+ },
+ {
+ "name": "led",
+ "version": 2
+ },
+ {
+ "name": "playback",
+ "version": 6
+ },
+ {
+ "name": "detection",
+ "version": 3
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 1
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 3
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "dayNightMode",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 3
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 3
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "linecrossingDetection",
+ "version": 2
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tamperDetection",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "nightVisionMode",
+ "version": 3
+ },
+ {
+ "name": "vehicleDetection",
+ "version": 1
+ },
+ {
+ "name": "petDetection",
+ "version": 1
+ },
+ {
+ "name": "packageDetection",
+ "version": 3
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ },
+ {
+ "name": "markerBox",
+ "version": 1
+ },
+ {
+ "name": "whiteLamp",
+ "version": 1
+ },
+ {
+ "name": "nvmp",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "quickResponse",
+ "version": 1
+ },
+ {
+ "name": "ldc",
+ "version": 1
+ },
+ {
+ "name": "upnpc",
+ "version": 2
+ },
+ {
+ "name": "chimeCtrl",
+ "version": 1
+ },
+ {
+ "name": "ring",
+ "version": 3
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "staticIp",
+ "version": 2
+ }
+ ]
+ }
+ },
+ "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": "80"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2025-01-09 08:38:30",
+ "seconds_from_1970": 1736433510
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {
+ "status": 0
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "4",
+ "rssiValue": -46,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ "digital_sensitivity": "60",
+ "enabled": "off",
+ "non_vehicle_enabled": "off",
+ "people_enabled": "off",
+ "sensitivity": "medium",
+ "vehicle_enabled": "off"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera d130",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "D130 1.0 IPC",
+ "device_model": "D130",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPODOORBELL",
+ "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,
+ "mac": "40-AE-30-00-00-00",
+ "manufacturer_name": "TP-LINK",
+ "mobile_access": "0",
+ "no_rtsp_constrain": 1,
+ "oem_id": "00000000000000000000000000000000",
+ "region": "US",
+ "sw_version": "1.1.9 Build 240716 Rel.51615n"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ "enabled": "off",
+ "random_range": "120",
+ "time": "15:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": true,
+ "state": "normal"
+ }
+ }
+ },
+ "getLastAlarmInfo": {
+ "system": {
+ "last_alarm_info": {
+ "last_alarm_time": "1736432241",
+ "last_alarm_type": "motion"
+ }
+ }
+ },
+ "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": "100",
+ "exp_level": "0",
+ "exp_type": "auto",
+ "focus_limited": "10",
+ "focus_type": "manual",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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": "50",
+ "smartwtl_level": "3",
+ "style": "standard",
+ "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_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": "30",
+ "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": "on",
+ "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": "3"
+ }
+ }
+ },
+ "getLedStatus": {
+ "led": {
+ "config": {
+ "enabled": "auto"
+ }
+ }
+ },
+ "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": "100",
+ "exp_level": "0",
+ "exp_type": "auto",
+ "focus_limited": "10",
+ "focus_type": "manual",
+ "high_light_compensation": "off",
+ "inf_delay": "5",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "4",
+ "inf_sensitivity_day2night": "1400",
+ "inf_sensitivity_night2day": "9100",
+ "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": "50",
+ "smartwtl_level": "3",
+ "style": "standard",
+ "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_day2night": "1400",
+ "wtl_sensitivity_night2day": "9100",
+ "wtl_start_time": "64800",
+ "wtl_type": "auto"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ "enabled": "on"
+ }
+ }
+ },
+ "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",
+ "md_night_vision",
+ "dbl_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": "30",
+ "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": "on",
+ "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": "3"
+ }
+ }
+ },
+ "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": "30",
+ "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": "on",
+ "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": "3"
+ }
+ }
+ },
+ "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": "0B",
+ "free_space_accurate": "0B",
+ "loop_record_status": "1",
+ "msg_push_free_space": "0B",
+ "msg_push_free_space_accurate": "0B",
+ "msg_push_total_space": "0B",
+ "msg_push_total_space_accurate": "0B",
+ "percent": "0",
+ "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": "1723813993",
+ "rw_attr": "rw",
+ "status": "normal",
+ "total_space": "119.1GB",
+ "total_space_accurate": "127878135808B",
+ "type": "local",
+ "video_free_space": "0B",
+ "video_free_space_accurate": "0B",
+ "video_total_space": "114.3GB",
+ "video_total_space_accurate": "122675003392B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ "timezone": "UTC-06:00",
+ "timing_mode": "ntp",
+ "zone_id": "America/Chicago"
+ }
+ }
+ },
+ "getVehicleDetectionConfig": {
+ "vehicle_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "1536",
+ "2048",
+ "2560",
+ "3072"
+ ],
+ "change_fps_support": "1",
+ "encode_types": [
+ "H264",
+ "H265"
+ ],
+ "frame_rates": [
+ "65551",
+ "65556",
+ "65561",
+ "65566"
+ ],
+ "minor_stream_support": "1",
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "2560*1920"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ "bitrate": "3072",
+ "bitrate_type": "vbr",
+ "default_bitrate": "3072",
+ "encode_type": "H264",
+ "frame_rate": "65551",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "2560*1920",
+ "smart_codec": "off"
+ }
+ }
+ },
+ "getWhitelampConfig": {
+ "image": {
+ "switch": {
+ "best_view_distance": "0",
+ "clear_licence_plate_mode": "off",
+ "flip_type": "off",
+ "full_color_min_keep_time": "30",
+ "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": "on",
+ "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": "3"
+ }
+ }
+ },
+ "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": "80",
+ "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": "80"
+ }
+ }
+ }
+ },
+ "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"
+ ],
+ "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": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "wpa3_supported": "true"
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json
index 04bcc262..4ef99fae 100644
--- a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json
+++ b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json
@@ -1,33 +1,37 @@
{
"discovery_result": {
- "decrypted_data": {
- "connect_ssid": "",
- "connect_type": "wired",
- "device_id": "0000000000000000000000000000000000000000",
- "http_port": 443,
- "owner": "00000000000000000000000000000000",
- "sd_status": "offline"
- },
- "device_id": "00000000000000000000000000000000",
- "device_model": "H200",
- "device_name": "#MASKED_NAME#",
- "device_type": "SMART.TAPOHUB",
- "encrypt_info": {
- "data": "",
- "key": "",
- "sym_schm": "AES"
- },
- "encrypt_type": [
- "3"
- ],
- "factory_default": false,
- "firmware_version": "1.3.2 Build 20240424 rel.75425",
- "hardware_version": "1.0",
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "A8-6E-84-00-00-00",
- "mgt_encrypt_schm": {
- "is_support_https": true
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "",
+ "connect_type": "wired",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "H200",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPOHUB",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.3.2 Build 20240424 rel.75425",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "isResetWiFi": false,
+ "is_support_iot_cloud": true,
+ "mac": "A8-6E-84-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ }
}
},
"getAlertConfig": {},
@@ -211,8 +215,8 @@
"fw_ver": "1.11.0 Build 230821 Rel.113553",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
- "jamming_rssi": -108,
- "jamming_signal_level": 2,
+ "jamming_rssi": -119,
+ "jamming_signal_level": 1,
"lastOnboardingTimestamp": 1714016798,
"mac": "202351000000",
"model": "S200B",
@@ -221,7 +225,7 @@
"parent_device_id": "0000000000000000000000000000000000000000",
"region": "Europe/London",
"report_interval": 16,
- "rssi": -66,
+ "rssi": -60,
"signal_level": 3,
"specs": "EU",
"status": "online",
@@ -242,8 +246,17 @@
"getClockStatus": {
"system": {
"clock_status": {
- "local_time": "2024-11-01 13:56:27",
- "seconds_from_1970": 1730469387
+ "local_time": "1984-10-21 23:48:23",
+ "seconds_from_1970": 467246903
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {
+ "current_ssid": "",
+ "err_code": 0,
+ "status": 0
}
}
},
@@ -326,6 +339,10 @@
}
}
},
+ "getMatterSetupInfo": {
+ "setup_code": "00000000000",
+ "setup_payload": "00:000000-000000000000"
+ },
"getMediaEncrypt": {
"cet": {
"media_encrypt": {
@@ -350,7 +367,7 @@
"getSirenConfig": {
"duration": 300,
"siren_type": "Doorbell Ring 1",
- "volume": "6"
+ "volume": "1"
},
"getSirenStatus": {
"status": "off",
@@ -386,5 +403,98 @@
"zone_id": "Europe/London"
}
}
+ },
+ "scanApList": {
+ "onboarding": {
+ "scan": {
+ "ap_list": [
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 0,
+ "bssid": "000000000000",
+ "encryption": 0,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 3,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 4,
+ "bssid": "000000000000",
+ "encryption": 3,
+ "rssi": 2,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ {
+ "auth": 3,
+ "bssid": "000000000000",
+ "encryption": 2,
+ "rssi": 1,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ }
+ ],
+ "wpa3_supported": "false"
+ }
+ }
}
}
diff --git a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.6.json b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.6.json
new file mode 100644
index 00000000..99460fe1
--- /dev/null
+++ b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.6.json
@@ -0,0 +1,556 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wired",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "H200",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPOHUB",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.3.6 Build 20240829 rel.71119",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "isResetWiFi": false,
+ "is_support_iot_cloud": true,
+ "mac": "F0-09-0D-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ }
+ }
+ },
+ "getAlertConfig": {},
+ "getAppComponentList": {
+ "app_component": {
+ "app_component_list": [
+ {
+ "name": "sdCard",
+ "version": 2
+ },
+ {
+ "name": "dateTime",
+ "version": 1
+ },
+ {
+ "name": "system",
+ "version": 4
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 1
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "hubRecord",
+ "version": 1
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "siren",
+ "version": 2
+ },
+ {
+ "name": "childControl",
+ "version": 1
+ },
+ {
+ "name": "childQuickSetup",
+ "version": 1
+ },
+ {
+ "name": "childInherit",
+ "version": 1
+ },
+ {
+ "name": "deviceLoad",
+ "version": 1
+ },
+ {
+ "name": "subg",
+ "version": 2
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "preWakeUp",
+ "version": 1
+ },
+ {
+ "name": "supportRE",
+ "version": 1
+ },
+ {
+ "name": "testSignal",
+ "version": 1
+ },
+ {
+ "name": "dataDownload",
+ "version": 1
+ },
+ {
+ "name": "testChildSignal",
+ "version": 1
+ },
+ {
+ "name": "ringLog",
+ "version": 1
+ },
+ {
+ "name": "matter",
+ "version": 1
+ },
+ {
+ "name": "localSmart",
+ "version": 1
+ },
+ {
+ "name": "generalCameraManage",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 6
+ },
+ {
+ "name": "hubPlayback",
+ "version": 1
+ }
+ ]
+ }
+ },
+ "getChildDeviceComponentList": {
+ "child_component_list": [
+ {
+ "component_list": [
+ {
+ "name": "sdCard",
+ "version": 2
+ },
+ {
+ "name": "timezone",
+ "version": 1
+ },
+ {
+ "name": "batCamSystem",
+ "version": 1
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 3
+ },
+ {
+ "name": "detection",
+ "version": 3
+ },
+ {
+ "name": "firmware",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 3
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 2
+ },
+ {
+ "name": "dayNightMode",
+ "version": 2
+ },
+ {
+ "name": "nightVisionMode",
+ "version": 3
+ },
+ {
+ "name": "batCamOsd",
+ "version": 1
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 3
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "vehicleDetection",
+ "version": 1
+ },
+ {
+ "name": "petDetection",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "pir",
+ "version": 1
+ },
+ {
+ "name": "battery",
+ "version": 3
+ },
+ {
+ "name": "clips",
+ "version": 1
+ },
+ {
+ "name": "batCamRelay",
+ "version": 1
+ },
+ {
+ "name": "batCamP2p",
+ "version": 1
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "whiteLamp",
+ "version": 1
+ },
+ {
+ "name": "infLamp",
+ "version": 1
+ },
+ {
+ "name": "packageDetection",
+ "version": 3
+ },
+ {
+ "name": "wakeUp",
+ "version": 1
+ },
+ {
+ "name": "ring",
+ "version": 1
+ },
+ {
+ "name": "antiTheft",
+ "version": 3
+ },
+ {
+ "name": "quickResponse",
+ "version": 2
+ },
+ {
+ "name": "doorbellNightVision",
+ "version": 1
+ },
+ {
+ "name": "dataDownload",
+ "version": 1
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ },
+ {
+ "name": "streamGrab",
+ "version": 1
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "batCamPreRelay",
+ "version": 1
+ },
+ {
+ "name": "batCamStatistics",
+ "version": 1
+ },
+ {
+ "name": "batCamNodeRelay",
+ "version": 1
+ },
+ {
+ "name": "batCamRtsp",
+ "version": 2
+ }
+ ],
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
+ }
+ ],
+ "start_index": 0,
+ "sum": 1
+ },
+ "getChildDeviceList": {
+ "child_device_list": [
+ {
+ "alias": "#MASKED_NAME#",
+ "anti_theft_status": 0,
+ "avatar": "camera d210",
+ "battery_charging": "NO",
+ "battery_installed": 1,
+ "battery_percent": 90,
+ "battery_temperature": 3,
+ "battery_voltage": 4022,
+ "cam_uptime": 5378,
+ "category": "camera",
+ "dev_name": "Tapo Smart Doorbell",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
+ "device_model": "D230",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPODOORBELL",
+ "ext_addr": "0000000000000000",
+ "firmware_status": "OK",
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.20",
+ "ipaddr": "172.23.30.2",
+ "led_status": "on",
+ "low_battery": false,
+ "mac": "F0:09:0D:00:00:00",
+ "oem_id": "00000000000000000000000000000000",
+ "onboarding_timestamp": 1732920657,
+ "online": true,
+ "parent_device_id": "0000000000000000000000000000000000000000",
+ "power": "BATTERY",
+ "power_save_mode": "off",
+ "power_save_status": "off",
+ "region": "EU",
+ "rssi": -46,
+ "short_addr": 0,
+ "status": "configured",
+ "subg_cam_rssi": 0,
+ "subg_hub_rssi": 0,
+ "sw_ver": "1.1.19 Build 20241011 rel.67020",
+ "system_time": 1735995953,
+ "updating": false,
+ "uptime": 3061186
+ }
+ ],
+ "start_index": 0,
+ "sum": 1
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2025-01-04 14:05:53",
+ "seconds_from_1970": 1735995953
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "ethernet"
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "hub_h200",
+ "bind_status": true,
+ "child_num": 1,
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "H200 1.0",
+ "device_model": "H200",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPOHUB",
+ "has_set_location_info": 1,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_version": "1.0",
+ "latitude": 0,
+ "local_ip": "127.0.0.123",
+ "longitude": 0,
+ "mac": "F0-09-0D-00-00-00",
+ "need_sync_sha1_password": 0,
+ "oem_id": "00000000000000000000000000000000",
+ "product_name": "Tapo Smart Hub",
+ "region": "EU",
+ "status": "configured",
+ "sw_version": "1.3.6 Build 20240829 rel.71119"
+ },
+ "info": {
+ "avatar": "hub_h200",
+ "bind_status": true,
+ "child_num": 1,
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "H200 1.0",
+ "device_model": "H200",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPOHUB",
+ "has_set_location_info": 1,
+ "hw_id": "00000000000000000000000000000000",
+ "hw_version": "1.0",
+ "latitude": 0,
+ "local_ip": "127.0.0.123",
+ "longitude": 0,
+ "mac": "F0-09-0D-00-00-00",
+ "need_sync_sha1_password": 0,
+ "oem_id": "00000000000000000000000000000000",
+ "product_name": "Tapo Smart Hub",
+ "region": "EU",
+ "status": "configured",
+ "sw_version": "1.3.6 Build 20240829 rel.71119"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ "enabled": "on",
+ "random_range": 30,
+ "time": "03:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": true,
+ "state": "normal"
+ }
+ }
+ },
+ "getLedStatus": {
+ "led": {
+ "config": {
+ ".name": "config",
+ ".type": "led",
+ "enabled": "on"
+ }
+ }
+ },
+ "getMatterSetupInfo": {
+ "setup_code": "00000000000",
+ "setup_payload": "00:0000000000000000000"
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ "enabled": "on"
+ }
+ }
+ },
+ "getSdCardStatus": {
+ "harddisk_manage": {
+ "hd_info": [
+ {
+ "hd_info_1": {
+ "detect_status": "offline",
+ "disk_name": "1",
+ "loop_record_status": "1",
+ "status": "offline"
+ }
+ }
+ ]
+ }
+ },
+ "getSirenConfig": {
+ "duration": 30,
+ "siren_type": "Doorbell Ring 3",
+ "volume": "10"
+ },
+ "getSirenStatus": {
+ "status": "off",
+ "time_left": 0
+ },
+ "getSirenTypeList": {
+ "siren_type_list": [
+ "Doorbell Ring 1",
+ "Doorbell Ring 2",
+ "Doorbell Ring 3",
+ "Doorbell Ring 4",
+ "Doorbell Ring 5",
+ "Doorbell Ring 6",
+ "Doorbell Ring 7",
+ "Doorbell Ring 8",
+ "Doorbell Ring 9",
+ "Doorbell Ring 10",
+ "Phone Ring",
+ "Alarm 1",
+ "Alarm 2",
+ "Alarm 3",
+ "Alarm 4",
+ "Dripping Tap",
+ "Alarm 5",
+ "Connection 1",
+ "Connection 2"
+ ]
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ "timezone": "UTC+01:00",
+ "zone_id": "Europe/Amsterdam"
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/H200(US)_1.0_1.3.6.json b/tests/fixtures/smartcam/H200(US)_1.0_1.3.6.json
index f1a6ae15..26c03793 100644
--- a/tests/fixtures/smartcam/H200(US)_1.0_1.3.6.json
+++ b/tests/fixtures/smartcam/H200(US)_1.0_1.3.6.json
@@ -1,34 +1,37 @@
{
"discovery_result": {
- "decrypted_data": {
- "connect_ssid": "",
- "connect_type": "wired",
- "device_id": "0000000000000000000000000000000000000000",
- "http_port": 443,
- "owner": "00000000000000000000000000000000",
- "sd_status": "offline"
- },
- "device_id": "00000000000000000000000000000000",
- "device_model": "H200",
- "device_name": "#MASKED_NAME#",
- "device_type": "SMART.TAPOHUB",
- "encrypt_info": {
- "data": "",
- "key": "",
- "sym_schm": "AES"
- },
- "encrypt_type": [
- "3"
- ],
- "factory_default": false,
- "firmware_version": "1.3.6 Build 20240829 rel.71119",
- "hardware_version": "1.0",
- "ip": "127.0.0.123",
- "isResetWiFi": false,
- "is_support_iot_cloud": true,
- "mac": "24-2F-D0-00-00-00",
- "mgt_encrypt_schm": {
- "is_support_https": true
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "",
+ "connect_type": "wired",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "H200",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPOHUB",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.3.6 Build 20240829 rel.71119",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "isResetWiFi": false,
+ "is_support_iot_cloud": true,
+ "mac": "24-2F-D0-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ }
}
},
"getAlertConfig": {},
diff --git a/tests/fixtures/smartcam/TC65_1.0_1.3.9.json b/tests/fixtures/smartcam/TC65_1.0_1.3.9.json
index 5b05a1b3..cec6b759 100644
--- a/tests/fixtures/smartcam/TC65_1.0_1.3.9.json
+++ b/tests/fixtures/smartcam/TC65_1.0_1.3.9.json
@@ -1,35 +1,38 @@
{
"discovery_result": {
- "decrypted_data": {
- "connect_ssid": "0000000",
- "connect_type": "wireless",
- "device_id": "0000000000000000000000000000000000000000",
- "http_port": 443,
- "last_alarm_time": "1698149810",
- "last_alarm_type": "motion",
- "owner": "00000000000000000000000000000000",
- "sd_status": "offline"
- },
- "device_id": "00000000000000000000000000000000",
- "device_model": "TC65",
- "device_name": "#MASKED_NAME#",
- "device_type": "SMART.IPCAMERA",
- "encrypt_info": {
- "data": "",
- "key": "",
- "sym_schm": "AES"
- },
- "encrypt_type": [
- "3"
- ],
- "factory_default": false,
- "firmware_version": "1.3.9 Build 231024 Rel.72919n(4555)",
- "hardware_version": "1.0",
- "ip": "127.0.0.123",
- "is_support_iot_cloud": true,
- "mac": "A8-6E-84-00-00-00",
- "mgt_encrypt_schm": {
- "is_support_https": true
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "1698149810",
+ "last_alarm_type": "motion",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "TC65",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.3.9 Build 231024 Rel.72919n(4555)",
+ "hardware_version": "1.0",
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "A8-6E-84-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ }
}
},
"getAlertPlan": {
diff --git a/tests/fixtures/smartcam/TC70_3.0_1.3.11.json b/tests/fixtures/smartcam/TC70_3.0_1.3.11.json
new file mode 100644
index 00000000..b5726982
--- /dev/null
+++ b/tests/fixtures/smartcam/TC70_3.0_1.3.11.json
@@ -0,0 +1,870 @@
+{
+ "discovery_result": {
+ "error_code": 0,
+ "result": {
+ "decrypted_data": {
+ "connect_ssid": "#MASKED_SSID#",
+ "connect_type": "wireless",
+ "device_id": "0000000000000000000000000000000000000000",
+ "http_port": 443,
+ "last_alarm_time": "1734271551",
+ "last_alarm_type": "motion",
+ "owner": "00000000000000000000000000000000",
+ "sd_status": "offline"
+ },
+ "device_id": "00000000000000000000000000000000",
+ "device_model": "TC70",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "encrypt_info": {
+ "data": "",
+ "key": "",
+ "sym_schm": "AES"
+ },
+ "encrypt_type": [
+ "3"
+ ],
+ "factory_default": false,
+ "firmware_version": "1.3.11 Build 231121 Rel.39429n(4555)",
+ "hardware_version": "3.0",
+ "ip": "127.0.0.123",
+ "is_support_iot_cloud": true,
+ "mac": "5C-E9-31-00-00-00",
+ "mgt_encrypt_schm": {
+ "is_support_https": true
+ }
+ }
+ },
+ "getAlertPlan": {
+ "msg_alarm_plan": {
+ "chn1_msg_alarm_plan": {
+ ".name": "chn1_msg_alarm_plan",
+ ".type": "plan",
+ "alarm_plan_1": "0000-0000,127",
+ "enabled": "off"
+ }
+ }
+ },
+ "getAppComponentList": {
+ "app_component": {
+ "app_component_list": [
+ {
+ "name": "sdCard",
+ "version": 1
+ },
+ {
+ "name": "timezone",
+ "version": 1
+ },
+ {
+ "name": "system",
+ "version": 3
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 4
+ },
+ {
+ "name": "detection",
+ "version": 1
+ },
+ {
+ "name": "alert",
+ "version": 1
+ },
+ {
+ "name": "firmware",
+ "version": 2
+ },
+ {
+ "name": "account",
+ "version": 1
+ },
+ {
+ "name": "quickSetup",
+ "version": 1
+ },
+ {
+ "name": "ptz",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 2
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 1
+ },
+ {
+ "name": "dayNightMode",
+ "version": 1
+ },
+ {
+ "name": "osd",
+ "version": 2
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "videoRotation",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 2
+ },
+ {
+ "name": "diagnose",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "personDetection",
+ "version": 1
+ },
+ {
+ "name": "targetTrack",
+ "version": 1
+ },
+ {
+ "name": "babyCryDetection",
+ "version": 1
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ }
+ ]
+ }
+ },
+ "getAudioConfig": {
+ "audio_config": {
+ "microphone": {
+ ".name": "microphone",
+ ".type": "audio_config",
+ "channels": "1",
+ "encode_type": "G711alaw",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "speaker": {
+ ".name": "speaker",
+ ".type": "audio_config",
+ "volume": "100"
+ }
+ }
+ },
+ "getBCDConfig": {
+ "sound_detection": {
+ "bcd": {
+ ".name": "bcd",
+ ".type": "on_off",
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ ".name": "harddisk",
+ ".type": "storage",
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2024-12-18 22:59:11",
+ "seconds_from_1970": 1734562751
+ }
+ }
+ },
+ "getConnectionType": {
+ "link_type": "wifi",
+ "rssi": "4",
+ "rssiValue": -50,
+ "ssid": "I01BU0tFRF9TU0lEIw=="
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ ".name": "motion_det",
+ ".type": "on_off",
+ "digital_sensitivity": "50",
+ "enabled": "on",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "camera c212",
+ "barcode": "",
+ "dev_id": "0000000000000000000000000000000000000000",
+ "device_alias": "#MASKED_NAME#",
+ "device_info": "TC70 3.0 IPC",
+ "device_model": "TC70",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.IPCAMERA",
+ "features": "3",
+ "ffs": false,
+ "has_set_location_info": 1,
+ "hw_desc": "00000000000000000000000000000000",
+ "hw_version": "3.0",
+ "is_cal": true,
+ "latitude": 0,
+ "longitude": 0,
+ "mac": "5C-E9-31-00-00-00",
+ "oem_id": "00000000000000000000000000000000",
+ "sw_version": "1.3.11 Build 231121 Rel.39429n(4555)"
+ }
+ }
+ },
+ "getFirmwareAutoUpgradeConfig": {
+ "auto_upgrade": {
+ "common": {
+ ".name": "common",
+ ".type": "on_off",
+ "enabled": "off",
+ "random_range": "120",
+ "time": "03:00"
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": false,
+ "state": "normal"
+ }
+ }
+ },
+ "getLastAlarmInfo": {
+ "system": {
+ "last_alarm_info": {
+ "last_alarm_time": "1734271551",
+ "last_alarm_type": "motion"
+ }
+ }
+ },
+ "getLdc": {
+ "image": {
+ "common": {
+ ".name": "common",
+ ".type": "para",
+ "area_compensation": "default",
+ "chroma": "50",
+ "contrast": "50",
+ "dehaze": "off",
+ "exp_gain": "0",
+ "exp_type": "auto",
+ "focus_limited": "600",
+ "focus_type": "semi_auto",
+ "high_light_compensation": "off",
+ "inf_delay": "10",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "1",
+ "inf_start_time": "64800",
+ "inf_type": "auto",
+ "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": "off",
+ "smartir_level": "100",
+ "wb_B_gain": "50",
+ "wb_G_gain": "50",
+ "wb_R_gain": "50",
+ "wb_type": "auto",
+ "wd_gain": "50",
+ "wide_dynamic": "off"
+ },
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getLedStatus": {
+ "led": {
+ "config": {
+ ".name": "config",
+ ".type": "led",
+ "enabled": "on"
+ }
+ }
+ },
+ "getLensMaskConfig": {
+ "lens_mask": {
+ "lens_mask_info": {
+ ".name": "lens_mask_info",
+ ".type": "lens_mask_info",
+ "enabled": "on"
+ }
+ }
+ },
+ "getLightFrequencyInfo": {
+ "image": {
+ "common": {
+ ".name": "common",
+ ".type": "para",
+ "area_compensation": "default",
+ "chroma": "50",
+ "contrast": "50",
+ "dehaze": "off",
+ "exp_gain": "0",
+ "exp_type": "auto",
+ "focus_limited": "600",
+ "focus_type": "semi_auto",
+ "high_light_compensation": "off",
+ "inf_delay": "10",
+ "inf_end_time": "21600",
+ "inf_sensitivity": "1",
+ "inf_start_time": "64800",
+ "inf_type": "auto",
+ "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": "off",
+ "smartir_level": "100",
+ "wb_B_gain": "50",
+ "wb_G_gain": "50",
+ "wb_R_gain": "50",
+ "wb_type": "auto",
+ "wd_gain": "50",
+ "wide_dynamic": "off"
+ }
+ }
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ ".name": "media_encrypt",
+ ".type": "on_off",
+ "enabled": "on"
+ }
+ }
+ },
+ "getMsgPushConfig": {
+ "msg_push": {
+ "chn1_msg_push_info": {
+ ".name": "chn1_msg_push_info",
+ ".type": "on_off",
+ "notification_enabled": "on",
+ "rich_notification_enabled": "off"
+ }
+ }
+ },
+ "getNightVisionModeConfig": {
+ "image": {
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getPersonDetectionConfig": {
+ "people_detection": {
+ "detection": {
+ ".name": "detection",
+ ".type": "on_off",
+ "enabled": "off"
+ }
+ }
+ },
+ "getPresetConfig": {
+ "preset": {
+ "preset": {
+ "id": [
+ "1"
+ ],
+ "name": [
+ "Viewpoint 1"
+ ],
+ "position_pan": [
+ "0.088935"
+ ],
+ "position_tilt": [
+ "-1.000000"
+ ],
+ "read_only": [
+ "0"
+ ]
+ }
+ }
+ },
+ "getRecordPlan": {
+ "record_plan": {
+ "chn1_channel": {
+ ".name": "chn1_channel",
+ ".type": "plan",
+ "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": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getSdCardStatus": {
+ "harddisk_manage": {
+ "hd_info": [
+ {
+ "hd_info_1": {
+ "detect_status": "offline",
+ "disk_name": "1",
+ "free_space": "0B",
+ "loop_record_status": "0",
+ "msg_push_free_space": "0B",
+ "msg_push_total_space": "0B",
+ "percent": "0",
+ "picture_free_space": "0B",
+ "picture_total_space": "0B",
+ "record_duration": "0",
+ "record_free_duration": "0",
+ "record_start_time": "0",
+ "rw_attr": "r",
+ "status": "offline",
+ "total_space": "0B",
+ "type": "local",
+ "video_free_space": "0B",
+ "video_total_space": "0B",
+ "write_protect": "0"
+ }
+ }
+ ]
+ }
+ },
+ "getTamperDetectionConfig": {
+ "tamper_detection": {
+ "tamper_det": {
+ ".name": "tamper_det",
+ ".type": "on_off",
+ "digital_sensitivity": "50",
+ "enabled": "off",
+ "sensitivity": "medium"
+ }
+ }
+ },
+ "getTargetTrackConfig": {
+ "target_track": {
+ "target_track_info": {
+ ".name": "target_track_info",
+ ".type": "target_track_info",
+ "enabled": "off"
+ }
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ ".name": "basic",
+ ".type": "setting",
+ "timezone": "UTC-00:00",
+ "timing_mode": "ntp",
+ "zone_id": "Europe/London"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ ".name": "main",
+ ".type": "capability",
+ "bitrate_types": [
+ "cbr",
+ "vbr"
+ ],
+ "bitrates": [
+ "256",
+ "512",
+ "1024",
+ "2048"
+ ],
+ "encode_types": [
+ "H264"
+ ],
+ "frame_rates": [
+ "65537",
+ "65546",
+ "65551"
+ ],
+ "qualitys": [
+ "1",
+ "3",
+ "5"
+ ],
+ "resolutions": [
+ "1920*1080",
+ "1280*720",
+ "640*360"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ ".name": "main",
+ ".type": "stream",
+ "bitrate": "1024",
+ "bitrate_type": "vbr",
+ "encode_type": "H264",
+ "frame_rate": "65551",
+ "gop_factor": "2",
+ "name": "VideoEncoder_1",
+ "quality": "3",
+ "resolution": "1280*720",
+ "stream_type": "general"
+ }
+ }
+ },
+ "getWhitelampConfig": {
+ "image": {
+ "switch": {
+ ".name": "switch",
+ ".type": "switch_type",
+ "flip_type": "off",
+ "ldc": "off",
+ "night_vision_mode": "inf_night_vision",
+ "rotate_type": "off",
+ "schedule_end_time": "64800",
+ "schedule_start_time": "21600",
+ "switch_mode": "common",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getWhitelampStatus": {
+ "rest_time": 0,
+ "status": 0
+ },
+ "get_audio_capability": {
+ "get": {
+ "audio_capability": {
+ "device_microphone": {
+ ".name": "device_microphone",
+ ".type": "capability",
+ "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": {
+ ".name": "device_speaker",
+ ".type": "capability",
+ "channels": "1",
+ "decode_type": [
+ "G711"
+ ],
+ "mute": "0",
+ "sampling_rate": [
+ "8"
+ ],
+ "volume": "1"
+ }
+ }
+ }
+ },
+ "get_audio_config": {
+ "get": {
+ "audio_config": {
+ "microphone": {
+ ".name": "microphone",
+ ".type": "audio_config",
+ "channels": "1",
+ "encode_type": "G711alaw",
+ "mute": "off",
+ "noise_cancelling": "on",
+ "sampling_rate": "8",
+ "volume": "100"
+ },
+ "speaker": {
+ ".name": "speaker",
+ ".type": "audio_config",
+ "volume": "100"
+ }
+ }
+ }
+ },
+ "get_cet": {
+ "get": {
+ "cet": {
+ "vhttpd": {
+ ".name": "vhttpd",
+ ".type": "server",
+ "port": "8800"
+ }
+ }
+ }
+ },
+ "get_function": {
+ "get": {
+ "function": {
+ "module_spec": {
+ ".name": "module_spec",
+ ".type": "module-spec",
+ "ae_weighting_table_resolution": "5*5",
+ "ai_enhance_capability": "1",
+ "app_version": "1.0.0",
+ "audio": [
+ "speaker",
+ "microphone"
+ ],
+ "audioexception_detection": "0",
+ "auth_encrypt": "1",
+ "backlight_coexistence": "1",
+ "change_password": "1",
+ "client_info": "1",
+ "cloud_storage_version": "1.0",
+ "custom_area_compensation": "1",
+ "custom_auto_mode_exposure_level": "0",
+ "device_share": [
+ "preview",
+ "playback",
+ "voice",
+ "motor",
+ "cloud_storage"
+ ],
+ "download": [
+ "video"
+ ],
+ "events": [
+ "motion",
+ "tamper"
+ ],
+ "greeter": "1.0",
+ "http_system_state_audio_support": "1",
+ "intrusion_detection": "1",
+ "led": "1",
+ "lens_mask": "1",
+ "linecrossing_detection": "1",
+ "linkage_capability": "1",
+ "local_storage": "1",
+ "media_encrypt": "1",
+ "msg_alarm": "1",
+ "msg_alarm_list": [
+ "sound",
+ "light"
+ ],
+ "msg_alarm_separate_list": [
+ "light",
+ "sound"
+ ],
+ "msg_push": "1",
+ "multi_user": "0",
+ "multicast": "0",
+ "network": [
+ "wifi"
+ ],
+ "ota_upgrade": "1",
+ "p2p_support_versions": [
+ "1.1"
+ ],
+ "playback": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "playback_scale": "1",
+ "preview": [
+ "local",
+ "p2p",
+ "relay"
+ ],
+ "privacy_mask_api_version": "1.0",
+ "ptz": "1",
+ "record_max_slot_cnt": "10",
+ "record_type": [
+ "timing",
+ "motion"
+ ],
+ "relay_support_versions": [
+ "1.3"
+ ],
+ "reonboarding": "1",
+ "smart_detection": "1",
+ "smart_msg_push_capability": "1",
+ "ssl_cer_version": "1.0",
+ "storage_api_version": "2.2",
+ "stream_max_sessions": "10",
+ "streaming_support_versions": [
+ "1.0"
+ ],
+ "target_track": "1.0",
+ "timing_reboot": "1",
+ "verification_change_password": "1",
+ "video_codec": [
+ "h264"
+ ],
+ "video_detection_digital_sensitivity": "1",
+ "wifi_cascade_connection": "1",
+ "wifi_connection_info": "1",
+ "wireless_hotspot": "1"
+ }
+ }
+ }
+ },
+ "get_motor": {
+ "get": {
+ "motor": {
+ "capability": {
+ ".name": "capability",
+ ".type": "ptz",
+ "absolute_move_supported": "1",
+ "calibrate_supported": "1",
+ "continuous_move_supported": "1",
+ "eflip_mode": [
+ "off",
+ "on"
+ ],
+ "home_position_mode": "none",
+ "limit_supported": "0",
+ "manual_control_level": [
+ "low",
+ "normal",
+ "high"
+ ],
+ "manual_control_mode": [
+ "compatible",
+ "pedestrian",
+ "motor_vehicle",
+ "non_motor_vehicle",
+ "self_adaptive"
+ ],
+ "park_supported": "0",
+ "pattern_supported": "0",
+ "plan_supported": "0",
+ "position_pan_range": [
+ "-1.000000",
+ "1.000000"
+ ],
+ "position_tilt_range": [
+ "-1.000000",
+ "1.000000"
+ ],
+ "poweroff_save_supported": "1",
+ "poweroff_save_time_range": [
+ "10",
+ "600"
+ ],
+ "preset_number_max": "8",
+ "preset_supported": "1",
+ "relative_move_supported": "1",
+ "reverse_mode": [
+ "off",
+ "on",
+ "auto"
+ ],
+ "scan_supported": "0",
+ "speed_pan_max": "1.00000",
+ "speed_tilt_max": "1.000000",
+ "tour_supported": "0"
+ }
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/smartcam/child/D230(EU)_1.20_1.1.19.json b/tests/fixtures/smartcam/child/D230(EU)_1.20_1.1.19.json
new file mode 100644
index 00000000..83ed36c1
--- /dev/null
+++ b/tests/fixtures/smartcam/child/D230(EU)_1.20_1.1.19.json
@@ -0,0 +1,525 @@
+{
+ "child_info_from_parent": {
+ "alias": "#MASKED_NAME#",
+ "anti_theft_status": 0,
+ "avatar": "camera d210",
+ "battery_charging": "NO",
+ "battery_installed": 1,
+ "battery_percent": 90,
+ "battery_temperature": 5,
+ "battery_voltage": 4073,
+ "cam_uptime": 5420,
+ "category": "camera",
+ "dev_name": "Tapo Smart Doorbell",
+ "device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
+ "device_model": "D230",
+ "device_name": "D230 1.20",
+ "device_type": "SMART.TAPODOORBELL",
+ "ext_addr": "0000000000000000",
+ "firmware_status": "OK",
+ "hw_id": "00000000000000000000000000000000",
+ "hw_ver": "1.20",
+ "ipaddr": "172.23.30.2",
+ "led_status": "on",
+ "low_battery": false,
+ "mac": "F0:09:0D:00:00:00",
+ "oem_id": "00000000000000000000000000000000",
+ "onboarding_timestamp": 1732920657,
+ "online": true,
+ "parent_device_id": "0000000000000000000000000000000000000000",
+ "power": "BATTERY",
+ "power_save_mode": "off",
+ "power_save_status": "off",
+ "region": "EU",
+ "rssi": -43,
+ "short_addr": 0,
+ "status": "configured",
+ "subg_cam_rssi": 0,
+ "subg_hub_rssi": 0,
+ "sw_ver": "1.1.19 Build 20241011 rel.67020",
+ "system_time": 1735996806,
+ "updating": false,
+ "uptime": 3062029
+ },
+ "getAppComponentList": {
+ "app_component": {
+ "app_component_list": [
+ {
+ "name": "sdCard",
+ "version": 2
+ },
+ {
+ "name": "timezone",
+ "version": 1
+ },
+ {
+ "name": "batCamSystem",
+ "version": 1
+ },
+ {
+ "name": "led",
+ "version": 1
+ },
+ {
+ "name": "playback",
+ "version": 3
+ },
+ {
+ "name": "detection",
+ "version": 3
+ },
+ {
+ "name": "firmware",
+ "version": 1
+ },
+ {
+ "name": "video",
+ "version": 3
+ },
+ {
+ "name": "lensMask",
+ "version": 2
+ },
+ {
+ "name": "lightFrequency",
+ "version": 2
+ },
+ {
+ "name": "dayNightMode",
+ "version": 2
+ },
+ {
+ "name": "nightVisionMode",
+ "version": 3
+ },
+ {
+ "name": "batCamOsd",
+ "version": 1
+ },
+ {
+ "name": "record",
+ "version": 1
+ },
+ {
+ "name": "audio",
+ "version": 3
+ },
+ {
+ "name": "personDetection",
+ "version": 2
+ },
+ {
+ "name": "vehicleDetection",
+ "version": 1
+ },
+ {
+ "name": "petDetection",
+ "version": 1
+ },
+ {
+ "name": "msgPush",
+ "version": 3
+ },
+ {
+ "name": "deviceShare",
+ "version": 1
+ },
+ {
+ "name": "tapoCare",
+ "version": 1
+ },
+ {
+ "name": "pir",
+ "version": 1
+ },
+ {
+ "name": "battery",
+ "version": 3
+ },
+ {
+ "name": "clips",
+ "version": 1
+ },
+ {
+ "name": "batCamRelay",
+ "version": 1
+ },
+ {
+ "name": "batCamP2p",
+ "version": 1
+ },
+ {
+ "name": "needSubscriptionServiceList",
+ "version": 1
+ },
+ {
+ "name": "iotCloud",
+ "version": 1
+ },
+ {
+ "name": "blockZone",
+ "version": 1
+ },
+ {
+ "name": "whiteLamp",
+ "version": 1
+ },
+ {
+ "name": "infLamp",
+ "version": 1
+ },
+ {
+ "name": "packageDetection",
+ "version": 3
+ },
+ {
+ "name": "wakeUp",
+ "version": 1
+ },
+ {
+ "name": "ring",
+ "version": 1
+ },
+ {
+ "name": "antiTheft",
+ "version": 3
+ },
+ {
+ "name": "quickResponse",
+ "version": 2
+ },
+ {
+ "name": "doorbellNightVision",
+ "version": 1
+ },
+ {
+ "name": "dataDownload",
+ "version": 1
+ },
+ {
+ "name": "detectionRegion",
+ "version": 2
+ },
+ {
+ "name": "streamGrab",
+ "version": 1
+ },
+ {
+ "name": "recordDownload",
+ "version": 1
+ },
+ {
+ "name": "batCamPreRelay",
+ "version": 1
+ },
+ {
+ "name": "batCamStatistics",
+ "version": 1
+ },
+ {
+ "name": "batCamNodeRelay",
+ "version": 1
+ },
+ {
+ "name": "batCamRtsp",
+ "version": 2
+ }
+ ]
+ }
+ },
+ "getAudioConfig": {
+ "audio_config": {
+ "microphone": {
+ "channels": "1",
+ "encode_type": "G711ulaw",
+ "sampling_rate": "8",
+ "volume": "58"
+ },
+ "microphone_algo": {
+ "aec": "on",
+ "hs": "off",
+ "ns": "off",
+ "sys_aec": "on"
+ },
+ "record_audio": {
+ "enabled": "on"
+ },
+ "speaker": {
+ "volume": "80"
+ },
+ "speaker_algo": {
+ "hs": "off",
+ "ns": "off"
+ }
+ }
+ },
+ "getCircularRecordingConfig": {
+ "harddisk_manage": {
+ "harddisk": {
+ "loop": "on"
+ }
+ }
+ },
+ "getClockStatus": {
+ "system": {
+ "clock_status": {
+ "local_time": "2025-01-04 14:20:10",
+ "seconds_from_1970": 1735996810
+ }
+ }
+ },
+ "getDetectionConfig": {
+ "motion_detection": {
+ "motion_det": {
+ "digital_sensitivity": "30",
+ "enabled": "on",
+ "sensitivity": "low"
+ },
+ "region_info": []
+ }
+ },
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "a_type": 3,
+ "anti_theft_status": 0,
+ "avatar": "camera d210",
+ "battery_charging": "NO",
+ "battery_overheated": false,
+ "battery_percent": 90,
+ "c_opt": [
+ 0,
+ 1
+ ],
+ "camera_switch": "on",
+ "dev_id": "SCRUBBED_CHILD_DEVICE_ID_1",
+ "device_alias": "#MASKED_NAME#",
+ "device_model": "D230",
+ "device_name": "#MASKED_NAME#",
+ "device_type": "SMART.TAPODOORBELL",
+ "firmware_status": "OK",
+ "hw_version": "1.20",
+ "last_activity_timestamp": 1735996775,
+ "led_status": "on",
+ "low_battery": false,
+ "mac": "F0-09-0D-00-00-00",
+ "oem_id": "00000000000000000000000000000000",
+ "online": true,
+ "parent_device_id": "0000000000000000000000000000000000000000",
+ "parent_link_type": "ethernet",
+ "power": "BATTERY",
+ "power_save_mode": "off",
+ "resolution": "2560*1920",
+ "rssi": -43,
+ "status": "configured",
+ "sw_version": "1.1.19 Build 20241011 rel.67020",
+ "system_time": 1735996808,
+ "updating": false
+ }
+ }
+ },
+ "getFirmwareUpdateStatus": {
+ "cloud_config": {
+ "upgrade_status": {
+ "lastUpgradingSuccess": true,
+ "state": "normal"
+ }
+ }
+ },
+ "getLastAlarmInfo": {
+ "system": {
+ "last_alarm_info": {
+ "last_alarm_time": "1735996775",
+ "last_alarm_type": "motion"
+ }
+ }
+ },
+ "getLdc": {
+ "image": {
+ "switch": {
+ "ldc": "off"
+ }
+ }
+ },
+ "getLedStatus": {
+ "led": {
+ "config": {
+ "enabled": "on"
+ }
+ }
+ },
+ "getLensMaskConfig": {
+ "lens_mask": {
+ "lens_mask_info": {
+ "enabled": "off"
+ }
+ }
+ },
+ "getLightFrequencyInfo": {
+ "image": {
+ "common": {
+ "light_freq_mode": "50"
+ }
+ }
+ },
+ "getLightTypeList": {
+ "light_type_list": [
+ "flicker"
+ ]
+ },
+ "getMediaEncrypt": {
+ "cet": {
+ "media_encrypt": {
+ "enabled": "on"
+ }
+ }
+ },
+ "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",
+ "dbl_night_vision"
+ ],
+ "supplement_lamp_type": [
+ "infrared_lamp",
+ "white_lamp"
+ ]
+ }
+ }
+ },
+ "getNightVisionModeConfig": {
+ "image": {
+ "switch": {
+ "night_vision_mode": "dbl_night_vision"
+ }
+ }
+ },
+ "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": {
+ "flip_type": "off"
+ }
+ }
+ },
+ "getSdCardStatus": {
+ "harddisk_manage": {
+ "hd_info": [
+ {
+ "hd_info_1": {
+ "detect_status": "offline",
+ "disk_name": "1",
+ "loop_record_status": "1",
+ "status": "offline"
+ }
+ }
+ ]
+ }
+ },
+ "getTimezone": {
+ "system": {
+ "basic": {
+ "timezone": "UTC+01:00",
+ "timing_mode": "ntp",
+ "zone_id": "Europe/Amsterdam"
+ }
+ }
+ },
+ "getVehicleDetectionConfig": {
+ "vehicle_detection": {
+ "detection": {
+ "enabled": "off",
+ "sensitivity": "60"
+ }
+ }
+ },
+ "getVideoCapability": {
+ "video_capability": {
+ "main": {
+ "bitrate_types": [
+ "vbr"
+ ],
+ "bitrates": [
+ "1457"
+ ],
+ "change_fps_support": "0",
+ "encode_types": [
+ "H264"
+ ],
+ "frame_rates": [
+ 65551
+ ],
+ "minor_stream_support": "1",
+ "qualities": [
+ "5"
+ ],
+ "resolutions": [
+ "2560*1920"
+ ]
+ }
+ }
+ },
+ "getVideoQualities": {
+ "video": {
+ "main": {
+ "bitrate": "1943",
+ "bitrate_type": "vbr",
+ "encode_type": "H264",
+ "frame_rate": "65551",
+ "quality": "5",
+ "resolution": "2560*1920"
+ }
+ }
+ },
+ "getWhitelampConfig": {
+ "image": {
+ "switch": {
+ "wtl_force_time": "300",
+ "wtl_intensity_level": "5"
+ }
+ }
+ },
+ "getWhitelampStatus": {
+ "rest_time": 0,
+ "status": 0
+ }
+}
diff --git a/tests/iot/test_iotbulb.py b/tests/iot/test_iotbulb.py
index b573a545..5b759c58 100644
--- a/tests/iot/test_iotbulb.py
+++ b/tests/iot/test_iotbulb.py
@@ -91,7 +91,9 @@ async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
light = dev.modules.get(Module.Light)
assert light
- assert light.valid_temperature_range == (2700, 5000)
+ color_temp_feat = light.get_feature("color_temp")
+ assert color_temp_feat
+ assert color_temp_feat.range == (2700, 5000)
assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text
diff --git a/tests/iot/test_iotdevice.py b/tests/iot/test_iotdevice.py
index 124910b7..858c5fbc 100644
--- a/tests/iot/test_iotdevice.py
+++ b/tests/iot/test_iotdevice.py
@@ -99,7 +99,7 @@ async def test_invalid_connection(mocker, dev):
@has_emeter_iot
async def test_initial_update_emeter(dev, mocker):
"""Test that the initial update performs second query if emeter is available."""
- dev._last_update = None
+ dev._last_update = {}
dev._legacy_features = set()
spy = mocker.spy(dev.protocol, "query")
await dev.update()
@@ -111,7 +111,7 @@ async def test_initial_update_emeter(dev, mocker):
@no_emeter_iot
async def test_initial_update_no_emeter(dev, mocker):
"""Test that the initial update performs second query if emeter is available."""
- dev._last_update = None
+ dev._last_update = {}
dev._legacy_features = set()
spy = mocker.spy(dev.protocol, "query")
await dev.update()
diff --git a/tests/protocols/test_iotprotocol.py b/tests/protocols/test_iotprotocol.py
index a2feaae3..fd8facc9 100644
--- a/tests/protocols/test_iotprotocol.py
+++ b/tests/protocols/test_iotprotocol.py
@@ -16,7 +16,7 @@ import pytest
from kasa.credentials import Credentials
from kasa.device import Device
from kasa.deviceconfig import DeviceConfig
-from kasa.exceptions import KasaException
+from kasa.exceptions import KasaException, TimeoutError
from kasa.iot import IotDevice
from kasa.protocols.iotprotocol import IotProtocol, _deprecated_TPLinkSmartHomeProtocol
from kasa.protocols.protocol import (
@@ -294,6 +294,210 @@ async def test_protocol_handles_cancellation_during_connection(
assert response == {"great": "success"}
+@pytest.mark.parametrize(
+ ("protocol_class", "transport_class", "encryption_class"),
+ [
+ (
+ _deprecated_TPLinkSmartHomeProtocol,
+ XorTransport,
+ _deprecated_TPLinkSmartHomeProtocol,
+ ),
+ (IotProtocol, XorTransport, XorEncryption),
+ ],
+ ids=("_deprecated_TPLinkSmartHomeProtocol", "IotProtocol-XorTransport"),
+)
+async def test_protocol_handles_timeout_during_write(
+ mocker, protocol_class, transport_class, encryption_class
+):
+ attempts = 0
+ encrypted = encryption_class.encrypt('{"great":"success"}')[
+ transport_class.BLOCK_SIZE :
+ ]
+
+ def _timeout_first_attempt(*_):
+ nonlocal attempts
+ attempts += 1
+ if attempts == 1:
+ raise TimeoutError("Simulated timeout")
+
+ async def _mock_read(byte_count):
+ nonlocal encrypted
+ if byte_count == transport_class.BLOCK_SIZE:
+ return struct.pack(">I", len(encrypted))
+ if byte_count == len(encrypted):
+ return encrypted
+
+ raise ValueError(f"No mock for {byte_count}")
+
+ def aio_mock_writer(_, __):
+ reader = mocker.patch("asyncio.StreamReader")
+ writer = mocker.patch("asyncio.StreamWriter")
+ mocker.patch.object(writer, "write", _timeout_first_attempt)
+ mocker.patch.object(reader, "readexactly", _mock_read)
+ mocker.patch.object(writer, "drain", new_callable=AsyncMock)
+ return reader, writer
+
+ config = DeviceConfig("127.0.0.1")
+ protocol = protocol_class(transport=transport_class(config=config))
+ mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer)
+ await protocol.query({})
+ writer_obj = protocol if hasattr(protocol, "writer") else protocol._transport
+ assert writer_obj.writer is not None
+ response = await protocol.query({})
+ assert response == {"great": "success"}
+
+
+@pytest.mark.parametrize(
+ ("protocol_class", "transport_class", "encryption_class"),
+ [
+ (
+ _deprecated_TPLinkSmartHomeProtocol,
+ XorTransport,
+ _deprecated_TPLinkSmartHomeProtocol,
+ ),
+ (IotProtocol, XorTransport, XorEncryption),
+ ],
+ ids=("_deprecated_TPLinkSmartHomeProtocol", "IotProtocol-XorTransport"),
+)
+async def test_protocol_handles_timeout_during_connection(
+ mocker, protocol_class, transport_class, encryption_class
+):
+ attempts = 0
+ encrypted = encryption_class.encrypt('{"great":"success"}')[
+ transport_class.BLOCK_SIZE :
+ ]
+
+ async def _mock_read(byte_count):
+ nonlocal encrypted
+ if byte_count == transport_class.BLOCK_SIZE:
+ return struct.pack(">I", len(encrypted))
+ if byte_count == len(encrypted):
+ return encrypted
+
+ raise ValueError(f"No mock for {byte_count}")
+
+ def aio_mock_writer(_, __):
+ nonlocal attempts
+ attempts += 1
+ if attempts == 1:
+ raise TimeoutError("Simulated timeout")
+ reader = mocker.patch("asyncio.StreamReader")
+ writer = mocker.patch("asyncio.StreamWriter")
+ mocker.patch.object(reader, "readexactly", _mock_read)
+ mocker.patch.object(writer, "drain", new_callable=AsyncMock)
+ return reader, writer
+
+ config = DeviceConfig("127.0.0.1")
+ protocol = protocol_class(transport=transport_class(config=config))
+ writer_obj = protocol if hasattr(protocol, "writer") else protocol._transport
+ await writer_obj.close()
+
+ mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer)
+ await protocol.query({"any": "thing"})
+
+ writer_obj = protocol if hasattr(protocol, "writer") else protocol._transport
+ assert writer_obj.writer is not None
+ response = await protocol.query({})
+ assert response == {"great": "success"}
+
+
+@pytest.mark.parametrize(
+ ("protocol_class", "transport_class", "encryption_class"),
+ [
+ (
+ _deprecated_TPLinkSmartHomeProtocol,
+ XorTransport,
+ _deprecated_TPLinkSmartHomeProtocol,
+ ),
+ (IotProtocol, XorTransport, XorEncryption),
+ ],
+ ids=("_deprecated_TPLinkSmartHomeProtocol", "IotProtocol-XorTransport"),
+)
+async def test_protocol_handles_timeout_failure_during_write(
+ mocker, protocol_class, transport_class, encryption_class
+):
+ encrypted = encryption_class.encrypt('{"great":"success"}')[
+ transport_class.BLOCK_SIZE :
+ ]
+
+ def _timeout_all_attempts(*_):
+ raise TimeoutError("Simulated timeout")
+
+ async def _mock_read(byte_count):
+ nonlocal encrypted
+ if byte_count == transport_class.BLOCK_SIZE:
+ return struct.pack(">I", len(encrypted))
+ if byte_count == len(encrypted):
+ return encrypted
+
+ raise ValueError(f"No mock for {byte_count}")
+
+ def aio_mock_writer(_, __):
+ reader = mocker.patch("asyncio.StreamReader")
+ writer = mocker.patch("asyncio.StreamWriter")
+ mocker.patch.object(writer, "write", _timeout_all_attempts)
+ mocker.patch.object(reader, "readexactly", _mock_read)
+ mocker.patch.object(writer, "drain", new_callable=AsyncMock)
+ return reader, writer
+
+ config = DeviceConfig("127.0.0.1")
+ protocol = protocol_class(transport=transport_class(config=config))
+ mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer)
+ with pytest.raises(
+ TimeoutError,
+ match="Timeout after 5 seconds sending request to the device 127.0.0.1:9999: Simulated timeout",
+ ):
+ await protocol.query({})
+ writer_obj = protocol if hasattr(protocol, "writer") else protocol._transport
+ assert writer_obj.writer is None
+
+
+@pytest.mark.parametrize(
+ ("protocol_class", "transport_class", "encryption_class"),
+ [
+ (
+ _deprecated_TPLinkSmartHomeProtocol,
+ XorTransport,
+ _deprecated_TPLinkSmartHomeProtocol,
+ ),
+ (IotProtocol, XorTransport, XorEncryption),
+ ],
+ ids=("_deprecated_TPLinkSmartHomeProtocol", "IotProtocol-XorTransport"),
+)
+async def test_protocol_handles_timeout_failure_during_connection(
+ mocker, protocol_class, transport_class, encryption_class
+):
+ encrypted = encryption_class.encrypt('{"great":"success"}')[
+ transport_class.BLOCK_SIZE :
+ ]
+
+ async def _mock_read(byte_count):
+ nonlocal encrypted
+ if byte_count == transport_class.BLOCK_SIZE:
+ return struct.pack(">I", len(encrypted))
+ if byte_count == len(encrypted):
+ return encrypted
+
+ raise ValueError(f"No mock for {byte_count}")
+
+ def aio_mock_writer(_, __):
+ raise TimeoutError("Simulated timeout")
+
+ config = DeviceConfig("127.0.0.1")
+ protocol = protocol_class(transport=transport_class(config=config))
+ writer_obj = protocol if hasattr(protocol, "writer") else protocol._transport
+ await writer_obj.close()
+
+ mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer)
+ with pytest.raises(
+ TimeoutError,
+ match="Timeout after 5 seconds connecting to the device: 127.0.0.1:9999: Simulated timeout",
+ ):
+ await protocol.query({})
+ writer_obj = protocol if hasattr(protocol, "writer") else protocol._transport
+ assert writer_obj.writer is None
+
+
@pytest.mark.parametrize(
("protocol_class", "transport_class", "encryption_class"),
[
diff --git a/tests/protocols/test_smartprotocol.py b/tests/protocols/test_smartprotocol.py
index 988c95eb..51492635 100644
--- a/tests/protocols/test_smartprotocol.py
+++ b/tests/protocols/test_smartprotocol.py
@@ -2,6 +2,7 @@ import logging
import pytest
import pytest_mock
+from pytest_mock import MockerFixture
from kasa.exceptions import (
SMART_RETRYABLE_ERRORS,
@@ -9,11 +10,13 @@ from kasa.exceptions import (
KasaException,
SmartErrorCode,
)
+from kasa.protocols.smartcamprotocol import SmartCamProtocol
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
from kasa.smart import SmartDevice
from ..conftest import device_smart
from ..fakeprotocol_smart import FakeSmartTransport
+from ..fakeprotocol_smartcam import FakeSmartCamTransport
DUMMY_QUERY = {"foobar": {"foo": "bar", "bar": "foo"}}
DUMMY_MULTIPLE_QUERY = {
@@ -371,6 +374,46 @@ async def test_smart_protocol_lists_multiple_request(mocker, list_sum, batch_siz
assert resp == response
+@pytest.mark.parametrize("list_sum", [5, 10, 30])
+@pytest.mark.parametrize("batch_size", [1, 2, 3, 50])
+async def test_smartcam_protocol_list_request(mocker, list_sum, batch_size):
+ """Test smartcam protocol list handling for lists."""
+ child_list = [{"foo": i} for i in range(list_sum)]
+
+ response = {
+ "getChildDeviceList": {
+ "child_device_list": child_list,
+ "start_index": 0,
+ "sum": list_sum,
+ },
+ "getChildDeviceComponentList": {
+ "child_component_list": child_list,
+ "start_index": 0,
+ "sum": list_sum,
+ },
+ }
+ request = {
+ "getChildDeviceList": {"childControl": {"start_index": 0}},
+ "getChildDeviceComponentList": {"childControl": {"start_index": 0}},
+ }
+
+ ft = FakeSmartCamTransport(
+ response,
+ "foobar",
+ list_return_size=batch_size,
+ components_not_included=True,
+ get_child_fixtures=False,
+ )
+ protocol = SmartCamProtocol(transport=ft)
+ query_spy = mocker.spy(protocol, "_execute_query")
+ resp = await protocol.query(request)
+ expected_count = 1 + 2 * (
+ int(list_sum / batch_size) + (0 if list_sum % batch_size else -1)
+ )
+ assert query_spy.call_count == expected_count
+ assert resp == response
+
+
async def test_incomplete_list(mocker, caplog):
"""Test for handling incomplete lists returned from queries."""
info = {
@@ -448,3 +491,81 @@ async def test_smart_queries_redaction(
await dev.update()
assert device_id not in caplog.text
assert "REDACTED_" + device_id[9::] in caplog.text
+
+
+async def test_no_method_returned_multiple(
+ mocker: MockerFixture, caplog: pytest.LogCaptureFixture
+):
+ """Test protocol handles multiple requests that don't return the method."""
+ req = {
+ "getDeviceInfo": {"device_info": {"name": ["basic_info", "info"]}},
+ "getAppComponentList": {"app_component": {"name": "app_component_list"}},
+ }
+ res = {
+ "result": {
+ "responses": [
+ {
+ "method": "getDeviceInfo",
+ "result": {
+ "device_info": {
+ "basic_info": {
+ "device_model": "C210",
+ },
+ }
+ },
+ "error_code": 0,
+ },
+ {
+ "result": {"app_component": {"app_component_list": []}},
+ "error_code": 0,
+ },
+ ]
+ },
+ "error_code": 0,
+ }
+
+ transport = FakeSmartCamTransport(
+ {},
+ "dummy-name",
+ components_not_included=True,
+ )
+ protocol = SmartProtocol(transport=transport)
+ mocker.patch.object(protocol._transport, "send", return_value=res)
+ await protocol.query(req)
+ assert "No method key in response" in caplog.text
+ caplog.clear()
+ await protocol.query(req)
+ assert "No method key in response" not in caplog.text
+
+
+async def test_no_multiple_methods(
+ mocker: MockerFixture, caplog: pytest.LogCaptureFixture
+):
+ """Test protocol sends NO_MULTI methods as single call."""
+ req = {
+ "getDeviceInfo": {"device_info": {"name": ["basic_info", "info"]}},
+ "getConnectStatus": {"onboarding": {"get_connect_status": {}}},
+ }
+ info = {
+ "getDeviceInfo": {
+ "device_info": {
+ "basic_info": {
+ "avatar": "Home",
+ }
+ }
+ },
+ "getConnectStatus": {
+ "onboarding": {
+ "get_connect_status": {"current_ssid": "", "err_code": 0, "status": 0}
+ }
+ },
+ }
+ transport = FakeSmartCamTransport(
+ info,
+ "dummy-name",
+ components_not_included=True,
+ )
+ protocol = SmartProtocol(transport=transport)
+ send_spy = mocker.spy(protocol._transport, "send")
+ await protocol.query(req)
+ assert send_spy.call_count == 2
diff --git a/tests/smart/modules/test_childlock.py b/tests/smart/modules/test_childlock.py
new file mode 100644
index 00000000..2ffa9104
--- /dev/null
+++ b/tests/smart/modules/test_childlock.py
@@ -0,0 +1,44 @@
+import pytest
+
+from kasa import Module
+from kasa.smart.modules import ChildLock
+
+from ...device_fixtures import parametrize
+
+childlock = parametrize(
+ "has child lock",
+ component_filter="button_and_led",
+ protocol_filter={"SMART"},
+)
+
+
+@childlock
+@pytest.mark.parametrize(
+ ("feature", "prop_name", "type"),
+ [
+ ("child_lock", "enabled", bool),
+ ],
+)
+async def test_features(dev, feature, prop_name, type):
+ """Test that features are registered and work as expected."""
+ protect: ChildLock = dev.modules[Module.ChildLock]
+ assert protect is not None
+
+ prop = getattr(protect, prop_name)
+ assert isinstance(prop, type)
+
+ feat = protect._device.features[feature]
+ assert feat.value == prop
+ assert isinstance(feat.value, type)
+
+
+@childlock
+async def test_enabled(dev):
+ """Test the API."""
+ protect: ChildLock = dev.modules[Module.ChildLock]
+ assert protect is not None
+
+ assert isinstance(protect.enabled, bool)
+ await protect.set_enabled(False)
+ await dev.update()
+ assert protect.enabled is False
diff --git a/tests/smart/modules/test_childsetup.py b/tests/smart/modules/test_childsetup.py
new file mode 100644
index 00000000..df3905a6
--- /dev/null
+++ b/tests/smart/modules/test_childsetup.py
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+import logging
+
+import pytest
+from pytest_mock import MockerFixture
+
+from kasa import Feature, Module, SmartDevice
+
+from ...device_fixtures import parametrize
+
+childsetup = parametrize(
+ "supports pairing", component_filter="child_quick_setup", protocol_filter={"SMART"}
+)
+
+
+@childsetup
+async def test_childsetup_features(dev: SmartDevice):
+ """Test the exposed features."""
+ cs = dev.modules.get(Module.ChildSetup)
+ assert cs
+
+ assert "pair" in cs._module_features
+ pair = cs._module_features["pair"]
+ assert pair.type == Feature.Type.Action
+
+
+@childsetup
+async def test_childsetup_pair(
+ dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
+):
+ """Test device pairing."""
+ caplog.set_level(logging.INFO)
+ mock_query_helper = mocker.spy(dev, "_query_helper")
+ mocker.patch("asyncio.sleep")
+
+ cs = dev.modules.get(Module.ChildSetup)
+ assert cs
+
+ await cs.pair()
+
+ mock_query_helper.assert_has_awaits(
+ [
+ mocker.call("begin_scanning_child_device", None),
+ mocker.call("get_support_child_device_category", None),
+ mocker.call("get_scan_child_device_list", params=mocker.ANY),
+ mocker.call("add_child_device_list", params=mocker.ANY),
+ ]
+ )
+ assert "Discovery done" in caplog.text
+
+
+@childsetup
+async def test_childsetup_unpair(
+ dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
+):
+ """Test unpair."""
+ mock_query_helper = mocker.spy(dev, "_query_helper")
+ DUMMY_ID = "dummy_id"
+
+ cs = dev.modules.get(Module.ChildSetup)
+ assert cs
+
+ await cs.unpair(DUMMY_ID)
+
+ mock_query_helper.assert_awaited_with(
+ "remove_child_device_list",
+ params={"child_device_list": [{"device_id": DUMMY_ID}]},
+ )
diff --git a/tests/smart/modules/test_clean.py b/tests/smart/modules/test_clean.py
new file mode 100644
index 00000000..f4c2813c
--- /dev/null
+++ b/tests/smart/modules/test_clean.py
@@ -0,0 +1,248 @@
+from __future__ import annotations
+
+import logging
+
+import pytest
+from pytest_mock import MockerFixture
+
+from kasa import Module
+from kasa.smart import SmartDevice
+from kasa.smart.modules.clean import ErrorCode, Status
+
+from ...device_fixtures import get_parent_and_child_modules, parametrize
+
+clean = parametrize("clean module", component_filter="clean", protocol_filter={"SMART"})
+
+
+@clean
+@pytest.mark.parametrize(
+ ("feature", "prop_name", "type"),
+ [
+ ("vacuum_status", "status", Status),
+ ("vacuum_error", "error", ErrorCode),
+ ("vacuum_fan_speed", "fan_speed_preset", str),
+ ("carpet_clean_mode", "carpet_clean_mode", str),
+ ("battery_level", "battery", int),
+ ],
+)
+async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
+ """Test that features are registered and work as expected."""
+ clean = next(get_parent_and_child_modules(dev, Module.Clean))
+ assert clean is not None
+
+ prop = getattr(clean, prop_name)
+ assert isinstance(prop, type)
+
+ feat = clean._device.features[feature]
+ assert feat.value == prop
+ assert isinstance(feat.value, type)
+
+
+@pytest.mark.parametrize(
+ ("feature", "value", "method", "params"),
+ [
+ pytest.param(
+ "vacuum_start",
+ 1,
+ "setSwitchClean",
+ {
+ "clean_mode": 0,
+ "clean_on": True,
+ "clean_order": True,
+ "force_clean": False,
+ },
+ id="vacuum_start",
+ ),
+ pytest.param(
+ "vacuum_pause", 1, "setRobotPause", {"pause": True}, id="vacuum_pause"
+ ),
+ pytest.param(
+ "vacuum_return_home",
+ 1,
+ "setSwitchCharge",
+ {"switch_charge": True},
+ id="vacuum_return_home",
+ ),
+ pytest.param(
+ "vacuum_fan_speed",
+ "Quiet",
+ "setCleanAttr",
+ {"suction": 1, "type": "global"},
+ id="vacuum_fan_speed",
+ ),
+ pytest.param(
+ "carpet_clean_mode",
+ "Boost",
+ "setCarpetClean",
+ {"carpet_clean_prefer": "boost"},
+ id="carpet_clean_mode",
+ ),
+ pytest.param(
+ "clean_count",
+ 2,
+ "setCleanAttr",
+ {"clean_number": 2, "type": "global"},
+ id="clean_count",
+ ),
+ ],
+)
+@clean
+async def test_actions(
+ dev: SmartDevice,
+ mocker: MockerFixture,
+ feature: str,
+ value: str | int,
+ method: str,
+ params: dict,
+):
+ """Test the clean actions."""
+ clean = next(get_parent_and_child_modules(dev, Module.Clean))
+ call = mocker.spy(clean, "call")
+
+ await dev.features[feature].set_value(value)
+ call.assert_called_with(method, params)
+
+
+@pytest.mark.parametrize(
+ ("err_status", "error", "warning_msg"),
+ [
+ pytest.param([], ErrorCode.Ok, None, id="empty error"),
+ pytest.param([0], ErrorCode.Ok, None, id="no error"),
+ pytest.param([3], ErrorCode.MainBrushStuck, None, id="known error"),
+ pytest.param(
+ [123],
+ ErrorCode.UnknownInternal,
+ "Unknown error code, please create an issue describing the error: 123",
+ id="unknown error",
+ ),
+ pytest.param(
+ [3, 4],
+ ErrorCode.MainBrushStuck,
+ "Multiple error codes, using the first one only: [3, 4]",
+ id="multi-error",
+ ),
+ ],
+)
+@clean
+async def test_post_update_hook(
+ dev: SmartDevice,
+ err_status: list,
+ error: ErrorCode,
+ warning_msg: str | None,
+ caplog: pytest.LogCaptureFixture,
+):
+ """Test that post update hook sets error states correctly."""
+ clean = next(get_parent_and_child_modules(dev, Module.Clean))
+ assert clean
+
+ caplog.set_level(logging.DEBUG)
+
+ # _post_update_hook will pop an item off the status list so create a copy.
+ err_status = [e for e in err_status]
+ clean.data["getVacStatus"]["err_status"] = err_status
+
+ await clean._post_update_hook()
+
+ assert clean._error_code is error
+
+ if error is not ErrorCode.Ok:
+ assert clean.status is Status.Error
+
+ if warning_msg:
+ assert warning_msg in caplog.text
+
+ # Check doesn't log twice
+ caplog.clear()
+ await clean._post_update_hook()
+
+ if warning_msg:
+ assert warning_msg not in caplog.text
+
+
+@clean
+async def test_resume(dev: SmartDevice, mocker: MockerFixture):
+ """Test that start calls resume if the state is paused."""
+ clean = next(get_parent_and_child_modules(dev, Module.Clean))
+
+ call = mocker.spy(clean, "call")
+ resume = mocker.spy(clean, "resume")
+
+ mocker.patch.object(
+ type(clean),
+ "status",
+ new_callable=mocker.PropertyMock,
+ return_value=Status.Paused,
+ )
+ await clean.start()
+
+ call.assert_called_with("setRobotPause", {"pause": False})
+ resume.assert_awaited()
+
+
+@clean
+async def test_unknown_status(
+ dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
+):
+ """Test that unknown status is logged."""
+ clean = next(get_parent_and_child_modules(dev, Module.Clean))
+
+ caplog.set_level(logging.DEBUG)
+ clean.data["getVacStatus"]["status"] = 123
+
+ assert clean.status is Status.UnknownInternal
+ assert "Got unknown status code: 123" in caplog.text
+
+ # Check only logs once
+ caplog.clear()
+
+ assert clean.status is Status.UnknownInternal
+ assert "Got unknown status code: 123" not in caplog.text
+
+ # Check logs again for other errors
+
+ caplog.clear()
+ clean.data["getVacStatus"]["status"] = 123456
+
+ assert clean.status is Status.UnknownInternal
+ assert "Got unknown status code: 123456" in caplog.text
+
+
+@clean
+@pytest.mark.parametrize(
+ ("setting", "value", "exc", "exc_message"),
+ [
+ pytest.param(
+ "vacuum_fan_speed",
+ "invalid speed",
+ ValueError,
+ "Invalid fan speed",
+ id="vacuum_fan_speed",
+ ),
+ pytest.param(
+ "carpet_clean_mode",
+ "invalid mode",
+ ValueError,
+ "Invalid carpet clean mode",
+ id="carpet_clean_mode",
+ ),
+ ],
+)
+async def test_invalid_settings(
+ dev: SmartDevice,
+ mocker: MockerFixture,
+ setting: str,
+ value: str,
+ exc: type[Exception],
+ exc_message: str,
+):
+ """Test invalid settings."""
+ clean = next(get_parent_and_child_modules(dev, Module.Clean))
+
+ # Not using feature.set_value() as it checks for valid values
+ setter_name = dev.features[setting].attribute_setter
+ assert isinstance(setter_name, str)
+
+ setter = getattr(clean, setter_name)
+
+ with pytest.raises(exc, match=exc_message):
+ await setter(value)
diff --git a/tests/smart/modules/test_cleanrecords.py b/tests/smart/modules/test_cleanrecords.py
new file mode 100644
index 00000000..cef69286
--- /dev/null
+++ b/tests/smart/modules/test_cleanrecords.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from datetime import datetime, timedelta
+from zoneinfo import ZoneInfo
+
+import pytest
+
+from kasa import Module
+from kasa.smart import SmartDevice
+
+from ...device_fixtures import get_parent_and_child_modules, parametrize
+
+cleanrecords = parametrize(
+ "has clean records", component_filter="clean_percent", protocol_filter={"SMART"}
+)
+
+
+@cleanrecords
+@pytest.mark.parametrize(
+ ("feature", "prop_name", "type"),
+ [
+ ("total_clean_area", "total_clean_area", int),
+ ("total_clean_time", "total_clean_time", timedelta),
+ ("last_clean_area", "last_clean_area", int),
+ ("last_clean_time", "last_clean_time", timedelta),
+ ("total_clean_count", "total_clean_count", int),
+ ("last_clean_timestamp", "last_clean_timestamp", datetime),
+ ],
+)
+async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
+ """Test that features are registered and work as expected."""
+ records = next(get_parent_and_child_modules(dev, Module.CleanRecords))
+ assert records is not None
+
+ prop = getattr(records, prop_name)
+ assert isinstance(prop, type)
+
+ feat = records._device.features[feature]
+ assert feat.value == prop
+ assert isinstance(feat.value, type)
+
+
+@cleanrecords
+async def test_timezone(dev: SmartDevice):
+ """Test that timezone is added to timestamps."""
+ clean_records = next(get_parent_and_child_modules(dev, Module.CleanRecords))
+ assert clean_records is not None
+
+ assert isinstance(clean_records.last_clean_timestamp, datetime)
+ assert clean_records.last_clean_timestamp.tzinfo
+
+ # Check for zone info to ensure that this wasn't picking upthe default
+ # of utc before the time module is updated.
+ assert isinstance(clean_records.last_clean_timestamp.tzinfo, ZoneInfo)
+
+ for record in clean_records.parsed_data.records:
+ assert isinstance(record.timestamp, datetime)
+ assert record.timestamp.tzinfo
+ assert isinstance(record.timestamp.tzinfo, ZoneInfo)
diff --git a/tests/smart/modules/test_consumables.py b/tests/smart/modules/test_consumables.py
new file mode 100644
index 00000000..7a28f3be
--- /dev/null
+++ b/tests/smart/modules/test_consumables.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+import pytest
+from pytest_mock import MockerFixture
+
+from kasa import Module
+from kasa.smart import SmartDevice
+from kasa.smart.modules.consumables import CONSUMABLE_METAS
+
+from ...device_fixtures import get_parent_and_child_modules, parametrize
+
+consumables = parametrize(
+ "has consumables", component_filter="consumables", protocol_filter={"SMART"}
+)
+
+
+@consumables
+@pytest.mark.parametrize(
+ "consumable_name", [consumable.id for consumable in CONSUMABLE_METAS]
+)
+@pytest.mark.parametrize("postfix", ["used", "remaining"])
+async def test_features(dev: SmartDevice, consumable_name: str, postfix: str):
+ """Test that features are registered and work as expected."""
+ consumables = next(get_parent_and_child_modules(dev, Module.Consumables))
+ assert consumables is not None
+
+ feature_name = f"{consumable_name}_{postfix}"
+
+ feat = consumables._device.features[feature_name]
+ assert isinstance(feat.value, timedelta)
+
+
+@consumables
+@pytest.mark.parametrize(
+ ("consumable_name", "data_key"),
+ [(consumable.id, consumable.data_key) for consumable in CONSUMABLE_METAS],
+)
+async def test_erase(
+ dev: SmartDevice, mocker: MockerFixture, consumable_name: str, data_key: str
+):
+ """Test autocollection switch."""
+ consumables = next(get_parent_and_child_modules(dev, Module.Consumables))
+ call = mocker.spy(consumables, "call")
+
+ feature_name = f"{consumable_name}_reset"
+ feat = dev._features[feature_name]
+ await feat.set_value(True)
+
+ call.assert_called_with(
+ "resetConsumablesTime", {"reset_list": [data_key.removesuffix("_time")]}
+ )
diff --git a/tests/smart/modules/test_dustbin.py b/tests/smart/modules/test_dustbin.py
new file mode 100644
index 00000000..d30d2459
--- /dev/null
+++ b/tests/smart/modules/test_dustbin.py
@@ -0,0 +1,92 @@
+from __future__ import annotations
+
+import pytest
+from pytest_mock import MockerFixture
+
+from kasa import Module
+from kasa.smart import SmartDevice
+from kasa.smart.modules.dustbin import Mode
+
+from ...device_fixtures import get_parent_and_child_modules, parametrize
+
+dustbin = parametrize(
+ "has dustbin", component_filter="dust_bucket", protocol_filter={"SMART"}
+)
+
+
+@dustbin
+@pytest.mark.parametrize(
+ ("feature", "prop_name", "type"),
+ [
+ ("dustbin_autocollection_enabled", "auto_collection", bool),
+ ("dustbin_mode", "mode", str),
+ ],
+)
+async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
+ """Test that features are registered and work as expected."""
+ dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
+ assert dustbin is not None
+
+ prop = getattr(dustbin, prop_name)
+ assert isinstance(prop, type)
+
+ feat = dustbin._device.features[feature]
+ assert feat.value == prop
+ assert isinstance(feat.value, type)
+
+
+@dustbin
+async def test_dustbin_mode(dev: SmartDevice, mocker: MockerFixture):
+ """Test dust mode."""
+ dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
+ call = mocker.spy(dustbin, "call")
+
+ mode_feature = dustbin._device.features["dustbin_mode"]
+ assert dustbin.mode == mode_feature.value
+
+ new_mode = Mode.Max
+ await dustbin.set_mode(new_mode.name)
+
+ params = dustbin._settings.copy()
+ params["dust_collection_mode"] = new_mode.value
+
+ call.assert_called_with("setDustCollectionInfo", params)
+
+ await dev.update()
+
+ assert dustbin.mode == new_mode.name
+
+ with pytest.raises(ValueError, match="Invalid auto/emptying mode speed"):
+ await dustbin.set_mode("invalid")
+
+
+@dustbin
+async def test_autocollection(dev: SmartDevice, mocker: MockerFixture):
+ """Test autocollection switch."""
+ dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
+ call = mocker.spy(dustbin, "call")
+
+ auto_collection = dustbin._device.features["dustbin_autocollection_enabled"]
+ assert dustbin.auto_collection == auto_collection.value
+
+ await auto_collection.set_value(True)
+
+ params = dustbin._settings.copy()
+ params["auto_dust_collection"] = True
+
+ call.assert_called_with("setDustCollectionInfo", params)
+
+ await dev.update()
+
+ assert dustbin.auto_collection is True
+
+
+@dustbin
+async def test_empty_dustbin(dev: SmartDevice, mocker: MockerFixture):
+ """Test the empty dustbin feature."""
+ dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
+ call = mocker.spy(dustbin, "call")
+
+ await dustbin.start_emptying()
+
+ call.assert_called_with("setSwitchDustCollection", {"switch_dust_collection": True})
diff --git a/tests/smart/modules/test_energy.py b/tests/smart/modules/test_energy.py
index fdbea88b..7b31d74b 100644
--- a/tests/smart/modules/test_energy.py
+++ b/tests/smart/modules/test_energy.py
@@ -1,7 +1,14 @@
+import copy
+import logging
+from contextlib import nullcontext as does_not_raise
+from unittest.mock import patch
+
import pytest
-from kasa import Module, SmartDevice
+from kasa import DeviceError, Module
+from kasa.exceptions import SmartErrorCode
from kasa.interfaces.energy import Energy
+from kasa.smart import SmartDevice
from kasa.smart.modules import Energy as SmartEnergyModule
from tests.conftest import has_emeter_smart
@@ -19,3 +26,84 @@ async def test_supported(dev: SmartDevice):
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is False
else:
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is True
+
+
+@has_emeter_smart
+async def test_get_energy_usage_error(
+ dev: SmartDevice, caplog: pytest.LogCaptureFixture
+):
+ """Test errors on get_energy_usage."""
+ caplog.set_level(logging.DEBUG)
+
+ energy_module = dev.modules.get(Module.Energy)
+ if not energy_module:
+ pytest.skip(f"Energy module not supported for {dev}.")
+
+ version = dev._components["energy_monitoring"]
+
+ expected_raise = does_not_raise() if version > 1 else pytest.raises(DeviceError)
+ if version > 1:
+ expected = "get_energy_usage"
+ expected_current_consumption = 2.002
+ else:
+ expected = "current_power"
+ expected_current_consumption = None
+
+ assert expected in energy_module.data
+ assert energy_module.current_consumption is not None
+ assert energy_module.consumption_today is not None
+ assert energy_module.consumption_this_month is not None
+
+ last_update = copy.deepcopy(dev._last_update)
+ resp = copy.deepcopy(last_update)
+
+ if ed := resp.get("get_emeter_data"):
+ ed["power_mw"] = 2002
+ if cp := resp.get("get_current_power"):
+ cp["current_power"] = 2.002
+ resp["get_energy_usage"] = SmartErrorCode.JSON_DECODE_FAIL_ERROR
+
+ # version 1 only has get_energy_usage so module should raise an error if
+ # version 1 and get_energy_usage is in error
+ with patch.object(dev.protocol, "query", return_value=resp):
+ await dev.update()
+
+ with expected_raise:
+ assert "get_energy_usage" not in energy_module.data
+
+ assert energy_module.current_consumption == expected_current_consumption
+ assert energy_module.consumption_today is None
+ assert energy_module.consumption_this_month is None
+
+ msg = (
+ f"Removed key get_energy_usage from response for device {dev.host}"
+ " as it returned error: JSON_DECODE_FAIL_ERROR"
+ )
+ if version > 1:
+ assert msg in caplog.text
+
+ # Now test with no get_emeter_data
+ # This may not be valid scenario but we have a fallback to get_current_power
+ # just in case that should be tested.
+ caplog.clear()
+ resp = copy.deepcopy(last_update)
+
+ if cp := resp.get("get_current_power"):
+ cp["current_power"] = 2.002
+ resp["get_energy_usage"] = SmartErrorCode.JSON_DECODE_FAIL_ERROR
+
+ # Remove get_emeter_data from the response and from the device which will
+ # remember it otherwise.
+ resp.pop("get_emeter_data", None)
+ dev._last_update.pop("get_emeter_data", None)
+
+ with patch.object(dev.protocol, "query", return_value=resp):
+ await dev.update()
+
+ with expected_raise:
+ assert "get_energy_usage" not in energy_module.data
+
+ assert energy_module.current_consumption == expected_current_consumption
+
+ # message should only be logged once
+ assert msg not in caplog.text
diff --git a/tests/smart/modules/test_homekit.py b/tests/smart/modules/test_homekit.py
new file mode 100644
index 00000000..81992398
--- /dev/null
+++ b/tests/smart/modules/test_homekit.py
@@ -0,0 +1,16 @@
+from kasa import Module
+from kasa.smart import SmartDevice
+
+from ...device_fixtures import parametrize
+
+homekit = parametrize(
+ "has homekit", component_filter="homekit", protocol_filter={"SMART"}
+)
+
+
+@homekit
+async def test_info(dev: SmartDevice):
+ """Test homekit info."""
+ homekit = dev.modules.get(Module.HomeKit)
+ assert homekit
+ assert homekit.info
diff --git a/tests/smart/modules/test_matter.py b/tests/smart/modules/test_matter.py
new file mode 100644
index 00000000..d3ff8073
--- /dev/null
+++ b/tests/smart/modules/test_matter.py
@@ -0,0 +1,20 @@
+from kasa import Module
+from kasa.smart import SmartDevice
+
+from ...device_fixtures import parametrize
+
+matter = parametrize(
+ "has matter", component_filter="matter", protocol_filter={"SMART", "SMARTCAM"}
+)
+
+
+@matter
+async def test_info(dev: SmartDevice):
+ """Test matter info."""
+ matter = dev.modules.get(Module.Matter)
+ assert matter
+ assert matter.info
+ setup_code = dev.features.get("matter_setup_code")
+ assert setup_code
+ setup_payload = dev.features.get("matter_setup_payload")
+ assert setup_payload
diff --git a/tests/smart/modules/test_mop.py b/tests/smart/modules/test_mop.py
new file mode 100644
index 00000000..0c638ca3
--- /dev/null
+++ b/tests/smart/modules/test_mop.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+import pytest
+from pytest_mock import MockerFixture
+
+from kasa import Module
+from kasa.smart import SmartDevice
+from kasa.smart.modules.mop import Waterlevel
+
+from ...device_fixtures import get_parent_and_child_modules, parametrize
+
+mop = parametrize("has mop", component_filter="mop", protocol_filter={"SMART"})
+
+
+@mop
+@pytest.mark.parametrize(
+ ("feature", "prop_name", "type"),
+ [
+ ("mop_attached", "mop_attached", bool),
+ ("mop_waterlevel", "waterlevel", str),
+ ],
+)
+async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
+ """Test that features are registered and work as expected."""
+ mod = next(get_parent_and_child_modules(dev, Module.Mop))
+ assert mod is not None
+
+ prop = getattr(mod, prop_name)
+ assert isinstance(prop, type)
+
+ feat = mod._device.features[feature]
+ assert feat.value == prop
+ assert isinstance(feat.value, type)
+
+
+@mop
+async def test_mop_waterlevel(dev: SmartDevice, mocker: MockerFixture):
+ """Test dust mode."""
+ mop_module = next(get_parent_and_child_modules(dev, Module.Mop))
+ call = mocker.spy(mop_module, "call")
+
+ waterlevel = mop_module._device.features["mop_waterlevel"]
+ assert mop_module.waterlevel == waterlevel.value
+
+ new_level = Waterlevel.High
+ await mop_module.set_waterlevel(new_level.name)
+
+ params = mop_module._settings.copy()
+ params["cistern"] = new_level.value
+
+ call.assert_called_with("setCleanAttr", params)
+
+ await dev.update()
+
+ assert mop_module.waterlevel == new_level.name
+
+ with pytest.raises(ValueError, match="Invalid waterlevel"):
+ await mop_module.set_waterlevel("invalid")
diff --git a/tests/smart/modules/test_speaker.py b/tests/smart/modules/test_speaker.py
new file mode 100644
index 00000000..e11741da
--- /dev/null
+++ b/tests/smart/modules/test_speaker.py
@@ -0,0 +1,71 @@
+from __future__ import annotations
+
+import pytest
+from pytest_mock import MockerFixture
+
+from kasa import Module
+from kasa.smart import SmartDevice
+
+from ...device_fixtures import get_parent_and_child_modules, parametrize
+
+speaker = parametrize(
+ "has speaker", component_filter="speaker", protocol_filter={"SMART"}
+)
+
+
+@speaker
+@pytest.mark.parametrize(
+ ("feature", "prop_name", "type"),
+ [
+ ("volume", "volume", int),
+ ],
+)
+async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
+ """Test that features are registered and work as expected."""
+ speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
+ assert speaker is not None
+
+ prop = getattr(speaker, prop_name)
+ assert isinstance(prop, type)
+
+ feat = speaker._device.features[feature]
+ assert feat.value == prop
+ assert isinstance(feat.value, type)
+
+
+@speaker
+async def test_set_volume(dev: SmartDevice, mocker: MockerFixture):
+ """Test speaker settings."""
+ speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
+ assert speaker is not None
+
+ call = mocker.spy(speaker, "call")
+
+ volume = speaker._device.features["volume"]
+ assert speaker.volume == volume.value
+
+ new_volume = 15
+ await speaker.set_volume(new_volume)
+
+ call.assert_called_with("setVolume", {"volume": new_volume})
+
+ await dev.update()
+
+ assert speaker.volume == new_volume
+
+ with pytest.raises(ValueError, match="Volume must be between 0 and 100"):
+ await speaker.set_volume(-10)
+
+ with pytest.raises(ValueError, match="Volume must be between 0 and 100"):
+ await speaker.set_volume(110)
+
+
+@speaker
+async def test_locate(dev: SmartDevice, mocker: MockerFixture):
+ """Test the locate method."""
+ speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
+ call = mocker.spy(speaker, "call")
+
+ await speaker.locate()
+
+ call.assert_called_with("playSelectAudio", {"audio_type": "seek_me"})
diff --git a/tests/smart/test_smartdevice.py b/tests/smart/test_smartdevice.py
index c53193a3..bb6f1393 100644
--- a/tests/smart/test_smartdevice.py
+++ b/tests/smart/test_smartdevice.py
@@ -2,27 +2,43 @@
from __future__ import annotations
+import copy
import logging
import time
-from typing import Any, cast
+from collections import OrderedDict
+from typing import TYPE_CHECKING, Any, cast
from unittest.mock import patch
import pytest
from freezegun.api import FrozenDateTimeFactory
from pytest_mock import MockerFixture
-from kasa import Device, KasaException, Module
+from kasa import Device, DeviceType, KasaException, Module
from kasa.exceptions import DeviceError, SmartErrorCode
-from kasa.protocols.smartprotocol import _ChildProtocolWrapper
from kasa.smart import SmartDevice
from kasa.smart.modules.energy import Energy
from kasa.smart.smartmodule import SmartModule
+from kasa.smartcam import SmartCamDevice
from tests.conftest import (
+ DISCOVERY_MOCK_IP,
device_smart,
get_device_for_fixture_protocol,
get_parent_and_child_modules,
+ smart_discovery,
)
-from tests.device_fixtures import variable_temp_smart
+from tests.device_fixtures import (
+ hub_smartcam,
+ hubs_smart,
+ parametrize_combine,
+ variable_temp_smart,
+)
+
+from ..fakeprotocol_smart import FakeSmartTransport
+from ..fakeprotocol_smartcam import FakeSmartCamTransport
+
+DUMMY_CHILD_REQUEST_PREFIX = "get_dummy_"
+
+hub_all = parametrize_combine([hubs_smart, hub_smartcam])
@device_smart
@@ -51,13 +67,41 @@ async def test_update_no_device_info(dev: SmartDevice, mocker: MockerFixture):
await dev.update()
+@smart_discovery
+async def test_device_type_no_update(discovery_mock, caplog: pytest.LogCaptureFixture):
+ """Test device type and repr when device not updated."""
+ dev = SmartDevice(DISCOVERY_MOCK_IP)
+ assert dev.device_type is DeviceType.Unknown
+ assert repr(dev) == f""
+
+ discovery_result = copy.deepcopy(discovery_mock.discovery_data["result"])
+
+ disco_model = discovery_result["device_model"]
+ short_model, _, _ = disco_model.partition("(")
+ dev.update_from_discover_info(discovery_result)
+ assert dev.device_type is DeviceType.Unknown
+ assert (
+ repr(dev)
+ == f""
+ )
+ discovery_result["device_type"] = "SMART.FOOBAR"
+ dev.update_from_discover_info(discovery_result)
+ dev._components = {"dummy": 1}
+ assert dev.device_type is DeviceType.Plug
+ assert (
+ repr(dev)
+ == f""
+ )
+ assert "Unknown device type, falling back to plug" in caplog.text
+
+
@device_smart
async def test_initial_update(dev: SmartDevice, mocker: MockerFixture):
"""Test the initial update cycle."""
# As the fixture data is already initialized, we reset the state for testing
dev._components_raw = None
dev._components = {}
- dev._modules = {}
+ dev._modules = OrderedDict()
dev._features = {}
dev._children = {}
dev._last_update = {}
@@ -109,6 +153,7 @@ async def test_negotiate(dev: SmartDevice, mocker: MockerFixture):
"get_child_device_list": None,
}
)
+ await dev.update()
assert len(dev._children) == dev.internal_state["get_child_device_list"]["sum"]
@@ -183,6 +228,166 @@ async def test_update_module_update_delays(
), f"Expected update time {expected_update_time} after {seconds} seconds for {module.name} with delay {mod_delay} got {module._last_update_time}"
+async def _get_child_responses(child_requests: list[dict[str, Any]], child_protocol):
+ """Get dummy responses for testing all child modules.
+
+ Even if they don't return really return query.
+ """
+ child_req = {item["method"]: item.get("params") for item in child_requests}
+ child_resp = {k: v for k, v in child_req.items() if k.startswith("get_dummy")}
+ child_req = {
+ k: v for k, v in child_req.items() if k.startswith("get_dummy") is False
+ }
+ resp = await child_protocol._query(child_req)
+ resp = {**child_resp, **resp}
+ return [
+ {"method": k, "error_code": 0, "result": v or {"dummy": "dummy"}}
+ for k, v in resp.items()
+ ]
+
+
+@hub_all
+@pytest.mark.xdist_group(name="caplog")
+async def test_hub_children_update_delays(
+ dev: SmartDevice,
+ mocker: MockerFixture,
+ caplog: pytest.LogCaptureFixture,
+ freezer: FrozenDateTimeFactory,
+):
+ """Test that hub children use the correct delay."""
+ if not dev.children:
+ pytest.skip(f"Device {dev.model} does not have children.")
+ # We need to have some modules initialized by now
+ assert dev._modules
+
+ new_dev = type(dev)("127.0.0.1", protocol=dev.protocol)
+ module_queries: dict[str, dict[str, dict]] = {}
+
+ # children should always update on first update
+ await new_dev.update(update_children=False)
+
+ if TYPE_CHECKING:
+ from ..fakeprotocol_smart import FakeSmartTransport
+
+ assert isinstance(dev.protocol._transport, FakeSmartTransport)
+ if dev.protocol._transport.child_protocols:
+ for child in new_dev.children:
+ for modname, module in child._modules.items():
+ if (
+ not (q := module.query())
+ and modname not in {"DeviceModule", "Light", "Battery", "Camera"}
+ and not module.SYSINFO_LOOKUP_KEYS
+ ):
+ q = {f"get_dummy_{modname}": {}}
+ mocker.patch.object(module, "query", return_value=q)
+ if q:
+ queries = module_queries.setdefault(child.device_id, {})
+ queries[cast(str, modname)] = q
+ module._last_update_time = None
+
+ module_queries[""] = {
+ cast(str, modname): q
+ for modname, module in dev._modules.items()
+ if (q := module.query())
+ }
+
+ async def _query(request, *args, **kwargs):
+ # If this is a child multipleRequest query return the error wrapped
+ child_id = None
+ # smart hub
+ if (
+ (cc := request.get("control_child"))
+ and (child_id := cc.get("device_id"))
+ and (requestData := cc["requestData"])
+ and requestData["method"] == "multipleRequest"
+ and (child_requests := requestData["params"]["requests"])
+ ):
+ child_protocol = dev.protocol._transport.child_protocols[child_id]
+ resp = await _get_child_responses(child_requests, child_protocol)
+ return {"control_child": {"responseData": {"result": {"responses": resp}}}}
+ # smartcam hub
+ if (
+ (mr := request.get("multipleRequest"))
+ and (requests := mr.get("requests"))
+ # assumes all requests for the same child
+ and (
+ child_id := next(iter(requests))
+ .get("params", {})
+ .get("childControl", {})
+ .get("device_id")
+ )
+ and (
+ child_requests := [
+ cc["request_data"]
+ for req in requests
+ if (cc := req["params"].get("childControl"))
+ ]
+ )
+ ):
+ child_protocol = dev.protocol._transport.child_protocols[child_id]
+ resp = await _get_child_responses(child_requests, child_protocol)
+ resp = [{"result": {"response_data": resp}} for resp in resp]
+ return {"multipleRequest": {"responses": resp}}
+
+ if child_id: # child single query
+ child_protocol = dev.protocol._transport.child_protocols[child_id]
+ resp_list = await _get_child_responses([requestData], child_protocol)
+ resp = {"control_child": {"responseData": resp_list[0]}}
+ else:
+ resp = await dev.protocol._query(request, *args, **kwargs)
+
+ return resp
+
+ mocker.patch.object(new_dev.protocol, "query", side_effect=_query)
+
+ first_update_time = time.monotonic()
+ assert new_dev._last_update_time == first_update_time
+
+ await new_dev.update()
+
+ for dev_id, modqueries in module_queries.items():
+ check_dev = new_dev._children[dev_id] if dev_id else new_dev
+ for modname in modqueries:
+ mod = cast(SmartModule, check_dev.modules[modname])
+ assert mod._last_update_time == first_update_time
+
+ for mod in new_dev.modules.values():
+ mod.MINIMUM_UPDATE_INTERVAL_SECS = 5
+ freezer.tick(180)
+
+ now = time.monotonic()
+ await new_dev.update()
+
+ child_tick = max(
+ module.MINIMUM_HUB_CHILD_UPDATE_INTERVAL_SECS
+ for child in new_dev.children
+ for module in child.modules.values()
+ )
+
+ for dev_id, modqueries in module_queries.items():
+ check_dev = new_dev._children[dev_id] if dev_id else new_dev
+ for modname in modqueries:
+ if modname in {"Firmware"}:
+ continue
+ mod = cast(SmartModule, check_dev.modules[modname])
+ expected_update_time = first_update_time if dev_id else now
+ assert mod._last_update_time == expected_update_time
+
+ freezer.tick(child_tick)
+
+ now = time.monotonic()
+ await new_dev.update()
+
+ for dev_id, modqueries in module_queries.items():
+ check_dev = new_dev._children[dev_id] if dev_id else new_dev
+ for modname in modqueries:
+ if modname in {"Firmware"}:
+ continue
+ mod = cast(SmartModule, check_dev.modules[modname])
+
+ assert mod._last_update_time == now
+
+
@pytest.mark.parametrize(
("first_update"),
[
@@ -230,25 +435,82 @@ async def test_update_module_query_errors(
new_dev = SmartDevice("127.0.0.1", protocol=dev.protocol)
if not first_update:
await new_dev.update()
- freezer.tick(
- max(module.MINIMUM_UPDATE_INTERVAL_SECS for module in dev._modules.values())
- )
+ freezer.tick(max(module.update_interval for module in dev._modules.values()))
- module_queries = {
- modname: q
+ module_queries: dict[str, dict[str, dict]] = {}
+ if TYPE_CHECKING:
+ from ..fakeprotocol_smart import FakeSmartTransport
+
+ assert isinstance(dev.protocol._transport, FakeSmartTransport)
+ if dev.protocol._transport.child_protocols:
+ for child in new_dev.children:
+ for modname, module in child._modules.items():
+ if (
+ not (q := module.query())
+ and modname not in {"DeviceModule", "Light"}
+ and not module.SYSINFO_LOOKUP_KEYS
+ ):
+ q = {f"get_dummy_{modname}": {}}
+ mocker.patch.object(module, "query", return_value=q)
+ if q:
+ queries = module_queries.setdefault(child.device_id, {})
+ queries[cast(str, modname)] = q
+
+ module_queries[""] = {
+ cast(str, modname): q
for modname, module in dev._modules.items()
if (q := module.query()) and modname not in critical_modules
}
+ raise_error = True
+
async def _query(request, *args, **kwargs):
+ pass
+ # If this is a childmultipleRequest query return the error wrapped
+ child_id = None
if (
- "component_nego" in request
- or "get_child_device_component_list" in request
- or "control_child" in request
+ (cc := request.get("control_child"))
+ and (child_id := cc.get("device_id"))
+ and (requestData := cc["requestData"])
+ and requestData["method"] == "multipleRequest"
+ and (child_requests := requestData["params"]["requests"])
):
- resp = await dev.protocol._query(request, *args, **kwargs)
- resp["get_connect_cloud_state"] = SmartErrorCode.CLOUD_FAILED_ERROR
+ if raise_error:
+ if not isinstance(error_type, SmartErrorCode):
+ raise TimeoutError()
+ if len(child_requests) > 1:
+ raise TimeoutError()
+
+ if raise_error:
+ resp = {
+ "method": child_requests[0]["method"],
+ "error_code": error_type.value,
+ }
+ else:
+ child_protocol = dev.protocol._transport.child_protocols[child_id]
+ resp = await _get_child_responses(child_requests, child_protocol)
+ return {"control_child": {"responseData": {"result": {"responses": resp}}}}
+
+ if (
+ not raise_error
+ or "component_nego" in request
+ # allow the initial child device query
+ or (
+ "get_child_device_component_list" in request
+ and "get_child_device_list" in request
+ and len(request) == 2
+ )
+ ):
+ if child_id: # child single query
+ child_protocol = dev.protocol._transport.child_protocols[child_id]
+ resp_list = await _get_child_responses([requestData], child_protocol)
+ resp = {"control_child": {"responseData": resp_list[0]}}
+ else:
+ resp = await dev.protocol._query(request, *args, **kwargs)
+ if raise_error:
+ resp["get_connect_cloud_state"] = SmartErrorCode.CLOUD_FAILED_ERROR
return resp
+
# Don't test for errors on get_device_info as that is likely terminal
if len(request) == 1 and "get_device_info" in request:
return await dev.protocol._query(request, *args, **kwargs)
@@ -259,80 +521,77 @@ async def test_update_module_query_errors(
raise TimeoutError("Dummy timeout")
raise error_type
- child_protocols = {
- cast(_ChildProtocolWrapper, child.protocol)._device_id: child.protocol
- for child in dev.children
- }
-
- async def _child_query(self, request, *args, **kwargs):
- return await child_protocols[self._device_id]._query(request, *args, **kwargs)
-
mocker.patch.object(new_dev.protocol, "query", side_effect=_query)
- # children not created yet so cannot patch.object
- mocker.patch(
- "kasa.protocols.smartprotocol._ChildProtocolWrapper.query", new=_child_query
- )
await new_dev.update()
msg = f"Error querying {new_dev.host} for modules"
assert msg in caplog.text
- for modname in module_queries:
- mod = cast(SmartModule, new_dev.modules[modname])
- assert mod.disabled is False, f"{modname} disabled"
- assert mod.update_interval == mod.UPDATE_INTERVAL_AFTER_ERROR_SECS
- for mod_query in module_queries[modname]:
- if not first_update or mod_query not in first_update_queries:
- msg = f"Error querying {new_dev.host} individually for module query '{mod_query}"
- assert msg in caplog.text
+ for dev_id, modqueries in module_queries.items():
+ check_dev = new_dev._children[dev_id] if dev_id else new_dev
+ for modname in modqueries:
+ mod = cast(SmartModule, check_dev.modules[modname])
+ if modname in {"DeviceModule"} or (
+ hasattr(mod, "_state_in_sysinfo") and mod._state_in_sysinfo is True
+ ):
+ continue
+ assert mod.disabled is False, f"{modname} disabled"
+ assert mod.update_interval == mod.UPDATE_INTERVAL_AFTER_ERROR_SECS
+ for mod_query in modqueries[modname]:
+ if not first_update or mod_query not in first_update_queries:
+ msg = f"Error querying {new_dev.host} individually for module query '{mod_query}"
+ assert msg in caplog.text
# Query again should not run for the modules
caplog.clear()
await new_dev.update()
- for modname in module_queries:
- mod = cast(SmartModule, new_dev.modules[modname])
- assert mod.disabled is False, f"{modname} disabled"
+ for dev_id, modqueries in module_queries.items():
+ check_dev = new_dev._children[dev_id] if dev_id else new_dev
+ for modname in modqueries:
+ mod = cast(SmartModule, check_dev.modules[modname])
+ assert mod.disabled is False, f"{modname} disabled"
freezer.tick(SmartModule.UPDATE_INTERVAL_AFTER_ERROR_SECS)
caplog.clear()
if recover:
- mocker.patch.object(
- new_dev.protocol, "query", side_effect=new_dev.protocol._query
- )
- mocker.patch(
- "kasa.protocols.smartprotocol._ChildProtocolWrapper.query",
- new=_ChildProtocolWrapper._query,
- )
+ raise_error = False
await new_dev.update()
msg = f"Error querying {new_dev.host} for modules"
if not recover:
assert msg in caplog.text
- for modname in module_queries:
- mod = cast(SmartModule, new_dev.modules[modname])
- if not recover:
- assert mod.disabled is True, f"{modname} not disabled"
- assert mod._error_count == 2
- assert mod._last_update_error
- for mod_query in module_queries[modname]:
- if not first_update or mod_query not in first_update_queries:
- msg = f"Error querying {new_dev.host} individually for module query '{mod_query}"
- assert msg in caplog.text
- # Test one of the raise_if_update_error
- if mod.name == "Energy":
- emod = cast(Energy, mod)
- with pytest.raises(KasaException, match="Module update error"):
- assert emod.current_consumption is not None
- else:
- assert mod.disabled is False
- assert mod._error_count == 0
- assert mod._last_update_error is None
- # Test one of the raise_if_update_error doesn't raise
- if mod.name == "Energy":
- emod = cast(Energy, mod)
- assert emod.current_consumption is not None
+
+ for dev_id, modqueries in module_queries.items():
+ check_dev = new_dev._children[dev_id] if dev_id else new_dev
+ for modname in modqueries:
+ mod = cast(SmartModule, check_dev.modules[modname])
+ if modname in {"DeviceModule"} or (
+ hasattr(mod, "_state_in_sysinfo") and mod._state_in_sysinfo is True
+ ):
+ continue
+ if not recover:
+ assert mod.disabled is True, f"{modname} not disabled"
+ assert mod._error_count == 2
+ assert mod._last_update_error
+ for mod_query in modqueries[modname]:
+ if not first_update or mod_query not in first_update_queries:
+ msg = f"Error querying {new_dev.host} individually for module query '{mod_query}"
+ assert msg in caplog.text
+ # Test one of the raise_if_update_error
+ if mod.name == "Energy":
+ emod = cast(Energy, mod)
+ with pytest.raises(KasaException, match="Module update error"):
+ assert emod.status is not None
+ else:
+ assert mod.disabled is False
+ assert mod._error_count == 0
+ assert mod._last_update_error is None
+ # Test one of the raise_if_update_error doesn't raise
+ if mod.name == "Energy":
+ emod = cast(Energy, mod)
+ assert emod.status is not None
async def test_get_modules():
@@ -441,4 +700,313 @@ async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixt
async def test_smart_temp_range(dev: Device):
light = dev.modules.get(Module.Light)
assert light
- assert light.valid_temperature_range
+ color_temp_feat = light.get_feature("color_temp")
+ assert color_temp_feat
+ assert color_temp_feat.range
+
+
+@device_smart
+async def test_initialize_modules_sysinfo_lookup_keys(
+ dev: SmartDevice, mocker: MockerFixture
+):
+ """Test that matching modules using SYSINFO_LOOKUP_KEYS are initialized correctly."""
+
+ class AvailableKey(SmartModule):
+ SYSINFO_LOOKUP_KEYS = ["device_id"]
+
+ class NonExistingKey(SmartModule):
+ SYSINFO_LOOKUP_KEYS = ["this_does_not_exist"]
+
+ # The __init_subclass__ hook in smartmodule checks the path,
+ # so we have to manually add these for testing.
+ mocker.patch.dict(
+ "kasa.smart.smartmodule.SmartModule.REGISTERED_MODULES",
+ {
+ AvailableKey._module_name(): AvailableKey,
+ NonExistingKey._module_name(): NonExistingKey,
+ },
+ )
+
+ # We have an already initialized device, so we try to initialize the modules again
+ await dev._initialize_modules()
+
+ assert "AvailableKey" in dev.modules
+ assert "NonExistingKey" not in dev.modules
+
+
+@device_smart
+async def test_initialize_modules_required_component(
+ dev: SmartDevice, mocker: MockerFixture
+):
+ """Test that matching modules using REQUIRED_COMPONENT are initialized correctly."""
+
+ class AvailableComponent(SmartModule):
+ REQUIRED_COMPONENT = "device"
+
+ class NonExistingComponent(SmartModule):
+ REQUIRED_COMPONENT = "this_does_not_exist"
+
+ # The __init_subclass__ hook in smartmodule checks the path,
+ # so we have to manually add these for testing.
+ mocker.patch.dict(
+ "kasa.smart.smartmodule.SmartModule.REGISTERED_MODULES",
+ {
+ AvailableComponent._module_name(): AvailableComponent,
+ NonExistingComponent._module_name(): NonExistingComponent,
+ },
+ )
+
+ # We have an already initialized device, so we try to initialize the modules again
+ await dev._initialize_modules()
+
+ assert "AvailableComponent" in dev.modules
+ assert "NonExistingComponent" not in dev.modules
+
+
+async def test_smartmodule_query():
+ """Test that a module that doesn't set QUERY_GETTER_NAME has empty query."""
+
+ class DummyModule(SmartModule):
+ pass
+
+ dummy_device = await get_device_for_fixture_protocol(
+ "KS240(US)_1.0_1.0.5.json", "SMART"
+ )
+ mod = DummyModule(dummy_device, "dummy")
+ assert mod.query() == {}
+
+
+@hub_all
+@pytest.mark.xdist_group(name="caplog")
+@pytest.mark.requires_dummy
+async def test_dynamic_devices(dev: Device, caplog: pytest.LogCaptureFixture):
+ """Test dynamic child devices."""
+ if not dev.children:
+ pytest.skip(f"Device {dev.model} does not have children.")
+
+ transport = dev.protocol._transport
+ assert isinstance(transport, FakeSmartCamTransport | FakeSmartTransport)
+
+ lu = dev._last_update
+ assert lu
+ child_device_info = lu.get("getChildDeviceList", lu.get("get_child_device_list"))
+ assert child_device_info
+
+ child_device_components = lu.get(
+ "getChildDeviceComponentList", lu.get("get_child_device_component_list")
+ )
+ assert child_device_components
+
+ mock_child_device_info = copy.deepcopy(child_device_info)
+ mock_child_device_components = copy.deepcopy(child_device_components)
+
+ first_child = child_device_info["child_device_list"][0]
+ first_child_device_id = first_child["device_id"]
+
+ first_child_components = next(
+ iter(
+ [
+ cc
+ for cc in child_device_components["child_component_list"]
+ if cc["device_id"] == first_child_device_id
+ ]
+ )
+ )
+
+ first_child_fake_transport = transport.child_protocols[first_child_device_id]
+
+ # Test adding devices
+ start_child_count = len(dev.children)
+ added_ids = []
+ for i in range(1, 3):
+ new_child = copy.deepcopy(first_child)
+ new_child_components = copy.deepcopy(first_child_components)
+
+ mock_device_id = f"mock_child_device_id_{i}"
+
+ transport.child_protocols[mock_device_id] = first_child_fake_transport
+ new_child["device_id"] = mock_device_id
+ new_child_components["device_id"] = mock_device_id
+
+ added_ids.append(mock_device_id)
+ mock_child_device_info["child_device_list"].append(new_child)
+ mock_child_device_components["child_component_list"].append(
+ new_child_components
+ )
+
+ def mock_get_child_device_queries(method, params):
+ if method in {"getChildDeviceList", "get_child_device_list"}:
+ result = mock_child_device_info
+ if method in {"getChildDeviceComponentList", "get_child_device_component_list"}:
+ result = mock_child_device_components
+ return {"result": result, "error_code": 0}
+
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ for added_id in added_ids:
+ assert added_id in dev._children
+ expected_new_length = start_child_count + len(added_ids)
+ assert len(dev.children) == expected_new_length
+
+ # Test removing devices
+ mock_child_device_info["child_device_list"] = [
+ info
+ for info in mock_child_device_info["child_device_list"]
+ if info["device_id"] != first_child_device_id
+ ]
+ mock_child_device_components["child_component_list"] = [
+ cc
+ for cc in mock_child_device_components["child_component_list"]
+ if cc["device_id"] != first_child_device_id
+ ]
+
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ expected_new_length -= 1
+ assert len(dev.children) == expected_new_length
+
+ # Test no child devices
+
+ mock_child_device_info["child_device_list"] = []
+ mock_child_device_components["child_component_list"] = []
+ mock_child_device_info["sum"] = 0
+ mock_child_device_components["sum"] = 0
+
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert len(dev.children) == 0
+
+ # Logging tests are only for smartcam hubs as smart hubs do not test categories
+ if not isinstance(dev, SmartCamDevice):
+ return
+
+ # setup
+ mock_child = copy.deepcopy(first_child)
+ mock_components = copy.deepcopy(first_child_components)
+
+ mock_child_device_info["child_device_list"] = [mock_child]
+ mock_child_device_components["child_component_list"] = [mock_components]
+ mock_child_device_info["sum"] = 1
+ mock_child_device_components["sum"] = 1
+
+ # Test can't find matching components
+
+ mock_child["device_id"] = "no_comps_1"
+ mock_components["device_id"] = "no_comps_2"
+
+ caplog.set_level("DEBUG")
+ caplog.clear()
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert "Could not find child components for device" in caplog.text
+
+ caplog.clear()
+
+ # Test doesn't log multiple
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert "Could not find child components for device" not in caplog.text
+
+ # Test invalid category
+
+ mock_child["device_id"] = "invalid_cat"
+ mock_components["device_id"] = "invalid_cat"
+ mock_child["category"] = "foobar"
+
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert "Child device type not supported" in caplog.text
+
+ caplog.clear()
+
+ # Test doesn't log multiple
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert "Child device type not supported" not in caplog.text
+
+ # Test no category
+
+ mock_child["device_id"] = "no_cat"
+ mock_components["device_id"] = "no_cat"
+ mock_child.pop("category")
+
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert "Child device type not supported" in caplog.text
+
+ # Test only log once
+
+ caplog.clear()
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert "Child device type not supported" not in caplog.text
+
+ # Test no device_id
+
+ mock_child.pop("device_id")
+
+ caplog.clear()
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert "Could not find child id for device" in caplog.text
+
+ # Test only log once
+
+ caplog.clear()
+ with patch.object(
+ transport, "get_child_device_queries", side_effect=mock_get_child_device_queries
+ ):
+ await dev.update()
+
+ assert "Could not find child id for device" not in caplog.text
+
+
+@hubs_smart
+async def test_unpair(dev: SmartDevice, mocker: MockerFixture):
+ """Verify that unpair calls childsetup module."""
+ if not dev.children:
+ pytest.skip("device has no children")
+
+ child = dev.children[0]
+
+ assert child.parent is not None
+ assert Module.ChildSetup in dev.modules
+ cs = dev.modules[Module.ChildSetup]
+
+ unpair_call = mocker.spy(cs, "unpair")
+
+ unpair_feat = child.features.get("unpair")
+ assert unpair_feat
+ await unpair_feat.set_value(None)
+
+ unpair_call.assert_called_with(child.device_id)
diff --git a/tests/smartcam/modules/test_babycrydetection.py b/tests/smartcam/modules/test_babycrydetection.py
new file mode 100644
index 00000000..89ff5ac4
--- /dev/null
+++ b/tests/smartcam/modules/test_babycrydetection.py
@@ -0,0 +1,45 @@
+"""Tests for smartcam baby cry detection module."""
+
+from __future__ import annotations
+
+from kasa import Device
+from kasa.smartcam.smartcammodule import SmartCamModule
+
+from ...device_fixtures import parametrize
+
+babycrydetection = parametrize(
+ "has babycry detection",
+ component_filter="babyCryDetection",
+ protocol_filter={"SMARTCAM"},
+)
+
+
+@babycrydetection
+async def test_babycrydetection(dev: Device):
+ """Test device babycry detection."""
+ babycry = dev.modules.get(SmartCamModule.SmartCamBabyCryDetection)
+ assert babycry
+
+ bcde_feat = dev.features.get("baby_cry_detection")
+ assert bcde_feat
+
+ original_enabled = babycry.enabled
+
+ try:
+ await babycry.set_enabled(not original_enabled)
+ await dev.update()
+ assert babycry.enabled is not original_enabled
+ assert bcde_feat.value is not original_enabled
+
+ await babycry.set_enabled(original_enabled)
+ await dev.update()
+ assert babycry.enabled is original_enabled
+ assert bcde_feat.value is original_enabled
+
+ await bcde_feat.set_value(not original_enabled)
+ await dev.update()
+ assert babycry.enabled is not original_enabled
+ assert bcde_feat.value is not original_enabled
+
+ finally:
+ await babycry.set_enabled(original_enabled)
diff --git a/tests/smartcam/modules/test_battery.py b/tests/smartcam/modules/test_battery.py
new file mode 100644
index 00000000..12cab14b
--- /dev/null
+++ b/tests/smartcam/modules/test_battery.py
@@ -0,0 +1,33 @@
+"""Tests for smartcam battery module."""
+
+from __future__ import annotations
+
+from kasa import Device
+from kasa.smartcam.smartcammodule import SmartCamModule
+
+from ...device_fixtures import parametrize
+
+battery_smartcam = parametrize(
+ "has battery",
+ component_filter="battery",
+ protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"},
+)
+
+
+@battery_smartcam
+async def test_battery(dev: Device):
+ """Test device battery."""
+ 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:
+ feat = dev.features.get(feat_id)
+ assert feat
+ assert feat.value is not None
diff --git a/tests/smartcam/test_smartcamera.py b/tests/smartcam/modules/test_camera.py
similarity index 57%
rename from tests/smartcam/test_smartcamera.py
rename to tests/smartcam/modules/test_camera.py
index ccb4fbc1..d668f9f4 100644
--- a/tests/smartcam/test_smartcamera.py
+++ b/tests/smartcam/modules/test_camera.py
@@ -4,15 +4,19 @@ from __future__ import annotations
import base64
import json
-from datetime import UTC, datetime
from unittest.mock import patch
import pytest
-from freezegun.api import FrozenDateTimeFactory
-from kasa import Credentials, Device, DeviceType, Module
+from kasa import Credentials, Device, DeviceType, Module, StreamResolution
-from ..conftest import camera_smartcam, device_smartcam, hub_smartcam
+from ...conftest import device_smartcam, parametrize
+
+not_child_camera_smartcam = parametrize(
+ "not child camera smartcam",
+ device_type_filter=[DeviceType.Camera],
+ protocol_filter={"SMARTCAM"},
+)
@device_smartcam
@@ -26,7 +30,7 @@ async def test_state(dev: Device):
assert dev.is_on is not state
-@camera_smartcam
+@not_child_camera_smartcam
async def test_stream_rtsp_url(dev: Device):
camera_module = dev.modules.get(Module.Camera)
assert camera_module
@@ -37,6 +41,16 @@ async def test_stream_rtsp_url(dev: Device):
url = camera_module.stream_rtsp_url(Credentials("foo", "bar"))
assert url == "rtsp://foo:bar@127.0.0.123:554/stream1"
+ url = camera_module.stream_rtsp_url(
+ Credentials("foo", "bar"), stream_resolution=StreamResolution.HD
+ )
+ assert url == "rtsp://foo:bar@127.0.0.123:554/stream1"
+
+ url = camera_module.stream_rtsp_url(
+ Credentials("foo", "bar"), stream_resolution=StreamResolution.SD
+ )
+ assert url == "rtsp://foo:bar@127.0.0.123:554/stream2"
+
with patch.object(dev.config, "credentials", Credentials("bar", "foo")):
url = camera_module.stream_rtsp_url()
assert url == "rtsp://bar:foo@127.0.0.123:554/stream1"
@@ -75,49 +89,12 @@ async def test_stream_rtsp_url(dev: Device):
url = camera_module.stream_rtsp_url()
assert url is None
- # Test with camera off
- await camera_module.set_state(False)
- await dev.update()
- url = camera_module.stream_rtsp_url(Credentials("foo", "bar"))
- assert url is None
- with patch.object(dev.config, "credentials", Credentials("bar", "foo")):
- url = camera_module.stream_rtsp_url()
- assert url is None
+@not_child_camera_smartcam
+async def test_onvif_url(dev: Device):
+ """Test the onvif url."""
+ camera_module = dev.modules.get(Module.Camera)
+ assert camera_module
-@device_smartcam
-async def test_alias(dev):
- test_alias = "TEST1234"
- original = dev.alias
-
- assert isinstance(original, str)
- await dev.set_alias(test_alias)
- await dev.update()
- assert dev.alias == test_alias
-
- await dev.set_alias(original)
- await dev.update()
- assert dev.alias == original
-
-
-@hub_smartcam
-async def test_hub(dev):
- assert dev.children
- for child in dev.children:
- assert "Cloud" in child.modules
- assert child.modules["Cloud"].data
- assert child.alias
- await child.update()
- assert "Time" not in child.modules
- assert child.time
-
-
-@device_smartcam
-async def test_device_time(dev: Device, freezer: FrozenDateTimeFactory):
- """Test a child device gets the time from it's parent module."""
- fallback_time = datetime.now(UTC).astimezone().replace(microsecond=0)
- assert dev.time != fallback_time
- module = dev.modules[Module.Time]
- await module.set_time(fallback_time)
- await dev.update()
- assert dev.time == fallback_time
+ url = camera_module.onvif_url()
+ assert url == "http://127.0.0.123:2020/onvif/device_service"
diff --git a/tests/smartcam/modules/test_childsetup.py b/tests/smartcam/modules/test_childsetup.py
new file mode 100644
index 00000000..a419393d
--- /dev/null
+++ b/tests/smartcam/modules/test_childsetup.py
@@ -0,0 +1,103 @@
+from __future__ import annotations
+
+import logging
+
+import pytest
+from pytest_mock import MockerFixture
+
+from kasa import Feature, Module, SmartDevice
+
+from ...device_fixtures import parametrize
+
+childsetup = parametrize(
+ "supports pairing", component_filter="childQuickSetup", protocol_filter={"SMARTCAM"}
+)
+
+
+@childsetup
+async def test_childsetup_features(dev: SmartDevice):
+ """Test the exposed features."""
+ cs = dev.modules[Module.ChildSetup]
+
+ assert "pair" in cs._module_features
+ pair = cs._module_features["pair"]
+ assert pair.type == Feature.Type.Action
+
+
+@childsetup
+async def test_childsetup_pair(
+ dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
+):
+ """Test device pairing."""
+ caplog.set_level(logging.INFO)
+ mock_query_helper = mocker.spy(dev, "_query_helper")
+ mocker.patch("asyncio.sleep")
+
+ cs = dev.modules[Module.ChildSetup]
+
+ await cs.pair()
+
+ mock_query_helper.assert_has_awaits(
+ [
+ mocker.call(
+ "startScanChildDevice",
+ params={
+ "childControl": {
+ "category": [
+ "camera",
+ "subg.trv",
+ "subg.trigger",
+ "subg.plugswitch",
+ ]
+ }
+ },
+ ),
+ mocker.call(
+ "getScanChildDeviceList",
+ {
+ "childControl": {
+ "category": [
+ "camera",
+ "subg.trv",
+ "subg.trigger",
+ "subg.plugswitch",
+ ]
+ }
+ },
+ ),
+ mocker.call(
+ "addScanChildDeviceList",
+ {
+ "childControl": {
+ "child_device_list": [
+ {
+ "device_id": "0000000000000000000000000000000000000000",
+ "category": "subg.trigger.button",
+ "device_model": "S200B",
+ "name": "I01BU0tFRF9OQU1FIw====",
+ }
+ ]
+ }
+ },
+ ),
+ ]
+ )
+ assert "Discovery done" in caplog.text
+
+
+@childsetup
+async def test_childsetup_unpair(
+ dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
+):
+ """Test unpair."""
+ mock_query_helper = mocker.spy(dev, "_query_helper")
+ DUMMY_ID = "dummy_id"
+
+ cs = dev.modules[Module.ChildSetup]
+
+ await cs.unpair(DUMMY_ID)
+
+ mock_query_helper.assert_awaited_with(
+ "removeChildDeviceList",
+ params={"childControl": {"child_device_list": [{"device_id": DUMMY_ID}]}},
+ )
diff --git a/tests/smartcam/modules/test_motiondetection.py b/tests/smartcam/modules/test_motiondetection.py
new file mode 100644
index 00000000..c4ff9807
--- /dev/null
+++ b/tests/smartcam/modules/test_motiondetection.py
@@ -0,0 +1,43 @@
+"""Tests for smartcam motion detection module."""
+
+from __future__ import annotations
+
+from kasa import Device
+from kasa.smartcam.smartcammodule import SmartCamModule
+
+from ...device_fixtures import parametrize
+
+motiondetection = parametrize(
+ "has motion detection", component_filter="detection", protocol_filter={"SMARTCAM"}
+)
+
+
+@motiondetection
+async def test_motiondetection(dev: Device):
+ """Test device motion detection."""
+ motion = dev.modules.get(SmartCamModule.SmartCamMotionDetection)
+ assert motion
+
+ mde_feat = dev.features.get("motion_detection")
+ assert mde_feat
+
+ original_enabled = motion.enabled
+
+ try:
+ await motion.set_enabled(not original_enabled)
+ await dev.update()
+ assert motion.enabled is not original_enabled
+ assert mde_feat.value is not original_enabled
+
+ await motion.set_enabled(original_enabled)
+ await dev.update()
+ assert motion.enabled is original_enabled
+ assert mde_feat.value is original_enabled
+
+ await mde_feat.set_value(not original_enabled)
+ await dev.update()
+ assert motion.enabled is not original_enabled
+ assert mde_feat.value is not original_enabled
+
+ finally:
+ await motion.set_enabled(original_enabled)
diff --git a/tests/smartcam/modules/test_persondetection.py b/tests/smartcam/modules/test_persondetection.py
new file mode 100644
index 00000000..34137587
--- /dev/null
+++ b/tests/smartcam/modules/test_persondetection.py
@@ -0,0 +1,45 @@
+"""Tests for smartcam person detection module."""
+
+from __future__ import annotations
+
+from kasa import Device
+from kasa.smartcam.smartcammodule import SmartCamModule
+
+from ...device_fixtures import parametrize
+
+persondetection = parametrize(
+ "has person detection",
+ component_filter="personDetection",
+ protocol_filter={"SMARTCAM"},
+)
+
+
+@persondetection
+async def test_persondetection(dev: Device):
+ """Test device person detection."""
+ person = dev.modules.get(SmartCamModule.SmartCamPersonDetection)
+ assert person
+
+ pde_feat = dev.features.get("person_detection")
+ assert pde_feat
+
+ original_enabled = person.enabled
+
+ try:
+ await person.set_enabled(not original_enabled)
+ await dev.update()
+ assert person.enabled is not original_enabled
+ assert pde_feat.value is not original_enabled
+
+ await person.set_enabled(original_enabled)
+ await dev.update()
+ assert person.enabled is original_enabled
+ assert pde_feat.value is original_enabled
+
+ await pde_feat.set_value(not original_enabled)
+ await dev.update()
+ assert person.enabled is not original_enabled
+ assert pde_feat.value is not original_enabled
+
+ finally:
+ await person.set_enabled(original_enabled)
diff --git a/tests/smartcam/modules/test_petdetection.py b/tests/smartcam/modules/test_petdetection.py
new file mode 100644
index 00000000..6eff0c8a
--- /dev/null
+++ b/tests/smartcam/modules/test_petdetection.py
@@ -0,0 +1,45 @@
+"""Tests for smartcam pet detection module."""
+
+from __future__ import annotations
+
+from kasa import Device
+from kasa.smartcam.smartcammodule import SmartCamModule
+
+from ...device_fixtures import parametrize
+
+petdetection = parametrize(
+ "has pet detection",
+ component_filter="petDetection",
+ protocol_filter={"SMARTCAM"},
+)
+
+
+@petdetection
+async def test_petdetection(dev: Device):
+ """Test device pet detection."""
+ pet = dev.modules.get(SmartCamModule.SmartCamPetDetection)
+ assert pet
+
+ pde_feat = dev.features.get("pet_detection")
+ assert pde_feat
+
+ original_enabled = pet.enabled
+
+ try:
+ await pet.set_enabled(not original_enabled)
+ await dev.update()
+ assert pet.enabled is not original_enabled
+ assert pde_feat.value is not original_enabled
+
+ await pet.set_enabled(original_enabled)
+ await dev.update()
+ assert pet.enabled is original_enabled
+ assert pde_feat.value is original_enabled
+
+ await pde_feat.set_value(not original_enabled)
+ await dev.update()
+ assert pet.enabled is not original_enabled
+ assert pde_feat.value is not original_enabled
+
+ finally:
+ await pet.set_enabled(original_enabled)
diff --git a/tests/smartcam/modules/test_tamperdetection.py b/tests/smartcam/modules/test_tamperdetection.py
new file mode 100644
index 00000000..ab2f851d
--- /dev/null
+++ b/tests/smartcam/modules/test_tamperdetection.py
@@ -0,0 +1,45 @@
+"""Tests for smartcam tamper detection module."""
+
+from __future__ import annotations
+
+from kasa import Device
+from kasa.smartcam.smartcammodule import SmartCamModule
+
+from ...device_fixtures import parametrize
+
+tamperdetection = parametrize(
+ "has tamper detection",
+ component_filter="tamperDetection",
+ protocol_filter={"SMARTCAM"},
+)
+
+
+@tamperdetection
+async def test_tamperdetection(dev: Device):
+ """Test device tamper detection."""
+ tamper = dev.modules.get(SmartCamModule.SmartCamTamperDetection)
+ assert tamper
+
+ tde_feat = dev.features.get("tamper_detection")
+ assert tde_feat
+
+ original_enabled = tamper.enabled
+
+ try:
+ await tamper.set_enabled(not original_enabled)
+ await dev.update()
+ assert tamper.enabled is not original_enabled
+ assert tde_feat.value is not original_enabled
+
+ await tamper.set_enabled(original_enabled)
+ await dev.update()
+ assert tamper.enabled is original_enabled
+ assert tde_feat.value is original_enabled
+
+ await tde_feat.set_value(not original_enabled)
+ await dev.update()
+ assert tamper.enabled is not original_enabled
+ assert tde_feat.value is not original_enabled
+
+ finally:
+ await tamper.set_enabled(original_enabled)
diff --git a/tests/smartcam/test_smartcamdevice.py b/tests/smartcam/test_smartcamdevice.py
new file mode 100644
index 00000000..8675b693
--- /dev/null
+++ b/tests/smartcam/test_smartcamdevice.py
@@ -0,0 +1,71 @@
+"""Tests for smart camera devices."""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+
+import pytest
+from freezegun.api import FrozenDateTimeFactory
+
+from kasa import Device, DeviceType, Module
+
+from ..conftest import device_smartcam, hub_smartcam
+
+
+@device_smartcam
+async def test_state(dev: Device):
+ if dev.device_type is DeviceType.Hub:
+ pytest.skip("Hubs cannot be switched on and off")
+
+ if Module.LensMask in dev.modules:
+ state = dev.is_on
+ await dev.set_state(not state)
+ await dev.update()
+ assert dev.is_on is not state
+
+ dev.modules.pop(Module.LensMask) # type: ignore[attr-defined]
+
+ # Test with no lens mask module. Device is always on.
+ assert dev.is_on is True
+ res = await dev.set_state(False)
+ assert res == {}
+ await dev.update()
+ assert dev.is_on is True
+
+
+@device_smartcam
+async def test_alias(dev):
+ test_alias = "TEST1234"
+ original = dev.alias
+
+ assert isinstance(original, str)
+ await dev.set_alias(test_alias)
+ await dev.update()
+ assert dev.alias == test_alias
+
+ await dev.set_alias(original)
+ await dev.update()
+ assert dev.alias == original
+
+
+@hub_smartcam
+async def test_hub(dev):
+ assert dev.children
+ for child in dev.children:
+ assert child.modules
+ assert child.device_info
+
+ assert child.alias
+ await child.update()
+ assert child.device_id
+
+
+@device_smartcam
+async def test_device_time(dev: Device, freezer: FrozenDateTimeFactory):
+ """Test a child device gets the time from it's parent module."""
+ fallback_time = datetime.now(UTC).astimezone().replace(microsecond=0)
+ assert dev.time != fallback_time
+ module = dev.modules[Module.Time]
+ await module.set_time(fallback_time)
+ await dev.update()
+ assert dev.time == fallback_time
diff --git a/tests/test_bulb.py b/tests/test_bulb.py
index 6956c4e8..14a2ca35 100644
--- a/tests/test_bulb.py
+++ b/tests/test_bulb.py
@@ -1,5 +1,9 @@
from __future__ import annotations
+import re
+from collections.abc import Callable
+from contextlib import nullcontext
+
import pytest
from kasa import Device, DeviceType, KasaException, Module
@@ -25,7 +29,7 @@ async def test_hsv(dev: Device, turn_on):
light = dev.modules.get(Module.Light)
assert light
await handle_turn_on(dev, turn_on)
- assert light.is_color
+ assert light.has_feature("hsv")
hue, saturation, brightness = light.hsv
assert 0 <= hue <= 360
@@ -106,7 +110,7 @@ async def test_invalid_hsv(
light = dev.modules.get(Module.Light)
assert light
await handle_turn_on(dev, turn_on)
- assert light.is_color
+ assert light.has_feature("hsv")
with pytest.raises(exception_cls, match=error):
await light.set_hsv(hue, sat, brightness)
@@ -124,7 +128,7 @@ async def test_color_state_information(dev: Device):
async def test_hsv_on_non_color(dev: Device):
light = dev.modules.get(Module.Light)
assert light
- assert not light.is_color
+ assert not light.has_feature("hsv")
with pytest.raises(KasaException):
await light.set_hsv(0, 0, 0)
@@ -173,9 +177,6 @@ async def test_non_variable_temp(dev: Device):
with pytest.raises(KasaException):
await light.set_color_temp(2700)
- with pytest.raises(KasaException):
- print(light.valid_temperature_range)
-
with pytest.raises(KasaException):
print(light.color_temp)
@@ -183,3 +184,67 @@ async def test_non_variable_temp(dev: Device):
@bulb
def test_device_type_bulb(dev: Device):
assert dev.device_type in {DeviceType.Bulb, DeviceType.LightStrip}
+
+
+@pytest.mark.parametrize(
+ ("attribute", "use_msg", "use_fn"),
+ [
+ pytest.param(
+ "is_color",
+ 'use has_feature("hsv") instead',
+ lambda device, mod: mod.has_feature("hsv"),
+ id="is_color",
+ ),
+ pytest.param(
+ "is_dimmable",
+ 'use has_feature("brightness") instead',
+ lambda device, mod: mod.has_feature("brightness"),
+ id="is_dimmable",
+ ),
+ pytest.param(
+ "is_variable_color_temp",
+ 'use has_feature("color_temp") instead',
+ lambda device, mod: mod.has_feature("color_temp"),
+ id="is_variable_color_temp",
+ ),
+ pytest.param(
+ "has_effects",
+ "check `Module.LightEffect in device.modules` instead",
+ lambda device, mod: Module.LightEffect in device.modules,
+ id="has_effects",
+ ),
+ ],
+)
+@bulb
+async def test_deprecated_light_is_has_attributes(
+ dev: Device, attribute: str, use_msg: str, use_fn: Callable[[Device, Module], bool]
+):
+ light = dev.modules.get(Module.Light)
+ assert light
+
+ msg = f"{attribute} is deprecated, {use_msg}"
+ with pytest.deprecated_call(match=(re.escape(msg))):
+ result = getattr(light, attribute)
+
+ assert result == use_fn(dev, light)
+
+
+@bulb
+async def test_deprecated_light_valid_temperature_range(dev: Device):
+ light = dev.modules.get(Module.Light)
+ assert light
+
+ color_temp = light.has_feature("color_temp")
+ dep_msg = (
+ "valid_temperature_range is deprecated, use "
+ 'get_feature("color_temp") minimum_value '
+ " and maximum_value instead"
+ )
+ exc_context = pytest.raises(KasaException, match="Color temperature not supported")
+ expected_context = nullcontext() if color_temp else exc_context
+
+ with (
+ expected_context,
+ pytest.deprecated_call(match=(re.escape(dep_msg))),
+ ):
+ assert light.valid_temperature_range # type: ignore[attr-defined]
diff --git a/tests/test_childdevice.py b/tests/test_childdevice.py
index 1e525efb..8bcc05db 100644
--- a/tests/test_childdevice.py
+++ b/tests/test_childdevice.py
@@ -145,7 +145,9 @@ async def test_child_device_type_unknown(caplog):
super().__init__(
SmartDevice("127.0.0.1"),
{"device_id": "1", "category": "foobar"},
- {"device", 1},
+ {
+ "component_list": [{"id": "device", "ver_code": 1}],
+ },
)
assert DummyDevice().device_type is DeviceType.Unknown
diff --git a/tests/test_cli.py b/tests/test_cli.py
index d1fc330c..269bc7aa 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,5 +1,4 @@
import json
-import os
import re
from datetime import datetime
from unittest.mock import ANY, PropertyMock, patch
@@ -12,6 +11,7 @@ from pytest_mock import MockerFixture
from kasa import (
AuthenticationError,
+ ColorTempRange,
Credentials,
Device,
DeviceError,
@@ -42,8 +42,9 @@ from kasa.cli.main import TYPES, _legacy_type_to_class, cli, cmd_command, raw_co
from kasa.cli.time import time
from kasa.cli.usage import energy
from kasa.cli.wifi import wifi
-from kasa.discover import Discover, DiscoveryResult
+from kasa.discover import Discover, DiscoveryResult, redact_data
from kasa.iot import IotDevice
+from kasa.json import dumps as json_dumps
from kasa.smart import SmartDevice
from kasa.smartcam import SmartCamDevice
@@ -60,15 +61,6 @@ from .conftest import (
pytestmark = [pytest.mark.requires_dummy]
-@pytest.fixture
-def runner():
- """Runner fixture that unsets the KASA_ environment variables for tests."""
- KASA_VARS = {k: None for k, v in os.environ.items() if k.startswith("KASA_")}
- runner = CliRunner(env=KASA_VARS)
-
- return runner
-
-
async def test_help(runner):
"""Test that all the lazy modules are correctly names."""
res = await runner.invoke(cli, ["--help"])
@@ -120,20 +112,69 @@ async def test_list_devices(discovery_mock, runner):
catch_exceptions=False,
)
assert res.exit_code == 0
- header = f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}"
- row = f"{discovery_mock.ip:<15} {discovery_mock.device_type:<20} {discovery_mock.encrypt_type:<7}"
+ header = (
+ f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
+ f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
+ )
+ row = (
+ f"{discovery_mock.ip:<15} {discovery_mock.model:<9} {discovery_mock.device_type:<20} "
+ f"{discovery_mock.encrypt_type:<7} {discovery_mock.https:<5} "
+ f"{discovery_mock.login_version or '-':<3}"
+ )
assert header in res.output
assert row in res.output
+async def test_discover_raw(discovery_mock, runner, mocker):
+ """Test the discover raw command."""
+ redact_spy = mocker.patch(
+ "kasa.protocols.protocol.redact_data", side_effect=redact_data
+ )
+ res = await runner.invoke(
+ cli,
+ ["--username", "foo", "--password", "bar", "discover", "raw"],
+ catch_exceptions=False,
+ )
+ assert res.exit_code == 0
+
+ expected = {
+ "discovery_response": discovery_mock.discovery_data,
+ "meta": {"ip": "127.0.0.123", "port": discovery_mock.discovery_port},
+ }
+ assert res.output == json_dumps(expected, indent=True) + "\n"
+
+ redact_spy.assert_not_called()
+
+ res = await runner.invoke(
+ cli,
+ ["--username", "foo", "--password", "bar", "discover", "raw", "--redact"],
+ catch_exceptions=False,
+ )
+ assert res.exit_code == 0
+
+ redact_spy.assert_called()
+
+
+@pytest.mark.parametrize(
+ ("exception", "expected"),
+ [
+ pytest.param(
+ AuthenticationError("Failed to authenticate"),
+ "Authentication failed",
+ id="auth",
+ ),
+ pytest.param(TimeoutError(), "Timed out", id="timeout"),
+ pytest.param(Exception("Foobar"), "Error: Foobar", id="other-error"),
+ ],
+)
@new_discovery
-async def test_list_auth_failed(discovery_mock, mocker, runner):
+async def test_list_update_failed(discovery_mock, mocker, runner, exception, expected):
"""Test that device update is called on main."""
device_class = Discover._get_device_class(discovery_mock.discovery_data)
mocker.patch.object(
device_class,
"update",
- side_effect=AuthenticationError("Failed to authenticate"),
+ side_effect=exception,
)
res = await runner.invoke(
cli,
@@ -141,10 +182,17 @@ async def test_list_auth_failed(discovery_mock, mocker, runner):
catch_exceptions=False,
)
assert res.exit_code == 0
- header = f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}"
- row = f"{discovery_mock.ip:<15} {discovery_mock.device_type:<20} {discovery_mock.encrypt_type:<7} - Authentication failed"
- assert header in res.output
- assert row in res.output
+ header = (
+ f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
+ f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
+ )
+ row = (
+ f"{discovery_mock.ip:<15} {discovery_mock.model:<9} {discovery_mock.device_type:<20} "
+ f"{discovery_mock.encrypt_type:<7} {discovery_mock.https:<5} "
+ f"{discovery_mock.login_version or '-':<3} - {expected}"
+ )
+ assert header in res.output.replace("\n", "")
+ assert row in res.output.replace("\n", "")
async def test_list_unsupported(unsupported_device_info, runner):
@@ -155,7 +203,10 @@ async def test_list_unsupported(unsupported_device_info, runner):
catch_exceptions=False,
)
assert res.exit_code == 0
- header = f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}"
+ header = (
+ f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
+ f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
+ )
row = f"{'127.0.0.1':<15} UNSUPPORTED DEVICE"
assert header in res.output
assert row in res.output
@@ -207,7 +258,8 @@ async def test_alias(dev, runner):
res = await runner.invoke(alias, obj=dev)
assert f"Alias: {new_alias}" in res.output
- await dev.set_alias(old_alias)
+ # If alias is None set it back to empty string
+ await dev.set_alias(old_alias or "")
async def test_raw_command(dev, mocker, runner):
@@ -215,7 +267,11 @@ async def test_raw_command(dev, mocker, runner):
from kasa.smart import SmartDevice
if isinstance(dev, SmartCamDevice):
- params = ["na", "getDeviceInfo"]
+ params = [
+ "na",
+ "getDeviceInfo",
+ '{"device_info": {"name": ["basic_info", "info"]}}',
+ ]
elif isinstance(dev, SmartDevice):
params = ["na", "get_device_info"]
else:
@@ -492,7 +548,9 @@ async def test_emeter(dev: Device, mocker, runner):
async def test_brightness(dev: Device, runner):
res = await runner.invoke(brightness, obj=dev)
- if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
+ if not (light := dev.modules.get(Module.Light)) or not light.has_feature(
+ "brightness"
+ ):
assert "This device does not support brightness." in res.output
return
@@ -509,13 +567,16 @@ async def test_brightness(dev: Device, runner):
async def test_color_temperature(dev: Device, runner):
res = await runner.invoke(temperature, obj=dev)
- if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
+ if not (light := dev.modules.get(Module.Light)) or not (
+ color_temp_feat := light.get_feature("color_temp")
+ ):
assert "Device does not support color temperature" in res.output
return
res = await runner.invoke(temperature, obj=dev)
assert f"Color temperature: {light.color_temp}" in res.output
- valid_range = light.valid_temperature_range
+ valid_range = color_temp_feat.range
+ assert isinstance(valid_range, ColorTempRange)
assert f"(min: {valid_range.min}, max: {valid_range.max})" in res.output
val = int((valid_range.min + valid_range.max) / 2)
@@ -541,7 +602,7 @@ async def test_color_temperature(dev: Device, runner):
async def test_color_hsv(dev: Device, runner: CliRunner):
res = await runner.invoke(hsv, obj=dev)
- if not (light := dev.modules.get(Module.Light)) or not light.is_color:
+ if not (light := dev.modules.get(Module.Light)) or not light.has_feature("hsv"):
assert "Device does not support colors" in res.output
return
@@ -731,6 +792,7 @@ async def test_without_device_type(dev, mocker, runner):
timeout=5,
discovery_timeout=7,
on_unsupported=ANY,
+ on_discovered_raw=ANY,
)
@@ -1094,7 +1156,7 @@ async def test_feature_set_child(mocker, runner):
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
get_child_device = mocker.spy(dummy_device, "get_child_device")
- child_id = "000000000000000000000000000000000000000001"
+ child_id = "SCRUBBED_CHILD_DEVICE_ID_1"
res = await runner.invoke(
cli,
@@ -1250,11 +1312,11 @@ async def test_discover_config(dev: Device, mocker, runner):
expected = f"--device-family {cparam.device_family.value} --encrypt-type {cparam.encryption_type.value} {'--https' if cparam.https else '--no-https'}"
assert expected in res.output
assert re.search(
- r"Attempt to connect to 127\.0\.0\.1 with \w+ \+ \w+ \+ \w+ failed",
+ r"Attempt to connect to 127\.0\.0\.1 with \w+ \+ \w+ \+ \w+ \+ \w+ failed",
res.output.replace("\n", ""),
)
assert re.search(
- r"Attempt to connect to 127\.0\.0\.1 with \w+ \+ \w+ \+ \w+ succeeded",
+ r"Attempt to connect to 127\.0\.0\.1 with \w+ \+ \w+ \+ \w+ \+ \w+ succeeded",
res.output.replace("\n", ""),
)
diff --git a/tests/test_common_modules.py b/tests/test_common_modules.py
index 32863604..cba1ef87 100644
--- a/tests/test_common_modules.py
+++ b/tests/test_common_modules.py
@@ -198,7 +198,7 @@ async def test_light_color_temp(dev: Device):
light = next(get_parent_and_child_modules(dev, Module.Light))
assert light
- if not light.is_variable_color_temp:
+ if not light.has_feature("color_temp"):
pytest.skip(
"Some smart light strips have color_temperature"
" component but min and max are the same"
diff --git a/tests/test_device.py b/tests/test_device.py
index 1d780c32..2c001bc6 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -16,6 +16,7 @@ import kasa
from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module
from kasa.iot import (
IotBulb,
+ IotCamera,
IotDevice,
IotDimmer,
IotLightStrip,
@@ -30,7 +31,7 @@ from kasa.iot.iottimezone import (
)
from kasa.iot.modules import IotLightPreset
from kasa.smart import SmartChildDevice, SmartDevice
-from kasa.smartcam import SmartCamDevice
+from kasa.smartcam import SmartCamChild, SmartCamDevice
def _get_subclasses(of_class):
@@ -55,16 +56,22 @@ device_classes = pytest.mark.parametrize(
)
+async def test_device_id(dev: Device):
+ """Test all devices have a device id."""
+ assert dev.device_id
+
+
async def test_alias(dev):
test_alias = "TEST1234"
original = dev.alias
- assert isinstance(original, str)
+ assert isinstance(original, str | None)
await dev.set_alias(test_alias)
await dev.update()
assert dev.alias == test_alias
- await dev.set_alias(original)
+ # If alias is None set it back to empty string
+ await dev.set_alias(original or "")
await dev.update()
assert dev.alias == original
@@ -77,10 +84,25 @@ async def test_device_class_ctors(device_class_name_obj):
credentials = Credentials("foo", "bar")
config = DeviceConfig(host, port_override=port, credentials=credentials)
klass = device_class_name_obj[1]
- if issubclass(klass, SmartChildDevice):
+ if issubclass(klass, SmartChildDevice | SmartCamChild):
parent = SmartDevice(host, config=config)
+ smartcam_required = {
+ "device_model": "foo",
+ "device_type": "SMART.TAPODOORBELL",
+ "alias": "Foo",
+ "sw_ver": "1.1",
+ "hw_ver": "1.0",
+ "mac": "1.2.3.4",
+ "hwId": "hw_id",
+ "oem_id": "oem_id",
+ }
dev = klass(
- parent, {"dummy": "info", "device_id": "dummy"}, {"dummy": "components"}
+ parent,
+ {"dummy": "info", "device_id": "dummy", **smartcam_required},
+ {
+ "component_list": [{"id": "device", "ver_code": 1}],
+ "app_component_list": [{"name": "device", "version": 1}],
+ },
)
else:
dev = klass(host, config=config)
@@ -97,10 +119,15 @@ async def test_device_class_repr(device_class_name_obj):
credentials = Credentials("foo", "bar")
config = DeviceConfig(host, port_override=port, credentials=credentials)
klass = device_class_name_obj[1]
- if issubclass(klass, SmartChildDevice):
+ if issubclass(klass, SmartChildDevice | SmartCamChild):
parent = SmartDevice(host, config=config)
dev = klass(
- parent, {"dummy": "info", "device_id": "dummy"}, {"dummy": "components"}
+ parent,
+ {"dummy": "info", "device_id": "dummy"},
+ {
+ "component_list": [{"id": "device", "ver_code": 1}],
+ "app_component_list": [{"name": "device", "version": 1}],
+ },
)
else:
dev = klass(host, config=config)
@@ -113,14 +140,18 @@ async def test_device_class_repr(device_class_name_obj):
IotStrip: DeviceType.Strip,
IotWallSwitch: DeviceType.WallSwitch,
IotLightStrip: DeviceType.LightStrip,
+ IotCamera: DeviceType.Camera,
SmartChildDevice: DeviceType.Unknown,
SmartDevice: DeviceType.Unknown,
- SmartCamDevice: DeviceType.Camera,
+ SmartCamDevice: DeviceType.Unknown,
+ SmartCamChild: DeviceType.Unknown,
}
type_ = CLASS_TO_DEFAULT_TYPE[klass]
child_repr = ">"
not_child_repr = f"<{type_} at 127.0.0.2 - update() needed>"
- expected_repr = child_repr if klass is SmartChildDevice else not_child_repr
+ expected_repr = (
+ child_repr if klass in {SmartChildDevice, SmartCamChild} else not_child_repr
+ )
assert repr(dev) == expected_repr
@@ -265,19 +296,19 @@ async def test_deprecated_light_attributes(dev: Device):
await _test_attribute(dev, "is_color", bool(light), "Light")
await _test_attribute(dev, "is_variable_color_temp", bool(light), "Light")
- exc = KasaException if light and not light.is_dimmable else None
+ exc = KasaException if light and not light.has_feature("brightness") else None
await _test_attribute(dev, "brightness", bool(light), "Light", will_raise=exc)
await _test_attribute(
dev, "set_brightness", bool(light), "Light", 50, will_raise=exc
)
- exc = KasaException if light and not light.is_color else None
+ exc = KasaException if light and not light.has_feature("hsv") else None
await _test_attribute(dev, "hsv", bool(light), "Light", will_raise=exc)
await _test_attribute(
dev, "set_hsv", bool(light), "Light", 50, 50, 50, will_raise=exc
)
- exc = KasaException if light and not light.is_variable_color_temp else None
+ exc = KasaException if light and not light.has_feature("color_temp") else None
await _test_attribute(dev, "color_temp", bool(light), "Light", will_raise=exc)
await _test_attribute(
dev, "set_color_temp", bool(light), "Light", 2700, will_raise=exc
diff --git a/tests/test_device_factory.py b/tests/test_device_factory.py
index ed73b3a3..19ccfb73 100644
--- a/tests/test_device_factory.py
+++ b/tests/test_device_factory.py
@@ -13,9 +13,13 @@ import aiohttp
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
from kasa import (
+ BaseProtocol,
Credentials,
Discover,
+ IotProtocol,
KasaException,
+ SmartCamProtocol,
+ SmartProtocol,
)
from kasa.device_factory import (
Device,
@@ -33,6 +37,16 @@ from kasa.deviceconfig import (
DeviceFamily,
)
from kasa.discover import DiscoveryResult
+from kasa.transports import (
+ AesTransport,
+ BaseTransport,
+ KlapTransport,
+ KlapTransportV2,
+ LinkieTransportV2,
+ SslAesTransport,
+ SslTransport,
+ XorTransport,
+)
from .conftest import DISCOVERY_MOCK_IP
@@ -46,12 +60,7 @@ def _get_connection_type_device_class(discovery_info):
device_class = Discover._get_device_class(discovery_info)
dr = DiscoveryResult.from_dict(discovery_info["result"])
- connection_type = DeviceConnectionParameters.from_values(
- dr.device_type,
- dr.mgt_encrypt_schm.encrypt_type,
- dr.mgt_encrypt_schm.lv,
- dr.mgt_encrypt_schm.is_support_https,
- )
+ connection_type = Discover._get_connection_parameters(dr)
else:
connection_type = DeviceConnectionParameters.from_values(
DeviceFamily.IotSmartPlugSwitch.value, DeviceEncryptionType.Xor.value
@@ -103,7 +112,7 @@ async def test_connect_custom_port(discovery_mock, mocker, custom_port):
connection_type=ctype,
credentials=Credentials("dummy_user", "dummy_password"),
)
- default_port = 80 if "result" in discovery_data else 9999
+ default_port = discovery_mock.default_port
ctype, _ = _get_connection_type_device_class(discovery_data)
@@ -203,3 +212,86 @@ async def test_device_class_from_unknown_family(caplog):
with caplog.at_level(logging.DEBUG):
assert get_device_class_from_family(dummy_name, https=False) == SmartDevice
assert f"Unknown SMART device with {dummy_name}" in caplog.text
+
+
+# Aliases to make the test params more readable
+CP = DeviceConnectionParameters
+DF = DeviceFamily
+ET = DeviceEncryptionType
+
+
+@pytest.mark.parametrize(
+ ("conn_params", "expected_protocol", "expected_transport"),
+ [
+ pytest.param(
+ CP(DF.SmartIpCamera, ET.Aes, https=True),
+ SmartCamProtocol,
+ SslAesTransport,
+ id="smartcam",
+ ),
+ pytest.param(
+ CP(DF.SmartTapoHub, ET.Aes, https=True),
+ SmartCamProtocol,
+ SslAesTransport,
+ id="smartcam-hub",
+ ),
+ pytest.param(
+ CP(DF.SmartTapoDoorbell, ET.Aes, https=True),
+ SmartCamProtocol,
+ SslAesTransport,
+ id="smartcam-doorbell",
+ ),
+ pytest.param(
+ CP(DF.IotIpCamera, ET.Aes, https=True),
+ IotProtocol,
+ LinkieTransportV2,
+ id="kasacam",
+ ),
+ pytest.param(
+ CP(DF.SmartTapoRobovac, ET.Aes, https=True),
+ SmartProtocol,
+ SslTransport,
+ id="robovac",
+ ),
+ pytest.param(
+ CP(DF.IotSmartPlugSwitch, ET.Klap, https=False),
+ IotProtocol,
+ KlapTransport,
+ id="iot-klap",
+ ),
+ pytest.param(
+ CP(DF.IotSmartPlugSwitch, ET.Xor, https=False),
+ IotProtocol,
+ XorTransport,
+ id="iot-xor",
+ ),
+ pytest.param(
+ CP(DF.SmartTapoPlug, ET.Aes, https=False),
+ SmartProtocol,
+ AesTransport,
+ id="smart-aes",
+ ),
+ pytest.param(
+ CP(DF.SmartTapoPlug, ET.Klap, https=False),
+ SmartProtocol,
+ KlapTransportV2,
+ id="smart-klap",
+ ),
+ pytest.param(
+ CP(DF.SmartTapoChime, ET.Klap, https=False),
+ SmartProtocol,
+ KlapTransportV2,
+ id="smart-chime",
+ ),
+ ],
+)
+async def test_get_protocol(
+ conn_params: DeviceConnectionParameters,
+ expected_protocol: type[BaseProtocol],
+ expected_transport: type[BaseTransport],
+):
+ """Test get_protocol returns the right protocol."""
+ config = DeviceConfig("127.0.0.1", connection_type=conn_params)
+ protocol = get_protocol(config)
+ assert isinstance(protocol, expected_protocol)
+ assert isinstance(protocol._transport, expected_transport)
diff --git a/tests/test_devtools.py b/tests/test_devtools.py
index e18243af..b49268d3 100644
--- a/tests/test_devtools.py
+++ b/tests/test_devtools.py
@@ -1,16 +1,26 @@
"""Module for dump_devinfo tests."""
+import copy
+
import pytest
-from devtools.dump_devinfo import get_legacy_fixture, get_smart_fixtures
+from devtools.dump_devinfo import (
+ _wrap_redactors,
+ get_legacy_fixture,
+ get_smart_fixtures,
+)
from kasa.iot import IotDevice
from kasa.protocols import IotProtocol
+from kasa.protocols.protocol import redact_data
+from kasa.protocols.smartprotocol import REDACTORS as SMART_REDACTORS
from kasa.smart import SmartDevice
from kasa.smartcam import SmartCamDevice
+from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
from .conftest import (
FixtureInfo,
get_device_for_fixture,
+ get_fixture_info,
parametrize,
)
@@ -29,11 +39,13 @@ async def test_fixture_names(fixture_info: FixtureInfo):
"""Test that device info gets the right fixture names."""
if fixture_info.protocol in {"SMARTCAM"}:
device_info = SmartCamDevice._get_device_info(
- fixture_info.data, fixture_info.data.get("discovery_result")
+ fixture_info.data,
+ fixture_info.data.get("discovery_result", {}).get("result"),
)
elif fixture_info.protocol in {"SMART"}:
device_info = SmartDevice._get_device_info(
- fixture_info.data, fixture_info.data.get("discovery_result")
+ fixture_info.data,
+ fixture_info.data.get("discovery_result", {}).get("result"),
)
elif fixture_info.protocol in {"SMART.CHILD"}:
device_info = SmartDevice._get_device_info(fixture_info.data, None)
@@ -62,22 +74,66 @@ async def test_smart_fixtures(fixture_info: FixtureInfo):
assert fixture_info.data == fixture_result.data
+def _normalize_child_device_ids(info: dict):
+ """Scrubbed child device ids in hubs may not match ids in child fixtures.
+
+ Different hub fixtures could create the same child fixture so we scrub
+ them again for the purpose of the test.
+ """
+ if dev_info := info.get("get_device_info"):
+ dev_info["device_id"] = "SCRUBBED"
+ elif (
+ dev_info := info.get("getDeviceInfo", {})
+ .get("device_info", {})
+ .get("basic_info")
+ ):
+ dev_info["dev_id"] = "SCRUBBED"
+
+
@smartcam_fixtures
async def test_smartcam_fixtures(fixture_info: FixtureInfo):
"""Test that smartcam fixtures are created the same."""
dev = await get_device_for_fixture(fixture_info, verbatim=True)
assert isinstance(dev, SmartCamDevice)
- if dev.children:
- pytest.skip("Test not currently implemented for devices with children.")
- fixtures = await get_smart_fixtures(
+
+ created_fixtures = await get_smart_fixtures(
dev.protocol,
discovery_info=fixture_info.data.get("discovery_result"),
batch_size=5,
)
- fixture_result = fixtures[0]
+ fixture_result = created_fixtures.pop(0)
assert fixture_info.data == fixture_result.data
+ for created_child_fixture in created_fixtures:
+ child_fixture_info = get_fixture_info(
+ created_child_fixture.filename + ".json",
+ created_child_fixture.protocol_suffix,
+ )
+
+ assert child_fixture_info
+
+ _normalize_child_device_ids(created_child_fixture.data)
+
+ saved_fixture_data = copy.deepcopy(child_fixture_info.data)
+ _normalize_child_device_ids(saved_fixture_data)
+ saved_fixture_data = {
+ key: val for key, val in saved_fixture_data.items() if val != -1001
+ }
+
+ # Remove the child info from parent from the comparison because the
+ # child may have been created by a different parent fixture
+ saved_fixture_data.pop(CHILD_INFO_FROM_PARENT, None)
+ created_cifp = created_child_fixture.data.pop(CHILD_INFO_FROM_PARENT, None)
+
+ # Still check that the created child info from parent was redacted.
+ # only smartcam children generate child_info_from_parent
+ if created_cifp:
+ redacted_cifp = redact_data(created_cifp, _wrap_redactors(SMART_REDACTORS))
+ assert created_cifp == redacted_cifp
+
+ assert saved_fixture_data == created_child_fixture.data
+
@iot_fixtures
async def test_iot_fixtures(fixture_info: FixtureInfo):
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index 7069e32f..96c9e9c6 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -134,7 +134,14 @@ async def test_discover_single(discovery_mock, custom_port, mocker):
discovery_mock.ip = host
discovery_mock.port_override = custom_port
- device_class = Discover._get_device_class(discovery_mock.discovery_data)
+ disco_data = discovery_mock.discovery_data
+ device_class = Discover._get_device_class(disco_data)
+ http_port = (
+ DiscoveryResult.from_dict(disco_data["result"]).mgt_encrypt_schm.http_port
+ if "result" in disco_data
+ else None
+ )
+
# discovery_mock patches protocol query methods so use spy here.
update_mock = mocker.spy(device_class, "update")
@@ -143,23 +150,27 @@ async def test_discover_single(discovery_mock, custom_port, mocker):
)
assert issubclass(x.__class__, Device)
assert x._discovery_info is not None
- assert x.port == custom_port or x.port == discovery_mock.default_port
+ assert (
+ x.port == custom_port
+ or x.port == discovery_mock.default_port
+ or x.port == http_port
+ )
# Make sure discovery does not call update()
assert update_mock.call_count == 0
- if discovery_mock.default_port == 80:
+ if discovery_mock.default_port != 9999:
assert x.alias is None
ct = DeviceConnectionParameters.from_values(
discovery_mock.device_type,
discovery_mock.encrypt_type,
- discovery_mock.login_version,
+ login_version=discovery_mock.login_version,
+ https=discovery_mock.https,
+ http_port=discovery_mock.http_port,
)
- uses_http = discovery_mock.default_port == 80
config = DeviceConfig(
host=host,
port_override=custom_port,
connection_type=ct,
- uses_http=uses_http,
credentials=Credentials(),
)
assert x.config == config
@@ -390,13 +401,12 @@ async def test_device_update_from_new_discovery_info(discovery_mock):
device_class = Discover._get_device_class(discovery_data)
device = device_class("127.0.0.1")
discover_info = DiscoveryResult.from_dict(discovery_data["result"])
- discover_dump = discover_info.to_dict()
- model, _, _ = discover_dump["device_model"].partition("(")
- discover_dump["model"] = model
- device.update_from_discover_info(discover_dump)
- assert device.mac == discover_dump["mac"].replace("-", ":")
- assert device.model == model
+ device.update_from_discover_info(discovery_data["result"])
+
+ assert device.mac == discover_info.mac.replace("-", ":")
+ no_region_model, _, _ = discover_info.device_model.partition("(")
+ assert device.model == no_region_model
# TODO implement requires_update for SmartDevice
if isinstance(device, IotDevice):
@@ -416,9 +426,9 @@ async def test_discover_single_http_client(discovery_mock, mocker):
x: Device = await Discover.discover_single(host)
- assert x.config.uses_http == (discovery_mock.default_port == 80)
+ assert x.config.uses_http == (discovery_mock.default_port != 9999)
- if discovery_mock.default_port == 80:
+ if discovery_mock.default_port != 9999:
assert x.protocol._transport._http_client.client != http_client
x.config.http_client = http_client
assert x.protocol._transport._http_client.client == http_client
@@ -433,9 +443,9 @@ async def test_discover_http_client(discovery_mock, mocker):
devices = await Discover.discover(discovery_timeout=0)
x: Device = devices[host]
- assert x.config.uses_http == (discovery_mock.default_port == 80)
+ assert x.config.uses_http == (discovery_mock.default_port != 9999)
- if discovery_mock.default_port == 80:
+ if discovery_mock.default_port != 9999:
assert x.protocol._transport._http_client.client != http_client
x.config.http_client = http_client
assert x.protocol._transport._http_client.client == http_client
@@ -665,8 +675,9 @@ async def test_discover_try_connect_all(discovery_mock, mocker):
cparams = DeviceConnectionParameters.from_values(
discovery_mock.device_type,
discovery_mock.encrypt_type,
- discovery_mock.login_version,
- discovery_mock.https,
+ login_version=discovery_mock.login_version,
+ https=discovery_mock.https,
+ http_port=discovery_mock.http_port,
)
protocol = get_protocol(
DeviceConfig(discovery_mock.ip, connection_type=cparams)
@@ -678,21 +689,26 @@ async def test_discover_try_connect_all(discovery_mock, mocker):
protocol_class = IotProtocol
transport_class = XorTransport
+ default_port = discovery_mock.default_port
+
async def _query(self, *args, **kwargs):
if (
self.__class__ is protocol_class
and self._transport.__class__ is transport_class
+ and self._transport._port == default_port
):
return discovery_mock.query_data
- raise KasaException()
+ raise KasaException("Unable to execute query")
async def _update(self, *args, **kwargs):
if (
self.protocol.__class__ is protocol_class
and self.protocol._transport.__class__ is transport_class
+ and self.protocol._transport._port == default_port
):
return
- raise KasaException()
+
+ raise KasaException("Unable to execute update")
mocker.patch("kasa.IotProtocol.query", new=_query)
mocker.patch("kasa.SmartProtocol.query", new=_query)
diff --git a/tests/test_feature.py b/tests/test_feature.py
index 46cdd116..3ccabeb4 100644
--- a/tests/test_feature.py
+++ b/tests/test_feature.py
@@ -74,7 +74,7 @@ def test_feature_value_container(mocker, dummy_feature: Feature):
def test_prop(self):
return "dummy"
- dummy_feature.container = DummyContainer()
+ dummy_feature.container = DummyContainer() # type: ignore[assignment]
dummy_feature.attribute_getter = "test_prop"
mock_dev_prop = mocker.patch.object(
@@ -191,7 +191,12 @@ async def test_feature_setters(dev: Device, mocker: MockerFixture):
exceptions = []
for feat in dev.features.values():
try:
- with patch.object(feat.device.protocol, "query") as query:
+ patch_dev = feat.container._device if feat.container else feat.device
+ with (
+ patch.object(patch_dev.protocol, "query", name=feat.id) as query,
+ # patch update in case feature setter does an update
+ patch.object(patch_dev, "update"),
+ ):
await _test_feature(feat, query)
# we allow our own exceptions to avoid mocking valid responses
except KasaException:
diff --git a/tests/test_readme_examples.py b/tests/test_readme_examples.py
index 394a3aff..b6513476 100644
--- a/tests/test_readme_examples.py
+++ b/tests/test_readme_examples.py
@@ -19,9 +19,12 @@ def test_bulb_examples(mocker):
assert not res["failed"]
-def test_smartdevice_examples(mocker):
+def test_iotdevice_examples(mocker):
"""Use HS110 for emeter examples."""
p = asyncio.run(get_device_for_fixture_protocol("HS110(EU)_1.0_1.2.5.json", "IOT"))
+ asyncio.run(p.set_alias("Bedroom Lamp Plug"))
+ asyncio.run(p.update())
+
mocker.patch("kasa.iot.iotdevice.IotDevice", return_value=p)
mocker.patch("kasa.iot.iotdevice.IotDevice.update")
res = xdoctest.doctest_module("kasa.iot.iotdevice", "all")
@@ -31,18 +34,16 @@ def test_smartdevice_examples(mocker):
def test_plug_examples(mocker):
"""Test plug examples."""
p = asyncio.run(get_device_for_fixture_protocol("HS110(EU)_1.0_1.2.5.json", "IOT"))
- # p = await get_device_for_fixture_protocol("HS110(EU)_1.0_1.2.5.json", "IOT")
+ asyncio.run(p.set_alias("Bedroom Lamp Plug"))
+ asyncio.run(p.update())
mocker.patch("kasa.iot.iotplug.IotPlug", return_value=p)
mocker.patch("kasa.iot.iotplug.IotPlug.update")
res = xdoctest.doctest_module("kasa.iot.iotplug", "all")
assert not res["failed"]
-def test_strip_examples(mocker):
+def test_strip_examples(readmes_mock):
"""Test strip examples."""
- p = asyncio.run(get_device_for_fixture_protocol("KP303(UK)_1.0_1.0.3.json", "IOT"))
- mocker.patch("kasa.iot.iotstrip.IotStrip", return_value=p)
- mocker.patch("kasa.iot.iotstrip.IotStrip.update")
res = xdoctest.doctest_module("kasa.iot.iotstrip", "all")
assert not res["failed"]
@@ -59,6 +60,8 @@ def test_dimmer_examples(mocker):
def test_lightstrip_examples(mocker):
"""Test lightstrip examples."""
p = asyncio.run(get_device_for_fixture_protocol("KL430(US)_1.0_1.0.10.json", "IOT"))
+ asyncio.run(p.set_alias("Bedroom Lightstrip"))
+ asyncio.run(p.update())
mocker.patch("kasa.iot.iotlightstrip.IotLightStrip", return_value=p)
mocker.patch("kasa.iot.iotlightstrip.IotLightStrip.update")
res = xdoctest.doctest_module("kasa.iot.iotlightstrip", "all")
@@ -154,4 +157,23 @@ async def readmes_mock(mocker):
"127.0.0.4": get_fixture_info("KL430(US)_1.0_1.0.10.json", "IOT"), # Lightstrip
"127.0.0.5": get_fixture_info("HS220(US)_1.0_1.5.7.json", "IOT"), # Dimmer
}
+ fixture_infos["127.0.0.1"].data["system"]["get_sysinfo"]["alias"] = (
+ "Bedroom Power Strip"
+ )
+ for index, child in enumerate(
+ fixture_infos["127.0.0.1"].data["system"]["get_sysinfo"]["children"]
+ ):
+ child["alias"] = f"Plug {index + 1}"
+ fixture_infos["127.0.0.2"].data["system"]["get_sysinfo"]["alias"] = (
+ "Bedroom Lamp Plug"
+ )
+ fixture_infos["127.0.0.3"].data["get_device_info"]["nickname"] = (
+ "TGl2aW5nIFJvb20gQnVsYg==" # Living Room Bulb
+ )
+ fixture_infos["127.0.0.4"].data["system"]["get_sysinfo"]["alias"] = (
+ "Bedroom Lightstrip"
+ )
+ fixture_infos["127.0.0.5"].data["system"]["get_sysinfo"]["alias"] = (
+ "Living Room Dimmer Switch"
+ )
return patch_discovery(fixture_infos, mocker)
diff --git a/tests/transports/test_linkietransport.py b/tests/transports/test_linkietransport.py
new file mode 100644
index 00000000..1ac8dba5
--- /dev/null
+++ b/tests/transports/test_linkietransport.py
@@ -0,0 +1,144 @@
+import base64
+from unittest.mock import ANY
+
+import aiohttp
+import pytest
+from yarl import URL
+
+from kasa.credentials import DEFAULT_CREDENTIALS, Credentials, get_default_credentials
+from kasa.deviceconfig import DeviceConfig
+from kasa.exceptions import KasaException
+from kasa.httpclient import HttpClient
+from kasa.json import dumps as json_dumps
+from kasa.transports.linkietransport import LinkieTransportV2
+
+KASACAM_REQUEST_PLAINTEXT = '{"smartlife.cam.ipcamera.dateTime":{"get_status":{}}}'
+KASACAM_RESPONSE_ENCRYPTED = "0PKG74LnnfKc+dvhw5bCgaycqZOjk7Gdv96syaiKsJLTvtupwKPC7aPGse632KrB48/tiPiX9JzDsNW2lK6fqZCgmKuZoZGh3A=="
+KASACAM_RESPONSE_ERROR = '{"smartlife.cam.ipcamera.cloud": {"get_inf": {"err_code": -10008, "err_msg": "Unsupported API call."}}}'
+KASA_DEFAULT_CREDENTIALS_HASH = "YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM="
+
+
+async def test_working(mocker):
+ """No errors with an expected request/response."""
+ host = "127.0.0.1"
+ mock_linkie_device = MockLinkieDevice(host)
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_linkie_device.post
+ )
+ transport_no_creds = LinkieTransportV2(config=DeviceConfig(host))
+
+ response = await transport_no_creds.send(KASACAM_REQUEST_PLAINTEXT)
+ assert response == {
+ "timezone": "UTC-05:00",
+ "area": "America/New_York",
+ "epoch_sec": 1690832800,
+ }
+
+
+async def test_credentials_hash(mocker):
+ """Ensure the default credentials are always passed as Basic Auth."""
+ # Test without credentials input
+
+ host = "127.0.0.1"
+ mock_linkie_device = MockLinkieDevice(host)
+ mock_post = mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_linkie_device.post
+ )
+ transport_no_creds = LinkieTransportV2(config=DeviceConfig(host))
+ await transport_no_creds.send(KASACAM_REQUEST_PLAINTEXT)
+ mock_post.assert_called_once_with(
+ URL(f"https://{host}:10443/data/LINKIE2.json"),
+ params=None,
+ data=ANY,
+ json=None,
+ timeout=ANY,
+ cookies=None,
+ headers={
+ "Authorization": "Basic " + _generate_kascam_basic_auth(),
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ ssl=ANY,
+ )
+
+ assert transport_no_creds.credentials_hash == KASA_DEFAULT_CREDENTIALS_HASH
+ # Test with credentials input
+
+ transport_with_creds = LinkieTransportV2(
+ config=DeviceConfig(host, credentials=Credentials("Admin", "password"))
+ )
+ mock_post.reset_mock()
+
+ await transport_with_creds.send(KASACAM_REQUEST_PLAINTEXT)
+ mock_post.assert_called_once_with(
+ URL(f"https://{host}:10443/data/LINKIE2.json"),
+ params=None,
+ data=ANY,
+ json=None,
+ timeout=ANY,
+ cookies=None,
+ headers={
+ "Authorization": "Basic " + _generate_kascam_basic_auth(),
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ ssl=ANY,
+ )
+
+
+@pytest.mark.parametrize(
+ ("return_status", "return_data", "expected"),
+ [
+ (500, KASACAM_RESPONSE_ENCRYPTED, "500"),
+ (200, "AAAAAAAAAAAAAAAAAAAAAAAA", "Unable to read response"),
+ (200, KASACAM_RESPONSE_ERROR, "Unsupported API call"),
+ ],
+)
+async def test_exceptions(mocker, return_status, return_data, expected):
+ """Test a variety of possible responses from the device."""
+ host = "127.0.0.1"
+ transport = LinkieTransportV2(config=DeviceConfig(host))
+ mock_linkie_device = MockLinkieDevice(
+ host, status_code=return_status, response=return_data
+ )
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_linkie_device.post
+ )
+
+ with pytest.raises(KasaException, match=expected):
+ await transport.send(KASACAM_REQUEST_PLAINTEXT)
+
+
+def _generate_kascam_basic_auth():
+ creds = get_default_credentials(DEFAULT_CREDENTIALS["KASACAMERA"])
+ creds_combined = f"{creds.username}:{creds.password}"
+ return base64.b64encode(creds_combined.encode()).decode()
+
+
+class MockLinkieDevice:
+ """Based on MockSslDevice."""
+
+ class _mock_response:
+ def __init__(self, status, request: dict):
+ self.status = status
+ self._json = request
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_t, exc_v, exc_tb):
+ pass
+
+ async def read(self):
+ if isinstance(self._json, dict):
+ return json_dumps(self._json).encode()
+ return self._json
+
+ def __init__(self, host, *, status_code=200, response=KASACAM_RESPONSE_ENCRYPTED):
+ self.host = host
+ self.http_client = HttpClient(DeviceConfig(self.host))
+ self.status_code = status_code
+ self.response = response
+
+ async def post(
+ self, url: URL, *, headers=None, params=None, json=None, data=None, **__
+ ):
+ return self._mock_response(self.status_code, self.response)
diff --git a/tests/transports/test_sslaestransport.py b/tests/transports/test_sslaestransport.py
index 6816fa35..e8ff9e52 100644
--- a/tests/transports/test_sslaestransport.py
+++ b/tests/transports/test_sslaestransport.py
@@ -15,24 +15,29 @@ from kasa.credentials import DEFAULT_CREDENTIALS, Credentials, get_default_crede
from kasa.deviceconfig import DeviceConfig
from kasa.exceptions import (
AuthenticationError,
+ DeviceError,
KasaException,
SmartErrorCode,
+ _RetryableError,
)
from kasa.httpclient import HttpClient
from kasa.transports.aestransport import AesEncyptionSession
from kasa.transports.sslaestransport import (
SslAesTransport,
TransportState,
+ _md5_hash,
_sha256_hash,
)
# Transport tests are not designed for real devices
-pytestmark = [pytest.mark.requires_dummy]
+# SslAesTransport use a socket to get it's own ip address
+pytestmark = [pytest.mark.requires_dummy, pytest.mark.enable_socket]
MOCK_ADMIN_USER = get_default_credentials(DEFAULT_CREDENTIALS["TAPOCAMERA"]).username
MOCK_PWD = "correct_pwd" # noqa: S105
MOCK_USER = "mock@example.com"
MOCK_STOCK = "abcdefghijklmnopqrstuvwxyz1234)("
+MOCK_UNENCRYPTED_PASSTHROUGH_STOK = "32charLowerCaseHexStok"
@pytest.mark.parametrize(
@@ -200,6 +205,182 @@ async def test_unencrypted_response(mocker, caplog):
)
+@pytest.mark.parametrize(("want_default"), [True, False])
+@pytest.mark.xdist_group(name="caplog")
+async def test_unencrypted_passthrough(mocker, caplog, want_default):
+ host = "127.0.0.1"
+ mock_ssl_aes_device = MockSslAesDevice(
+ host, unencrypted_passthrough=True, want_default_username=want_default
+ )
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
+ )
+
+ transport = SslAesTransport(
+ config=DeviceConfig(host, credentials=Credentials(MOCK_USER, MOCK_PWD))
+ )
+
+ request = {
+ "method": "getDeviceInfo",
+ "params": None,
+ }
+ caplog.set_level(logging.DEBUG)
+ res = await transport.send(json_dumps(request))
+ assert "result" in res
+ assert (
+ f"Succesfully logged in to {host} with less secure passthrough" in caplog.text
+ )
+
+
+@pytest.mark.parametrize(("want_default"), [True, False])
+@pytest.mark.xdist_group(name="caplog")
+async def test_unencrypted_passthrough_errors(mocker, caplog, want_default):
+ host = "127.0.0.1"
+ request = {
+ "method": "getDeviceInfo",
+ "params": None,
+ }
+ transport = SslAesTransport(
+ config=DeviceConfig(host, credentials=Credentials(MOCK_USER, MOCK_PWD))
+ )
+ caplog.set_level(logging.DEBUG)
+
+ # Test bad password
+ mock_ssl_aes_device = MockSslAesDevice(
+ host,
+ unencrypted_passthrough=True,
+ want_default_username=want_default,
+ digest_password_fail=True,
+ )
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
+ )
+
+ msg = f"Unable to log in to {host} with less secure login"
+ with pytest.raises(AuthenticationError):
+ await transport.send(json_dumps(request))
+
+ assert msg in caplog.text
+
+ # Test bad status code in handshake
+ mock_ssl_aes_device = MockSslAesDevice(
+ host,
+ unencrypted_passthrough=True,
+ want_default_username=want_default,
+ status_code=401,
+ )
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
+ )
+
+ msg = f"{host} responded with an unexpected " f"status code 401 to handshake1"
+ with pytest.raises(KasaException, match=msg):
+ await transport.send(json_dumps(request))
+
+ # Test bad status code in login
+ mock_ssl_aes_device = MockSslAesDevice(
+ host,
+ unencrypted_passthrough=True,
+ want_default_username=want_default,
+ status_code_list=[200, 401],
+ )
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
+ )
+
+ msg = f"{host} responded with an unexpected " f"status code 401 to login"
+ with pytest.raises(KasaException, match=msg):
+ await transport.send(json_dumps(request))
+
+ # Test bad status code in send
+ mock_ssl_aes_device = MockSslAesDevice(
+ host,
+ unencrypted_passthrough=True,
+ want_default_username=want_default,
+ status_code_list=[200, 200, 401],
+ )
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
+ )
+
+ msg = f"{host} responded with an unexpected " f"status code 401 to unencrypted send"
+ with pytest.raises(KasaException, match=msg):
+ await transport.send(json_dumps(request))
+
+ # Test error code in send response
+ mock_ssl_aes_device = MockSslAesDevice(
+ host,
+ unencrypted_passthrough=True,
+ want_default_username=want_default,
+ send_error_code=SmartErrorCode.BAD_USERNAME.value,
+ )
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
+ )
+
+ msg = f"Error sending message: {host}:"
+ with pytest.raises(KasaException, match=msg):
+ await transport.send(json_dumps(request))
+
+
+async def test_device_blocked_response(mocker):
+ host = "127.0.0.1"
+ mock_ssl_aes_device = MockSslAesDevice(host, device_blocked=True)
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
+ )
+
+ transport = SslAesTransport(
+ config=DeviceConfig(host, credentials=Credentials(MOCK_USER, MOCK_PWD))
+ )
+ msg = "Device blocked for 1685 seconds"
+
+ with pytest.raises(DeviceError, match=msg):
+ await transport.perform_handshake()
+
+
+@pytest.mark.parametrize(
+ ("response", "expected_msg"),
+ [
+ pytest.param(
+ {"error_code": -1, "msg": "Check tapo tag failed"},
+ '{"error_code": -1, "msg": "Check tapo tag failed"}',
+ id="can-decrypt",
+ ),
+ pytest.param(
+ b"12345678",
+ str({"result": {"response": "12345678"}, "error_code": 0}),
+ id="cannot-decrypt",
+ ),
+ ],
+)
+async def test_device_500_error(mocker, response, expected_msg):
+ """Test 500 error raises retryable exception."""
+ host = "127.0.0.1"
+ mock_ssl_aes_device = MockSslAesDevice(host)
+ mocker.patch.object(
+ aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
+ )
+
+ transport = SslAesTransport(
+ config=DeviceConfig(host, credentials=Credentials(MOCK_USER, MOCK_PWD))
+ )
+
+ request = {
+ "method": "getDeviceInfo",
+ "params": None,
+ }
+
+ await transport.perform_handshake()
+
+ mock_ssl_aes_device.put_next_response(response)
+ mock_ssl_aes_device.status_code = 500
+
+ msg = f"Device 127.0.0.1 replied with status 500 after handshake, response: {expected_msg}"
+ with pytest.raises(_RetryableError, match=msg):
+ await transport.send(json_dumps(request))
+
+
async def test_port_override():
"""Test that port override sets the app_url."""
host = "127.0.0.1"
@@ -235,6 +416,43 @@ class MockSslAesDevice:
},
}
+ DEVICE_BLOCKED_RESP = {
+ "data": {"code": SmartErrorCode.DEVICE_BLOCKED.value, "sec_left": 1685},
+ "error_code": SmartErrorCode.SESSION_EXPIRED.value,
+ }
+
+ UNENCRYPTED_PASSTHROUGH_BAD_USER_RESP = {
+ "error_code": SmartErrorCode.SESSION_EXPIRED.value,
+ "result": {
+ "data": {
+ "code": SmartErrorCode.BAD_USERNAME.value,
+ "encrypt_type": ["1", "2"],
+ "key": "Someb64keyWithUnknownPurpose",
+ "nonce": "MixedCaseAlphaNumericWithUnknownPurpose",
+ }
+ },
+ }
+
+ UNENCRYPTED_PASSTHROUGH_HANDSHAKE_RESP = {
+ "error_code": SmartErrorCode.SESSION_EXPIRED.value,
+ "result": {
+ "data": {
+ "code": SmartErrorCode.SESSION_EXPIRED.value,
+ "time": 9,
+ "max_time": 10,
+ "sec_left": 0,
+ "encrypt_type": ["1", "2"],
+ "key": "Someb64keyWithUnknownPurpose",
+ "nonce": "MixedCaseAlphaNumericWithUnknownPurpose",
+ }
+ },
+ }
+
+ UNENCRYPTED_PASSTHROUGH_GOOD_LOGIN_RESPONSE = {
+ "error_code": 0,
+ "result": {"stok": MOCK_UNENCRYPTED_PASSTHROUGH_STOK, "user_group": "root"},
+ }
+
class _mock_response:
def __init__(self, status, request: dict):
self.status = status
@@ -256,6 +474,7 @@ class MockSslAesDevice:
host,
*,
status_code=200,
+ status_code_list=None,
want_default_username: bool = False,
do_not_encrypt_response=False,
send_response=None,
@@ -263,6 +482,8 @@ class MockSslAesDevice:
send_error_code=0,
secure_passthrough_error_code=0,
digest_password_fail=False,
+ device_blocked=False,
+ unencrypted_passthrough=False,
):
self.host = host
self.http_client = HttpClient(DeviceConfig(self.host))
@@ -272,11 +493,21 @@ class MockSslAesDevice:
# test behaviour attributes
self.status_code = status_code
+ self.status_code_list = status_code_list if status_code_list else []
self.send_error_code = send_error_code
self.secure_passthrough_error_code = secure_passthrough_error_code
self.do_not_encrypt_response = do_not_encrypt_response
self.want_default_username = want_default_username
self.digest_password_fail = digest_password_fail
+ self.device_blocked = device_blocked
+ self.unencrypted_passthrough = unencrypted_passthrough
+
+ self._next_responses: list[dict | bytes] = []
+
+ def _get_status_code(self):
+ if self.status_code_list:
+ return self.status_code_list.pop(0)
+ return self.status_code
async def post(self, url: URL, params=None, json=None, data=None, *_, **__):
if data:
@@ -291,27 +522,54 @@ class MockSslAesDevice:
return await self._return_handshake1_response(url, json)
if method == "login" and self.handshake1_complete:
+ if self.unencrypted_passthrough:
+ return await self._return_unencrypted_passthrough_login_response(
+ url, json
+ )
+
return await self._return_handshake2_response(url, json)
elif method == "securePassthrough":
assert url == URL(f"https://{self.host}/stok={MOCK_STOCK}/ds")
return await self._return_secure_passthrough_response(url, json)
else:
- assert url == URL(f"https://{self.host}/stok={MOCK_STOCK}/ds")
+ # The unencrypted passthrough with have actual query method names.
+ # This path is also used by the mock class to return unencrypted
+ # responses to single 'get' queries which the secure fw returns as unencrypted
+ stok = (
+ MOCK_UNENCRYPTED_PASSTHROUGH_STOK
+ if self.unencrypted_passthrough
+ else MOCK_STOCK
+ )
+ assert url == URL(f"https://{self.host}/stok={stok}/ds")
return await self._return_send_response(url, json)
async def _return_handshake1_response(self, url: URL, request: dict[str, Any]):
request_nonce = request["params"].get("cnonce")
request_username = request["params"].get("username")
+ if self.device_blocked:
+ return self._mock_response(self.status_code, self.DEVICE_BLOCKED_RESP)
+
if (self.want_default_username and request_username != MOCK_ADMIN_USER) or (
not self.want_default_username and request_username != MOCK_USER
):
- return self._mock_response(self.status_code, self.BAD_USER_RESP)
+ resp = (
+ self.UNENCRYPTED_PASSTHROUGH_BAD_USER_RESP
+ if self.unencrypted_passthrough
+ else self.BAD_USER_RESP
+ )
+ return self._mock_response(self.status_code, resp)
device_confirm = SslAesTransport.generate_confirm_hash(
request_nonce, self.server_nonce, _sha256_hash(MOCK_PWD.encode())
)
self.handshake1_complete = True
+
+ if self.unencrypted_passthrough:
+ return self._mock_response(
+ self._get_status_code(), self.UNENCRYPTED_PASSTHROUGH_HANDSHAKE_RESP
+ )
+
resp = {
"error_code": SmartErrorCode.INVALID_NONCE.value,
"result": {
@@ -324,7 +582,29 @@ class MockSslAesDevice:
}
},
}
- return self._mock_response(self.status_code, resp)
+ return self._mock_response(self._get_status_code(), resp)
+
+ async def _return_unencrypted_passthrough_login_response(
+ self, url: URL, request: dict[str, Any]
+ ):
+ request_username = request["params"].get("username")
+ request_password = request["params"].get("password")
+ if (self.want_default_username and request_username != MOCK_ADMIN_USER) or (
+ not self.want_default_username and request_username != MOCK_USER
+ ):
+ return self._mock_response(
+ self._get_status_code(), self.UNENCRYPTED_PASSTHROUGH_BAD_USER_RESP
+ )
+
+ expected_pwd = _md5_hash(MOCK_PWD.encode())
+ if request_password != expected_pwd or self.digest_password_fail:
+ return self._mock_response(
+ self._get_status_code(), self.UNENCRYPTED_PASSTHROUGH_HANDSHAKE_RESP
+ )
+
+ return self._mock_response(
+ self._get_status_code(), self.UNENCRYPTED_PASSTHROUGH_GOOD_LOGIN_RESPONSE
+ )
async def _return_handshake2_response(self, url: URL, request: dict[str, Any]):
request_nonce = request["params"].get("cnonce")
@@ -332,14 +612,14 @@ class MockSslAesDevice:
if (self.want_default_username and request_username != MOCK_ADMIN_USER) or (
not self.want_default_username and request_username != MOCK_USER
):
- return self._mock_response(self.status_code, self.BAD_USER_RESP)
+ return self._mock_response(self._get_status_code(), self.BAD_USER_RESP)
request_password = request["params"].get("digest_passwd")
expected_pwd = SslAesTransport.generate_digest_password(
request_nonce, self.server_nonce, _sha256_hash(MOCK_PWD.encode())
)
if request_password != expected_pwd or self.digest_password_fail:
- return self._mock_response(self.status_code, self.BAD_PWD_RESP)
+ return self._mock_response(self._get_status_code(), self.BAD_PWD_RESP)
lsk = SslAesTransport.generate_encryption_token(
"lsk", request_nonce, self.server_nonce, _sha256_hash(MOCK_PWD.encode())
@@ -352,18 +632,31 @@ class MockSslAesDevice:
"error_code": 0,
"result": {"stok": MOCK_STOCK, "user_group": "root", "start_seq": 100},
}
- return self._mock_response(self.status_code, resp)
+ return self._mock_response(self._get_status_code(), resp)
async def _return_secure_passthrough_response(self, url: URL, json: dict[str, Any]):
encrypted_request = json["params"]["request"]
assert self.encryption_session
decrypted_request = self.encryption_session.decrypt(encrypted_request.encode())
decrypted_request_dict = json_loads(decrypted_request)
- decrypted_response = await self._post(url, decrypted_request_dict)
- async with decrypted_response:
- decrypted_response_data = await decrypted_response.read()
- encrypted_response = self.encryption_session.encrypt(decrypted_response_data)
+ if self._next_responses:
+ next_response = self._next_responses.pop(0)
+ if isinstance(next_response, dict):
+ decrypted_response_data = json_dumps(next_response).encode()
+ encrypted_response = self.encryption_session.encrypt(
+ decrypted_response_data
+ )
+ else:
+ encrypted_response = next_response
+ else:
+ decrypted_response = await self._post(url, decrypted_request_dict)
+ async with decrypted_response:
+ decrypted_response_data = await decrypted_response.read()
+ encrypted_response = self.encryption_session.encrypt(
+ decrypted_response_data
+ )
+
response = (
decrypted_response_data
if self.do_not_encrypt_response
@@ -373,8 +666,11 @@ class MockSslAesDevice:
"result": {"response": response.decode()},
"error_code": self.secure_passthrough_error_code,
}
- return self._mock_response(self.status_code, result)
+ return self._mock_response(self._get_status_code(), result)
async def _return_send_response(self, url: URL, json: dict[str, Any]):
result = {"result": {"method": None}, "error_code": self.send_error_code}
- return self._mock_response(self.status_code, result)
+ return self._mock_response(self._get_status_code(), result)
+
+ def put_next_response(self, request: dict | bytes) -> None:
+ self._next_responses.append(request)
diff --git a/uv.lock b/uv.lock
index 12e2cb81..df6132ca 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,16 +3,16 @@ requires-python = ">=3.11, <4.0"
[[package]]
name = "aiohappyeyeballs"
-version = "2.4.3"
+version = "2.4.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 }
+sdist = { url = "https://files.pythonhosted.org/packages/7f/55/e4373e888fdacb15563ef6fa9fa8c8252476ea071e96fb46defac9f18bf2/aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", size = 21977 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 },
+ { url = "https://files.pythonhosted.org/packages/b9/74/fbb6559de3607b3300b9be3cc64e97548d55678e44623db17820dbd20002/aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8", size = 14756 },
]
[[package]]
name = "aiohttp"
-version = "3.11.7"
+version = "3.11.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -23,65 +23,65 @@ dependencies = [
{ name = "propcache" },
{ name = "yarl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4b/cb/f9bb10e0cf6f01730b27d370b10cc15822bea4395acd687abc8cc5fed3ed/aiohttp-3.11.7.tar.gz", hash = "sha256:01a8aca4af3da85cea5c90141d23f4b0eee3cbecfd33b029a45a80f28c66c668", size = 7666482 }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/13/7f/272fa1adf68fe2fbebfe686a67b50cfb40d86dfe47d0441aff6f0b7c4c0e/aiohttp-3.11.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cea52d11e02123f125f9055dfe0ccf1c3857225fb879e4a944fae12989e2aef2", size = 706820 },
- { url = "https://files.pythonhosted.org/packages/79/3c/6d612ef77cdba75364393f04c5c577481e3b5123a774eea447ada1ddd14f/aiohttp-3.11.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ce18f703b7298e7f7633efd6a90138d99a3f9a656cb52c1201e76cb5d79cf08", size = 466654 },
- { url = "https://files.pythonhosted.org/packages/4f/b8/1052667d4800cd49bb4f869f1ed42f5e9d5acd4676275e64ccc244c9c040/aiohttp-3.11.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:670847ee6aeb3a569cd7cdfbe0c3bec1d44828bbfbe78c5d305f7f804870ef9e", size = 454041 },
- { url = "https://files.pythonhosted.org/packages/9f/07/80fa7302314a6ee1c9278550e9d95b77a4c895999bfbc5364ed0ee28dc7c/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dda726f89bfa5c465ba45b76515135a3ece0088dfa2da49b8bb278f3bdeea12", size = 1684778 },
- { url = "https://files.pythonhosted.org/packages/2e/30/a71eb45197ad6bb6af87dfb39be8b56417d24d916047d35ef3f164af87f4/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25b74a811dba37c7ea6a14d99eb9402d89c8d739d50748a75f3cf994cf19c43", size = 1740992 },
- { url = "https://files.pythonhosted.org/packages/22/74/0f9394429f3c4197129333a150a85cb2a642df30097a39dd41257f0b3bdc/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5522ee72f95661e79db691310290c4618b86dff2d9b90baedf343fd7a08bf79", size = 1781816 },
- { url = "https://files.pythonhosted.org/packages/7f/1a/1e256b39179c98d16d53ac62f64bfcfe7c5b2c1e68b83cddd4165854524f/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fbf41a6bbc319a7816ae0f0177c265b62f2a59ad301a0e49b395746eb2a9884", size = 1676692 },
- { url = "https://files.pythonhosted.org/packages/9b/37/f19d2e00efcabb9183b16bd91244de1d9c4ff7bf0fb5b8302e29a78f3286/aiohttp-3.11.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59ee1925b5a5efdf6c4e7be51deee93984d0ac14a6897bd521b498b9916f1544", size = 1619523 },
- { url = "https://files.pythonhosted.org/packages/ae/3c/af50cf5e06b98783fd776f17077f7b7e755d461114af5d6744dc037fc3b0/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24054fce8c6d6f33a3e35d1c603ef1b91bbcba73e3f04a22b4f2f27dac59b347", size = 1644084 },
- { url = "https://files.pythonhosted.org/packages/c0/a6/4e0233b085cbf2b6de573515c1eddde82f1c1f17e69347e32a5a5f2617ff/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:351849aca2c6f814575c1a485c01c17a4240413f960df1bf9f5deb0003c61a53", size = 1648332 },
- { url = "https://files.pythonhosted.org/packages/06/20/7062e76e7817318c421c0f9d7b650fb81aaecf6d2f3a9833805b45ec2ea8/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:12724f3a211fa243570e601f65a8831372caf1a149d2f1859f68479f07efec3d", size = 1730912 },
- { url = "https://files.pythonhosted.org/packages/6c/1c/ff6ae4b1789894e6faf8a4e260cd3861cad618dc80ad15326789a7765750/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7ea4490360b605804bea8173d2d086b6c379d6bb22ac434de605a9cbce006e7d", size = 1752619 },
- { url = "https://files.pythonhosted.org/packages/33/58/ddd5cba5ca245c00b04e9d28a7988b0f0eda02de494f8e62ecd2780655c2/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e0bf378db07df0a713a1e32381a1b277e62ad106d0dbe17b5479e76ec706d720", size = 1692801 },
- { url = "https://files.pythonhosted.org/packages/b2/fc/32d5e2070b43d3722b7ea65ddc6b03ffa39bcc4b5ab6395a825cde0872ad/aiohttp-3.11.7-cp311-cp311-win32.whl", hash = "sha256:cd8d62cab363dfe713067027a5adb4907515861f1e4ce63e7be810b83668b847", size = 414899 },
- { url = "https://files.pythonhosted.org/packages/ec/7e/50324c6d3df4540f5963def810b9927f220c99864065849a1dfcae77a6ce/aiohttp-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:bf0e6cce113596377cadda4e3ac5fb89f095bd492226e46d91b4baef1dd16f60", size = 440938 },
- { url = "https://files.pythonhosted.org/packages/bf/1e/2e96b2526c590dcb99db0b94ac4f9b927ecc07f94735a8a941dee143d48b/aiohttp-3.11.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4bb7493c3e3a36d3012b8564bd0e2783259ddd7ef3a81a74f0dbfa000fce48b7", size = 702326 },
- { url = "https://files.pythonhosted.org/packages/b5/ce/b5d7f3e68849f1f5e0b85af4ac9080b9d3c0a600857140024603653c2209/aiohttp-3.11.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e143b0ef9cb1a2b4f74f56d4fbe50caa7c2bb93390aff52f9398d21d89bc73ea", size = 461944 },
- { url = "https://files.pythonhosted.org/packages/28/fa/f4d98db1b7f8f0c3f74bdbd6d0d98cfc89984205cd33f1b8ee3f588ee5ad/aiohttp-3.11.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7c58a240260822dc07f6ae32a0293dd5bccd618bb2d0f36d51c5dbd526f89c0", size = 454348 },
- { url = "https://files.pythonhosted.org/packages/04/f0/c238dda5dc9a3d12b76636e2cf0ea475890ac3a1c7e4ff0fd6c3cea2fc2d/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d20cfe63a1c135d26bde8c1d0ea46fd1200884afbc523466d2f1cf517d1fe33", size = 1678795 },
- { url = "https://files.pythonhosted.org/packages/79/ee/3a18f792247e6d95dba13aaedc9dc317c3c6e75f4b88c2dd4b960d20ad2f/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12e4d45847a174f77b2b9919719203769f220058f642b08504cf8b1cf185dacf", size = 1734411 },
- { url = "https://files.pythonhosted.org/packages/f5/79/3eb84243087a9a32cae821622c935107b4b55a5b21b76772e8e6c41092e9/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf4efa2d01f697a7dbd0509891a286a4af0d86902fc594e20e3b1712c28c0106", size = 1788959 },
- { url = "https://files.pythonhosted.org/packages/91/93/ad77782c5edfa17aafc070bef978fbfb8459b2f150595ffb01b559c136f9/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee6a4cdcbf54b8083dc9723cdf5f41f722c00db40ccf9ec2616e27869151129", size = 1687463 },
- { url = "https://files.pythonhosted.org/packages/ba/48/db35bd21b7877efa0be5f28385d8978c55323c5ce7685712e53f3f6c0bd9/aiohttp-3.11.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6095aaf852c34f42e1bd0cf0dc32d1e4b48a90bfb5054abdbb9d64b36acadcb", size = 1618374 },
- { url = "https://files.pythonhosted.org/packages/ba/77/30f87db55c79fd145ed5fd15b92f2e820ce81065d41ae437797aaa550e3b/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1cf03d27885f8c5ebf3993a220cc84fc66375e1e6e812731f51aab2b2748f4a6", size = 1637021 },
- { url = "https://files.pythonhosted.org/packages/af/76/10b188b78ee18d0595af156d6a238bc60f9d8571f0f546027eb7eaf65b25/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1a17f6a230f81eb53282503823f59d61dff14fb2a93847bf0399dc8e87817307", size = 1650792 },
- { url = "https://files.pythonhosted.org/packages/fa/33/4411bbb8ad04c47d0f4c7bd53332aaf350e49469cf6b65b132d4becafe27/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:481f10a1a45c5f4c4a578bbd74cff22eb64460a6549819242a87a80788461fba", size = 1696248 },
- { url = "https://files.pythonhosted.org/packages/fe/2d/6135d0dc1851a33d3faa937b20fef81340bc95e8310536d4c7f1f8ecc026/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:db37248535d1ae40735d15bdf26ad43be19e3d93ab3f3dad8507eb0f85bb8124", size = 1729188 },
- { url = "https://files.pythonhosted.org/packages/f5/76/a57ceff577ae26fe9a6f31ac799bc638ecf26e4acdf04295290b9929b349/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d18a8b44ec8502a7fde91446cd9c9b95ce7c49f1eacc1fb2358b8907d4369fd", size = 1690038 },
- { url = "https://files.pythonhosted.org/packages/4b/81/b20e09003b6989a7f23a721692137a6143420a151063c750ab2a04878e3c/aiohttp-3.11.7-cp312-cp312-win32.whl", hash = "sha256:3d1c9c15d3999107cbb9b2d76ca6172e6710a12fda22434ee8bd3f432b7b17e8", size = 409887 },
- { url = "https://files.pythonhosted.org/packages/b7/0b/607c98bff1d07bb21e0c39e7711108ef9ff4f2a361a3ec1ce8dce93623a5/aiohttp-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:018f1b04883a12e77e7fc161934c0f298865d3a484aea536a6a2ca8d909f0ba0", size = 436462 },
- { url = "https://files.pythonhosted.org/packages/7a/53/8d77186c6a33bd087714df18274cdcf6e36fd69a9e841c85b7e81a20b18e/aiohttp-3.11.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:241a6ca732d2766836d62c58c49ca7a93d08251daef0c1e3c850df1d1ca0cbc4", size = 695811 },
- { url = "https://files.pythonhosted.org/packages/62/b6/4c3d107a5406aa6f99f618afea82783f54ce2d9644020f50b9c88f6e823d/aiohttp-3.11.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa3705a8d14de39898da0fbad920b2a37b7547c3afd2a18b9b81f0223b7d0f68", size = 458530 },
- { url = "https://files.pythonhosted.org/packages/d9/05/dbf0bd3966be8ebed3beb4007a2d1356d79af4fe7c93e54f984df6385193/aiohttp-3.11.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9acfc7f652b31853eed3b92095b0acf06fd5597eeea42e939bd23a17137679d5", size = 451371 },
- { url = "https://files.pythonhosted.org/packages/19/6a/2198580314617b6cf9c4b813b84df5832b5f8efedcb8a7e8b321a187233c/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcefcf2915a2dbdbce37e2fc1622129a1918abfe3d06721ce9f6cdac9b6d2eaa", size = 1662905 },
- { url = "https://files.pythonhosted.org/packages/2b/65/08696fd7503f6a6f9f782bd012bf47f36d4ed179a7d8c95dba4726d5cc67/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1f6490dd1862af5aae6cfcf2a274bffa9a5b32a8f5acb519a7ecf5a99a88866", size = 1713794 },
- { url = "https://files.pythonhosted.org/packages/c8/a3/b9a72dce6f15e2efbc09fa67c1067c4f3a3bb05661c0ae7b40799cde02b7/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac5462582d6561c1c1708853a9faf612ff4e5ea5e679e99be36143d6eabd8e", size = 1770757 },
- { url = "https://files.pythonhosted.org/packages/78/7e/8fb371b5f8c4c1eaa0d0a50750c0dd68059f86794aeb36919644815486f5/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1a6309005acc4b2bcc577ba3b9169fea52638709ffacbd071f3503264620da", size = 1673136 },
- { url = "https://files.pythonhosted.org/packages/2f/0f/09685d13d2c7634cb808868ea29c170d4dcde4215a4a90fb86491cd3ae25/aiohttp-3.11.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b973cce96793725ef63eb449adfb74f99c043c718acb76e0d2a447ae369962", size = 1600370 },
- { url = "https://files.pythonhosted.org/packages/00/2e/18fd38b117f9b3a375166ccb70ed43cf7e3dfe2cc947139acc15feefc5a2/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ce91a24aac80de6be8512fb1c4838a9881aa713f44f4e91dd7bb3b34061b497d", size = 1613459 },
- { url = "https://files.pythonhosted.org/packages/2c/94/10a82abc680d753be33506be699aaa330152ecc4f316eaf081f996ee56c2/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:875f7100ce0e74af51d4139495eec4025affa1a605280f23990b6434b81df1bd", size = 1613924 },
- { url = "https://files.pythonhosted.org/packages/e9/58/897c0561f5c522dda6e173192f1e4f10144e1a7126096f17a3f12b7aa168/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c171fc35d3174bbf4787381716564042a4cbc008824d8195eede3d9b938e29a8", size = 1681164 },
- { url = "https://files.pythonhosted.org/packages/8b/8b/3a48b1cdafa612679d976274355f6a822de90b85d7dba55654ecfb01c979/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ee9afa1b0d2293c46954f47f33e150798ad68b78925e3710044e0d67a9487791", size = 1712139 },
- { url = "https://files.pythonhosted.org/packages/aa/9d/70ab5b4dd7900db04af72840e033aee06e472b1343e372ea256ed675511c/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8360c7cc620abb320e1b8d603c39095101391a82b1d0be05fb2225471c9c5c52", size = 1667446 },
- { url = "https://files.pythonhosted.org/packages/cb/98/b5fbcc8f6056f0c56001c75227e6b7ca9ee4f2e5572feca82ff3d65d485d/aiohttp-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7a9318da4b4ada9a67c1dd84d1c0834123081e746bee311a16bb449f363d965e", size = 408689 },
- { url = "https://files.pythonhosted.org/packages/ef/07/4d1504577fa6349dd2e3839e89fb56e5dee38d64efe3d4366e9fcfda0cdb/aiohttp-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:fc6da202068e0a268e298d7cd09b6e9f3997736cd9b060e2750963754552a0a9", size = 434809 },
+ { url = "https://files.pythonhosted.org/packages/34/ae/e8806a9f054e15f1d18b04db75c23ec38ec954a10c0a68d3bd275d7e8be3/aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76", size = 708624 },
+ { url = "https://files.pythonhosted.org/packages/c7/e0/313ef1a333fb4d58d0c55a6acb3cd772f5d7756604b455181049e222c020/aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538", size = 468507 },
+ { url = "https://files.pythonhosted.org/packages/a9/60/03455476bf1f467e5b4a32a465c450548b2ce724eec39d69f737191f936a/aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204", size = 455571 },
+ { url = "https://files.pythonhosted.org/packages/be/f9/469588603bd75bf02c8ffb8c8a0d4b217eed446b49d4a767684685aa33fd/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9", size = 1685694 },
+ { url = "https://files.pythonhosted.org/packages/88/b9/1b7fa43faf6c8616fa94c568dc1309ffee2b6b68b04ac268e5d64b738688/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03", size = 1743660 },
+ { url = "https://files.pythonhosted.org/packages/2a/8b/0248d19dbb16b67222e75f6aecedd014656225733157e5afaf6a6a07e2e8/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287", size = 1785421 },
+ { url = "https://files.pythonhosted.org/packages/c4/11/f478e071815a46ca0a5ae974651ff0c7a35898c55063305a896e58aa1247/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e", size = 1675145 },
+ { url = "https://files.pythonhosted.org/packages/26/5d/284d182fecbb5075ae10153ff7374f57314c93a8681666600e3a9e09c505/aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665", size = 1619804 },
+ { url = "https://files.pythonhosted.org/packages/1b/78/980064c2ad685c64ce0e8aeeb7ef1e53f43c5b005edcd7d32e60809c4992/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b", size = 1654007 },
+ { url = "https://files.pythonhosted.org/packages/21/8d/9e658d63b1438ad42b96f94da227f2e2c1d5c6001c9e8ffcc0bfb22e9105/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34", size = 1650022 },
+ { url = "https://files.pythonhosted.org/packages/85/fd/a032bf7f2755c2df4f87f9effa34ccc1ef5cea465377dbaeef93bb56bbd6/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d", size = 1732899 },
+ { url = "https://files.pythonhosted.org/packages/c5/0c/c2b85fde167dd440c7ba50af2aac20b5a5666392b174df54c00f888c5a75/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2", size = 1755142 },
+ { url = "https://files.pythonhosted.org/packages/bc/78/91ae1a3b3b3bed8b893c5d69c07023e151b1c95d79544ad04cf68f596c2f/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773", size = 1692736 },
+ { url = "https://files.pythonhosted.org/packages/77/89/a7ef9c4b4cdb546fcc650ca7f7395aaffbd267f0e1f648a436bec33c9b95/aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62", size = 416418 },
+ { url = "https://files.pythonhosted.org/packages/fc/db/2192489a8a51b52e06627506f8ac8df69ee221de88ab9bdea77aa793aa6a/aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac", size = 442509 },
+ { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666 },
+ { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057 },
+ { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996 },
+ { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367 },
+ { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989 },
+ { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265 },
+ { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841 },
+ { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317 },
+ { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416 },
+ { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514 },
+ { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095 },
+ { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611 },
+ { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576 },
+ { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363 },
+ { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666 },
+ { url = "https://files.pythonhosted.org/packages/49/d1/d8af164f400bad432b63e1ac857d74a09311a8334b0481f2f64b158b50eb/aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9", size = 697982 },
+ { url = "https://files.pythonhosted.org/packages/92/d1/faad3bf9fa4bfd26b95c69fc2e98937d52b1ff44f7e28131855a98d23a17/aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194", size = 460662 },
+ { url = "https://files.pythonhosted.org/packages/db/61/0d71cc66d63909dabc4590f74eba71f91873a77ea52424401c2498d47536/aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f", size = 452950 },
+ { url = "https://files.pythonhosted.org/packages/07/db/6d04bc7fd92784900704e16b745484ef45b77bd04e25f58f6febaadf7983/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104", size = 1665178 },
+ { url = "https://files.pythonhosted.org/packages/54/5c/e95ade9ae29f375411884d9fd98e50535bf9fe316c9feb0f30cd2ac8f508/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff", size = 1717939 },
+ { url = "https://files.pythonhosted.org/packages/6f/1c/1e7d5c5daea9e409ed70f7986001b8c9e3a49a50b28404498d30860edab6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3", size = 1775125 },
+ { url = "https://files.pythonhosted.org/packages/5d/66/890987e44f7d2f33a130e37e01a164168e6aff06fce15217b6eaf14df4f6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1", size = 1677176 },
+ { url = "https://files.pythonhosted.org/packages/8f/dc/e2ba57d7a52df6cdf1072fd5fa9c6301a68e1cd67415f189805d3eeb031d/aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4", size = 1603192 },
+ { url = "https://files.pythonhosted.org/packages/6c/9e/8d08a57de79ca3a358da449405555e668f2c8871a7777ecd2f0e3912c272/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d", size = 1618296 },
+ { url = "https://files.pythonhosted.org/packages/56/51/89822e3ec72db352c32e7fc1c690370e24e231837d9abd056490f3a49886/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87", size = 1616524 },
+ { url = "https://files.pythonhosted.org/packages/2c/fa/e2e6d9398f462ffaa095e84717c1732916a57f1814502929ed67dd7568ef/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2", size = 1685471 },
+ { url = "https://files.pythonhosted.org/packages/ae/5f/6bb976e619ca28a052e2c0ca7b0251ccd893f93d7c24a96abea38e332bf6/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12", size = 1715312 },
+ { url = "https://files.pythonhosted.org/packages/79/c1/756a7e65aa087c7fac724d6c4c038f2faaa2a42fe56dbc1dd62a33ca7213/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5", size = 1672783 },
+ { url = "https://files.pythonhosted.org/packages/73/ba/a6190ebb02176c7f75e6308da31f5d49f6477b651a3dcfaaaca865a298e2/aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d", size = 410229 },
+ { url = "https://files.pythonhosted.org/packages/b8/62/c9fa5bafe03186a0e4699150a7fed9b1e73240996d0d2f0e5f70f3fdf471/aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99", size = 436081 },
]
[[package]]
name = "aiosignal"
-version = "1.3.1"
+version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "frozenlist" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 },
+ { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 },
]
[[package]]
@@ -95,15 +95,16 @@ wheels = [
[[package]]
name = "anyio"
-version = "4.6.2.post1"
+version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 },
+ { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
@@ -117,24 +118,25 @@ wheels = [
[[package]]
name = "asyncclick"
-version = "8.1.7.2"
+version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "colorama", marker = "platform_system == 'Windows'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5e/bf/59d836c3433d7aa07f76c2b95c4eb763195ea8a5d7f9ad3311ed30c2af61/asyncclick-8.1.7.2.tar.gz", hash = "sha256:219ea0f29ccdc1bb4ff43bcab7ce0769ac6d48a04f997b43ec6bee99a222daa0", size = 349073 }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/e1e5fdf1c1bb7e6e614987c120a98d9324bf8edfaa5f5cd16a6235c9d91b/asyncclick-8.1.8.tar.gz", hash = "sha256:0f0eb0f280e04919d67cf71b9fcdfb4db2d9ff7203669c40284485c149578e4c", size = 232900 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/6e/9acdbb25733e1de411663b59abe521bec738e72fe4e85843f6ff8b212832/asyncclick-8.1.7.2-py3-none-any.whl", hash = "sha256:1ab940b04b22cb89b5b400725132b069d01b0c3472a9702c7a2c9d5d007ded02", size = 99191 },
+ { url = "https://files.pythonhosted.org/packages/14/cc/a436f0fc2d04e57a0697e0f87a03b9eaed03ad043d2d5f887f8eebcec95f/asyncclick-8.1.8-py3-none-any.whl", hash = "sha256:eb1ccb44bc767f8f0695d592c7806fdf5bd575605b4ee246ffd5fadbcfdbd7c6", size = 99093 },
+ { url = "https://files.pythonhosted.org/packages/92/c4/ae9e9d25522c6dc96ff167903880a0fe94d7bd31ed999198ee5017d977ed/asyncclick-8.1.8.0-py3-none-any.whl", hash = "sha256:be146a2d8075d4fe372ff4e877f23c8b5af269d16705c1948123b9415f6fd678", size = 99115 },
]
[[package]]
name = "attrs"
-version = "24.2.0"
+version = "24.3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 }
+sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 },
+ { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
]
[[package]]
@@ -148,11 +150,11 @@ wheels = [
[[package]]
name = "certifi"
-version = "2024.8.30"
+version = "2024.12.14"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
+ { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 },
]
[[package]]
@@ -211,56 +213,50 @@ wheels = [
[[package]]
name = "charset-normalizer"
-version = "3.4.0"
+version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
+sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 },
- { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 },
- { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 },
- { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 },
- { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 },
- { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 },
- { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 },
- { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 },
- { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 },
- { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 },
- { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 },
- { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 },
- { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 },
- { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 },
- { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 },
- { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 },
- { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 },
- { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 },
- { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 },
- { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 },
- { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 },
- { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 },
- { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 },
- { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 },
- { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 },
- { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 },
- { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 },
- { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 },
- { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 },
- { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 },
- { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
- { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
- { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
- { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
- { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
- { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
- { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
- { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
- { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
- { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
- { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
- { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
- { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
- { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
- { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
- { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
+ { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
+ { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
+ { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
+ { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
+ { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
+ { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
+ { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
+ { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
+ { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
+ { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
+ { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
+ { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
+ { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
+ { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
+ { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
+ { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
+ { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
+ { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
+ { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
+ { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
+ { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
+ { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
+ { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
+ { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
+ { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
+ { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
+ { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
+ { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
+ { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
+ { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
+ { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
+ { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
+ { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
+ { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
+ { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
+ { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
+ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
+ { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
+ { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
+ { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
[[package]]
@@ -287,50 +283,50 @@ wheels = [
[[package]]
name = "coverage"
-version = "7.6.8"
+version = "7.6.10"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 }
+sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ab/9f/e98211980f6e2f439e251737482aa77906c9b9c507824c71a2ce7eea0402/coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", size = 207093 },
- { url = "https://files.pythonhosted.org/packages/fd/c7/8bab83fb9c20f7f8163c5a20dcb62d591b906a214a6dc6b07413074afc80/coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", size = 207536 },
- { url = "https://files.pythonhosted.org/packages/1e/d6/00243df625f1b282bb25c83ce153ae2c06f8e7a796a8d833e7235337b4d9/coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", size = 239482 },
- { url = "https://files.pythonhosted.org/packages/1e/07/faf04b3eeb55ffc2a6f24b65dffe6e0359ec3b283e6efb5050ea0707446f/coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", size = 236886 },
- { url = "https://files.pythonhosted.org/packages/43/23/c79e497bf4d8fcacd316bebe1d559c765485b8ec23ac4e23025be6bfce09/coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", size = 238749 },
- { url = "https://files.pythonhosted.org/packages/b5/e5/791bae13be3c6451e32ef7af1192e711c6a319f3c597e9b218d148fd0633/coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", size = 237679 },
- { url = "https://files.pythonhosted.org/packages/05/c6/bbfdfb03aada601fb8993ced17468c8c8e0b4aafb3097026e680fabb7ce1/coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", size = 236317 },
- { url = "https://files.pythonhosted.org/packages/67/f9/f8e5a4b2ce96d1b0e83ae6246369eb8437001dc80ec03bb51c87ff557cd8/coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", size = 237084 },
- { url = "https://files.pythonhosted.org/packages/f0/70/b05328901e4debe76e033717e1452d00246c458c44e9dbd893e7619c2967/coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", size = 209638 },
- { url = "https://files.pythonhosted.org/packages/70/55/1efa24f960a2fa9fbc44a9523d3f3c50ceb94dd1e8cd732168ab2dc41b07/coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9", size = 210506 },
- { url = "https://files.pythonhosted.org/packages/76/ce/3edf581c8fe429ed8ced6e6d9ac693c25975ef9093413276dab6ed68a80a/coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", size = 207285 },
- { url = "https://files.pythonhosted.org/packages/09/9c/cf102ab046c9cf8895c3f7aadcde6f489a4b2ec326757e8c6e6581829b5e/coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", size = 207522 },
- { url = "https://files.pythonhosted.org/packages/39/06/42aa6dd13dbfca72e1fd8ffccadbc921b6e75db34545ebab4d955d1e7ad3/coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", size = 240543 },
- { url = "https://files.pythonhosted.org/packages/a0/20/2932971dc215adeca8eeff446266a7fef17a0c238e881ffedebe7bfa0669/coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", size = 237577 },
- { url = "https://files.pythonhosted.org/packages/ac/85/4323ece0cd5452c9522f4b6e5cc461e6c7149a4b1887c9e7a8b1f4e51146/coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", size = 239646 },
- { url = "https://files.pythonhosted.org/packages/77/52/b2537487d8f36241e518e84db6f79e26bc3343b14844366e35b090fae0d4/coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", size = 239128 },
- { url = "https://files.pythonhosted.org/packages/7c/99/7f007762012186547d0ecc3d328da6b6f31a8c99f05dc1e13dcd929918cd/coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", size = 237434 },
- { url = "https://files.pythonhosted.org/packages/97/53/e9b5cf0682a1cab9352adfac73caae0d77ae1d65abc88975d510f7816389/coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", size = 239095 },
- { url = "https://files.pythonhosted.org/packages/0c/50/054f0b464fbae0483217186478eefa2e7df3a79917ed7f1d430b6da2cf0d/coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", size = 209895 },
- { url = "https://files.pythonhosted.org/packages/df/d0/09ba870360a27ecf09e177ca2ff59d4337fc7197b456f22ceff85cffcfa5/coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", size = 210684 },
- { url = "https://files.pythonhosted.org/packages/9a/84/6f0ccf94a098ac3d6d6f236bd3905eeac049a9e0efcd9a63d4feca37ac4b/coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", size = 207313 },
- { url = "https://files.pythonhosted.org/packages/db/2b/e3b3a3a12ebec738c545897ac9f314620470fcbc368cdac88cf14974ba20/coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", size = 207574 },
- { url = "https://files.pythonhosted.org/packages/db/c0/5bf95d42b6a8d21dfce5025ce187f15db57d6460a59b67a95fe8728162f1/coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", size = 240090 },
- { url = "https://files.pythonhosted.org/packages/57/b8/d6fd17d1a8e2b0e1a4e8b9cb1f0f261afd422570735899759c0584236916/coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", size = 237237 },
- { url = "https://files.pythonhosted.org/packages/d4/e4/a91e9bb46809c8b63e68fc5db5c4d567d3423b6691d049a4f950e38fbe9d/coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", size = 239225 },
- { url = "https://files.pythonhosted.org/packages/31/9c/9b99b0591ec4555b7292d271e005f27b465388ce166056c435b288db6a69/coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", size = 238888 },
- { url = "https://files.pythonhosted.org/packages/a6/85/285c2df9a04bc7c31f21fd9d4a24d19e040ec5e2ff06e572af1f6514c9e7/coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", size = 236974 },
- { url = "https://files.pythonhosted.org/packages/cb/a1/95ec8522206f76cdca033bf8bb61fff56429fb414835fc4d34651dfd29fc/coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", size = 238815 },
- { url = "https://files.pythonhosted.org/packages/8d/ac/687e9ba5e6d0979e9dab5c02e01c4f24ac58260ef82d88d3b433b3f84f1e/coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", size = 209957 },
- { url = "https://files.pythonhosted.org/packages/2f/a3/b61cc8e3fcf075293fb0f3dee405748453c5ba28ac02ceb4a87f52bdb105/coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", size = 210711 },
- { url = "https://files.pythonhosted.org/packages/ee/4b/891c8b9acf1b62c85e4a71dac142ab9284e8347409b7355de02e3f38306f/coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", size = 208053 },
- { url = "https://files.pythonhosted.org/packages/18/a9/9e330409b291cc002723d339346452800e78df1ce50774ca439ade1d374f/coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", size = 208329 },
- { url = "https://files.pythonhosted.org/packages/9c/0d/33635fd429f6589c6e1cdfc7bf581aefe4c1792fbff06383f9d37f59db60/coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", size = 251052 },
- { url = "https://files.pythonhosted.org/packages/23/32/8a08da0e46f3830bbb9a5b40614241b2e700f27a9c2889f53122486443ed/coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", size = 246765 },
- { url = "https://files.pythonhosted.org/packages/56/3f/3b86303d2c14350fdb1c6c4dbf9bc76000af2382f42ca1d4d99c6317666e/coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", size = 249125 },
- { url = "https://files.pythonhosted.org/packages/36/cb/c4f081b9023f9fd8646dbc4ef77be0df090263e8f66f4ea47681e0dc2cff/coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", size = 248615 },
- { url = "https://files.pythonhosted.org/packages/32/ee/53bdbf67760928c44b57b2c28a8c0a4bf544f85a9ee129a63ba5c78fdee4/coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", size = 246507 },
- { url = "https://files.pythonhosted.org/packages/57/49/5a57910bd0af6d8e802b4ca65292576d19b54b49f81577fd898505dee075/coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", size = 247785 },
- { url = "https://files.pythonhosted.org/packages/bd/37/e450c9f6b297c79bb9858407396ed3e084dcc22990dd110ab01d5ceb9770/coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", size = 210605 },
- { url = "https://files.pythonhosted.org/packages/44/79/7d0c7dd237c6905018e2936cd1055fe1d42e7eba2ebab3c00f4aad2a27d7/coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", size = 211777 },
+ { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 },
+ { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 },
+ { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 },
+ { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 },
+ { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 },
+ { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 },
+ { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 },
+ { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 },
+ { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 },
+ { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 },
+ { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 },
+ { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 },
+ { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 },
+ { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 },
+ { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 },
+ { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 },
+ { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 },
+ { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 },
+ { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 },
+ { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 },
+ { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 },
+ { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 },
+ { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 },
+ { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 },
+ { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 },
+ { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 },
+ { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 },
+ { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 },
+ { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 },
+ { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 },
+ { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 },
+ { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 },
+ { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 },
+ { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 },
+ { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 },
+ { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 },
+ { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 },
+ { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 },
+ { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 },
+ { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 },
]
[package.optional-dependencies]
@@ -340,31 +336,33 @@ toml = [
[[package]]
name = "cryptography"
-version = "43.0.3"
+version = "44.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 }
+sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 },
- { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 },
- { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 },
- { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 },
- { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 },
- { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 },
- { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 },
- { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 },
- { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 },
- { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 },
- { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 },
- { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 },
- { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 },
- { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 },
- { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 },
- { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 },
- { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 },
- { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 },
+ { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 },
+ { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 },
+ { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 },
+ { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 },
+ { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 },
+ { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 },
+ { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 },
+ { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 },
+ { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 },
+ { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 },
+ { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 },
+ { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 },
+ { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 },
+ { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 },
+ { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 },
+ { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 },
+ { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 },
+ { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 },
+ { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 },
+ { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 },
]
[[package]]
@@ -471,11 +469,11 @@ wheels = [
[[package]]
name = "identify"
-version = "2.6.3"
+version = "2.6.5"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1a/5f/05f0d167be94585d502b4adf8c7af31f1dc0b1c7e14f9938a88fdbbcf4a7/identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02", size = 99179 }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/92/69934b9ef3c31ca2470980423fda3d00f0460ddefdf30a67adf7f17e2e00/identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc", size = 99213 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c9/f5/09644a3ad803fae9eca8efa17e1f2aef380c7f0b02f7ec4e8d446e51d64a/identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd", size = 99049 },
+ { url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 },
]
[[package]]
@@ -519,14 +517,14 @@ wheels = [
[[package]]
name = "jinja2"
-version = "3.1.4"
+version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 }
+sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 },
+ { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
]
[[package]]
@@ -700,30 +698,33 @@ wheels = [
[[package]]
name = "mypy"
-version = "1.13.0"
+version = "1.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 }
+sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 },
- { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 },
- { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 },
- { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 },
- { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 },
- { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 },
- { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 },
- { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 },
- { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 },
- { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 },
- { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 },
- { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 },
- { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 },
- { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 },
- { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 },
- { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 },
+ { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 },
+ { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 },
+ { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 },
+ { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 },
+ { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 },
+ { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 },
+ { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 },
+ { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 },
+ { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 },
+ { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 },
+ { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 },
+ { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 },
+ { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 },
+ { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 },
+ { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 },
+ { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 },
+ { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 },
+ { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 },
+ { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 },
]
[[package]]
@@ -763,45 +764,45 @@ wheels = [
[[package]]
name = "orjson"
-version = "3.10.12"
+version = "3.10.13"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e0/04/bb9f72987e7f62fb591d6c880c0caaa16238e4e530cbc3bdc84a7372d75f/orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff", size = 5438647 }
+sdist = { url = "https://files.pythonhosted.org/packages/45/0b/8c7eaf1e2152f1e0fb28ae7b22e2b35a6b1992953a1ebe0371ba4d41d3ad/orjson-3.10.13.tar.gz", hash = "sha256:eb9bfb14ab8f68d9d9492d4817ae497788a15fd7da72e14dfabc289c3bb088ec", size = 5438389 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d3/48/7c3cd094488f5a3bc58488555244609a8c4d105bc02f2b77e509debf0450/orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74", size = 248687 },
- { url = "https://files.pythonhosted.org/packages/ff/90/e55f0e25c7fdd1f82551fe787f85df6f378170caca863c04c810cd8f2730/orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23", size = 136953 },
- { url = "https://files.pythonhosted.org/packages/2a/b3/109c020cf7fee747d400de53b43b183ca9d3ebda3906ad0b858eb5479718/orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252", size = 149090 },
- { url = "https://files.pythonhosted.org/packages/96/d4/35c0275dc1350707d182a1b5da16d1184b9439848060af541285407f18f9/orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef", size = 140480 },
- { url = "https://files.pythonhosted.org/packages/3b/79/f863ff460c291ad2d882cc3b580cc444bd4ec60c9df55f6901e6c9a3f519/orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252", size = 156564 },
- { url = "https://files.pythonhosted.org/packages/98/7e/8d5835449ddd873424ee7b1c4ba73a0369c1055750990d824081652874d6/orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4", size = 131279 },
- { url = "https://files.pythonhosted.org/packages/46/f5/d34595b6d7f4f984c6fef289269a7f98abcdc2445ebdf90e9273487dda6b/orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae", size = 139764 },
- { url = "https://files.pythonhosted.org/packages/b3/5b/ee6e9ddeab54a7b7806768151c2090a2d36025bc346a944f51cf172ef7f7/orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b", size = 131915 },
- { url = "https://files.pythonhosted.org/packages/c4/45/febee5951aef6db5cd8cdb260548101d7ece0ca9d4ddadadf1766306b7a4/orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da", size = 415783 },
- { url = "https://files.pythonhosted.org/packages/27/a5/5a8569e49f3a6c093bee954a3de95062a231196f59e59df13a48e2420081/orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07", size = 142387 },
- { url = "https://files.pythonhosted.org/packages/6e/05/02550fb38c5bf758f3994f55401233a2ef304e175f473f2ac6dbf464cc8b/orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd", size = 130664 },
- { url = "https://files.pythonhosted.org/packages/8c/f4/ba31019d0646ce51f7ac75af6dabf98fd89dbf8ad87a9086da34710738e7/orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79", size = 143623 },
- { url = "https://files.pythonhosted.org/packages/83/fe/babf08842b989acf4c46103fefbd7301f026423fab47e6f3ba07b54d7837/orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8", size = 135074 },
- { url = "https://files.pythonhosted.org/packages/a1/2f/989adcafad49afb535da56b95d8f87d82e748548b2a86003ac129314079c/orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d", size = 248678 },
- { url = "https://files.pythonhosted.org/packages/69/b9/8c075e21a50c387649db262b618ebb7e4d40f4197b949c146fc225dd23da/orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f", size = 136763 },
- { url = "https://files.pythonhosted.org/packages/87/d3/78edf10b4ab14c19f6d918cf46a145818f4aca2b5a1773c894c5490d3a4c/orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70", size = 149137 },
- { url = "https://files.pythonhosted.org/packages/16/81/5db8852bdf990a0ddc997fa8f16b80895b8cc77c0fe3701569ed2b4b9e78/orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69", size = 140567 },
- { url = "https://files.pythonhosted.org/packages/fa/a6/9ce1e3e3db918512efadad489630c25841eb148513d21dab96f6b4157fa1/orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9", size = 156620 },
- { url = "https://files.pythonhosted.org/packages/47/d4/05133d6bea24e292d2f7628b1e19986554f7d97b6412b3e51d812e38db2d/orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192", size = 131555 },
- { url = "https://files.pythonhosted.org/packages/b9/7a/b3fbffda8743135c7811e95dc2ab7cdbc5f04999b83c2957d046f1b3fac9/orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559", size = 139743 },
- { url = "https://files.pythonhosted.org/packages/b5/13/95bbcc9a6584aa083da5ce5004ce3d59ea362a542a0b0938d884fd8790b6/orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc", size = 131733 },
- { url = "https://files.pythonhosted.org/packages/e8/29/dddbb2ea6e7af426fcc3da65a370618a88141de75c6603313d70768d1df1/orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f", size = 415788 },
- { url = "https://files.pythonhosted.org/packages/53/df/4aea59324ac539975919b4705ee086aced38e351a6eb3eea0f5071dd5661/orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be", size = 142347 },
- { url = "https://files.pythonhosted.org/packages/55/55/a52d83d7c49f8ff44e0daab10554490447d6c658771569e1c662aa7057fe/orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c", size = 130829 },
- { url = "https://files.pythonhosted.org/packages/a1/8b/b1beb1624dd4adf7d72e2d9b73c4b529e7851c0c754f17858ea13e368b33/orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708", size = 143659 },
- { url = "https://files.pythonhosted.org/packages/13/91/634c9cd0bfc6a857fc8fab9bf1a1bd9f7f3345e0d6ca5c3d4569ceb6dcfa/orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb", size = 135221 },
- { url = "https://files.pythonhosted.org/packages/1b/bb/3f560735f46fa6f875a9d7c4c2171a58cfb19f56a633d5ad5037a924f35f/orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543", size = 248662 },
- { url = "https://files.pythonhosted.org/packages/a3/df/54817902350636cc9270db20486442ab0e4db33b38555300a1159b439d16/orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296", size = 126055 },
- { url = "https://files.pythonhosted.org/packages/2e/77/55835914894e00332601a74540840f7665e81f20b3e2b9a97614af8565ed/orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e", size = 131507 },
- { url = "https://files.pythonhosted.org/packages/33/9e/b91288361898e3158062a876b5013c519a5d13e692ac7686e3486c4133ab/orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f", size = 131686 },
- { url = "https://files.pythonhosted.org/packages/b2/15/08ce117d60a4d2d3fd24e6b21db463139a658e9f52d22c9c30af279b4187/orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e", size = 415710 },
- { url = "https://files.pythonhosted.org/packages/71/af/c09da5ed58f9c002cf83adff7a4cdf3e6cee742aa9723395f8dcdb397233/orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6", size = 142305 },
- { url = "https://files.pythonhosted.org/packages/17/d1/8612038d44f33fae231e9ba480d273bac2b0383ce9e77cb06bede1224ae3/orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e", size = 130815 },
- { url = "https://files.pythonhosted.org/packages/67/2c/d5f87834be3591555cfaf9aecdf28f480a6f0b4afeaac53bad534bf9518f/orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc", size = 143664 },
- { url = "https://files.pythonhosted.org/packages/6a/05/7d768fa3ca23c9b3e1e09117abeded1501119f1d8de0ab722938c91ab25d/orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825", size = 134944 },
+ { url = "https://files.pythonhosted.org/packages/01/44/7a047e47779953e3f657a612ad36f71a0bca02cf57ff490c427e22b01833/orjson-3.10.13-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a36c0d48d2f084c800763473020a12976996f1109e2fcb66cfea442fdf88047f", size = 248732 },
+ { url = "https://files.pythonhosted.org/packages/d6/e9/54976977aaacc5030fdd8012479638bb8d4e2a16519b516ac2bd03a48eab/orjson-3.10.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0065896f85d9497990731dfd4a9991a45b0a524baec42ef0a63c34630ee26fd6", size = 136954 },
+ { url = "https://files.pythonhosted.org/packages/7f/a7/663fb04e031d5c80a348aeb7271c6042d13f80393c4951b8801a703b89c0/orjson-3.10.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92b4ec30d6025a9dcdfe0df77063cbce238c08d0404471ed7a79f309364a3d19", size = 149101 },
+ { url = "https://files.pythonhosted.org/packages/f9/f1/5f2a4bf7525ef4acf48902d2df2bcc1c5aa38f6cc17ee0729a1d3e110ddb/orjson-3.10.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a94542d12271c30044dadad1125ee060e7a2048b6c7034e432e116077e1d13d2", size = 140445 },
+ { url = "https://files.pythonhosted.org/packages/12/d3/e68afa1db9860880e59260348b54c0518d8dfe2297e932f8e333ace878fa/orjson-3.10.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3723e137772639af8adb68230f2aa4bcb27c48b3335b1b1e2d49328fed5e244c", size = 156530 },
+ { url = "https://files.pythonhosted.org/packages/77/ee/492b198c77b9985ae28e0c6b8092c2994cd18d6be40dc7cb7f9a385b7096/orjson-3.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f00c7fb18843bad2ac42dc1ce6dd214a083c53f1e324a0fd1c8137c6436269b", size = 131260 },
+ { url = "https://files.pythonhosted.org/packages/57/d2/5167cc1ccbe56bacdd9fc79e6a3276cba6aa90057305e8485db58b8250c4/orjson-3.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0e2759d3172300b2f892dee85500b22fca5ac49e0c42cfff101aaf9c12ac9617", size = 139821 },
+ { url = "https://files.pythonhosted.org/packages/74/f0/c1cf568e0f90d812e00c77da2db04a13e94afe639665b9a09c271456dc41/orjson-3.10.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee948c6c01f6b337589c88f8e0bb11e78d32a15848b8b53d3f3b6fea48842c12", size = 131904 },
+ { url = "https://files.pythonhosted.org/packages/55/7d/a611542afbbacca4693a2319744944134df62957a1f206303d5b3160e349/orjson-3.10.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa6fe68f0981fba0d4bf9cdc666d297a7cdba0f1b380dcd075a9a3dd5649a69e", size = 415733 },
+ { url = "https://files.pythonhosted.org/packages/64/3f/e8182716695cd8d5ebec49d283645b8c7b1de7ed1c27db2891b6957e71f6/orjson-3.10.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbcd7aad6bcff258f6896abfbc177d54d9b18149c4c561114f47ebfe74ae6bfd", size = 142456 },
+ { url = "https://files.pythonhosted.org/packages/dc/10/e4b40f15be7e4e991737d77062399c7f67da9b7e3bc28bbcb25de1717df3/orjson-3.10.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2149e2fcd084c3fd584881c7f9d7f9e5ad1e2e006609d8b80649655e0d52cd02", size = 130676 },
+ { url = "https://files.pythonhosted.org/packages/ad/b1/8b9fb36d470fe8ff99727972c77846673ebc962cb09a5af578804f9f2408/orjson-3.10.13-cp311-cp311-win32.whl", hash = "sha256:89367767ed27b33c25c026696507c76e3d01958406f51d3a2239fe9e91959df2", size = 143672 },
+ { url = "https://files.pythonhosted.org/packages/b5/15/90b3711f40d27aff80dd42c1eec2f0ed704a1fa47eef7120350e2797892d/orjson-3.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:dca1d20f1af0daff511f6e26a27354a424f0b5cf00e04280279316df0f604a6f", size = 135082 },
+ { url = "https://files.pythonhosted.org/packages/35/84/adf8842cf36904e6200acff76156862d48d39705054c1e7c5fa98fe14417/orjson-3.10.13-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a3614b00621c77f3f6487792238f9ed1dd8a42f2ec0e6540ee34c2d4e6db813a", size = 248778 },
+ { url = "https://files.pythonhosted.org/packages/69/2f/22ac0c5f46748e9810287a5abaeabdd67f1120a74140db7d529582c92342/orjson-3.10.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c976bad3996aa027cd3aef78aa57873f3c959b6c38719de9724b71bdc7bd14b", size = 136759 },
+ { url = "https://files.pythonhosted.org/packages/39/67/6f05de77dd383cb623e2807bceae13f136e9f179cd32633b7a27454e953f/orjson-3.10.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f74d878d1efb97a930b8a9f9898890067707d683eb5c7e20730030ecb3fb930", size = 149123 },
+ { url = "https://files.pythonhosted.org/packages/f8/5c/b5e144e9adbb1dc7d1fdf54af9510756d09b65081806f905d300a926a755/orjson-3.10.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33ef84f7e9513fb13b3999c2a64b9ca9c8143f3da9722fbf9c9ce51ce0d8076e", size = 140557 },
+ { url = "https://files.pythonhosted.org/packages/91/fd/7bdbc0aa374d49cdb917ee51c80851c99889494be81d5e7ec9f5f9cbe149/orjson-3.10.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd2bcde107221bb9c2fa0c4aaba735a537225104173d7e19cf73f70b3126c993", size = 156626 },
+ { url = "https://files.pythonhosted.org/packages/48/90/e583d6e29937ec30a164f1d86a0439c1a2477b5aae9f55d94b37a4f5b5f0/orjson-3.10.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:064b9dbb0217fd64a8d016a8929f2fae6f3312d55ab3036b00b1d17399ab2f3e", size = 131551 },
+ { url = "https://files.pythonhosted.org/packages/47/0b/838c00ec7f048527aa0382299cd178bbe07c2cb1024b3111883e85d56d1f/orjson-3.10.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0044b0b8c85a565e7c3ce0a72acc5d35cda60793edf871ed94711e712cb637d", size = 139790 },
+ { url = "https://files.pythonhosted.org/packages/ac/90/df06ac390f319a61d55a7a4efacb5d7082859f6ea33f0fdd5181ad0dde0c/orjson-3.10.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7184f608ad563032e398f311910bc536e62b9fbdca2041be889afcbc39500de8", size = 131717 },
+ { url = "https://files.pythonhosted.org/packages/ea/68/eafb5e2fc84aafccfbd0e9e0552ff297ef5f9b23c7f2600cc374095a50de/orjson-3.10.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d36f689e7e1b9b6fb39dbdebc16a6f07cbe994d3644fb1c22953020fc575935f", size = 415690 },
+ { url = "https://files.pythonhosted.org/packages/b8/cf/aa93b48801b2e42da223ef5a99b3e4970b02e7abea8509dd2a6a083e27fa/orjson-3.10.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54433e421618cd5873e51c0e9d0b9fb35f7bf76eb31c8eab20b3595bb713cd3d", size = 142396 },
+ { url = "https://files.pythonhosted.org/packages/8b/50/fb1a7060b79231c60a688037c2c8e9fe289b5a4378ec1f32cf8d33d9adf8/orjson-3.10.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e1ba0c5857dd743438acecc1cd0e1adf83f0a81fee558e32b2b36f89e40cee8b", size = 130842 },
+ { url = "https://files.pythonhosted.org/packages/94/e6/44067052e28a13176da874ca53419b43cf0f6f01f4bf0539f2f70d8eacf6/orjson-3.10.13-cp312-cp312-win32.whl", hash = "sha256:a42b9fe4b0114b51eb5cdf9887d8c94447bc59df6dbb9c5884434eab947888d8", size = 143773 },
+ { url = "https://files.pythonhosted.org/packages/f2/7d/510939d1b7f8ba387849e83666e898f214f38baa46c5efde94561453974d/orjson-3.10.13-cp312-cp312-win_amd64.whl", hash = "sha256:3a7df63076435f39ec024bdfeb4c9767ebe7b49abc4949068d61cf4857fa6d6c", size = 135234 },
+ { url = "https://files.pythonhosted.org/packages/ef/42/482fced9a135c798f31e1088f608fa16735fdc484eb8ffdd29aa32d4e842/orjson-3.10.13-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2cdaf8b028a976ebab837a2c27b82810f7fc76ed9fb243755ba650cc83d07730", size = 248726 },
+ { url = "https://files.pythonhosted.org/packages/00/e7/6345653906ee6d2d6eabb767cdc4482c7809572dbda59224f40e48931efa/orjson-3.10.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a946796e390cbb803e069472de37f192b7a80f4ac82e16d6eb9909d9e39d56", size = 126032 },
+ { url = "https://files.pythonhosted.org/packages/ad/b8/0d2a2c739458ff7f9917a132225365d72d18f4b65c50cb8ebb5afb6fe184/orjson-3.10.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d64f1db5ecbc21eb83097e5236d6ab7e86092c1cd4c216c02533332951afc", size = 131547 },
+ { url = "https://files.pythonhosted.org/packages/8d/ac/a1dc389cf364d576cf587a6f78dac6c905c5cac31b9dbd063bbb24335bf7/orjson-3.10.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:711878da48f89df194edd2ba603ad42e7afed74abcd2bac164685e7ec15f96de", size = 131682 },
+ { url = "https://files.pythonhosted.org/packages/43/6c/debab76b830aba6449ec8a75ac77edebb0e7decff63eb3ecfb2cf6340a2e/orjson-3.10.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cf16f06cb77ce8baf844bc222dbcb03838f61d0abda2c3341400c2b7604e436e", size = 415621 },
+ { url = "https://files.pythonhosted.org/packages/c2/32/106e605db5369a6717036065e2b41ac52bd0d2712962edb3e026a452dc07/orjson-3.10.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8257c3fb8dd7b0b446b5e87bf85a28e4071ac50f8c04b6ce2d38cb4abd7dff57", size = 142388 },
+ { url = "https://files.pythonhosted.org/packages/a3/02/6b2103898d60c2565bf97abffdf3a4cf338920b9feb55eec1fd791ab10ee/orjson-3.10.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9c3a87abe6f849a4a7ac8a8a1dede6320a4303d5304006b90da7a3cd2b70d2c", size = 130825 },
+ { url = "https://files.pythonhosted.org/packages/87/7c/db115e2380435da569732999d5c4c9b9868efe72e063493cb73c36bb649a/orjson-3.10.13-cp313-cp313-win32.whl", hash = "sha256:527afb6ddb0fa3fe02f5d9fba4920d9d95da58917826a9be93e0242da8abe94a", size = 143723 },
+ { url = "https://files.pythonhosted.org/packages/cc/5e/c2b74a0b38ec561a322d8946663924556c1f967df2eefe1b9e0b98a33950/orjson-3.10.13-cp313-cp313-win_amd64.whl", hash = "sha256:b5f7c298d4b935b222f52d6c7f2ba5eafb59d690d9a3840b7b5c5cda97f6ec5c", size = 134968 },
]
[[package]]
@@ -870,59 +871,59 @@ wheels = [
[[package]]
name = "propcache"
-version = "0.2.0"
+version = "0.2.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 }
+sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e0/1c/71eec730e12aec6511e702ad0cd73c2872eccb7cad39de8ba3ba9de693ef/propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", size = 80811 },
- { url = "https://files.pythonhosted.org/packages/89/c3/7e94009f9a4934c48a371632197406a8860b9f08e3f7f7d922ab69e57a41/propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", size = 46365 },
- { url = "https://files.pythonhosted.org/packages/c0/1d/c700d16d1d6903aeab28372fe9999762f074b80b96a0ccc953175b858743/propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", size = 45602 },
- { url = "https://files.pythonhosted.org/packages/2e/5e/4a3e96380805bf742712e39a4534689f4cddf5fa2d3a93f22e9fd8001b23/propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", size = 236161 },
- { url = "https://files.pythonhosted.org/packages/a5/85/90132481183d1436dff6e29f4fa81b891afb6cb89a7306f32ac500a25932/propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", size = 244938 },
- { url = "https://files.pythonhosted.org/packages/4a/89/c893533cb45c79c970834274e2d0f6d64383ec740be631b6a0a1d2b4ddc0/propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", size = 243576 },
- { url = "https://files.pythonhosted.org/packages/8c/56/98c2054c8526331a05f205bf45cbb2cda4e58e56df70e76d6a509e5d6ec6/propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", size = 236011 },
- { url = "https://files.pythonhosted.org/packages/2d/0c/8b8b9f8a6e1abd869c0fa79b907228e7abb966919047d294ef5df0d136cf/propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504", size = 224834 },
- { url = "https://files.pythonhosted.org/packages/18/bb/397d05a7298b7711b90e13108db697732325cafdcd8484c894885c1bf109/propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", size = 224946 },
- { url = "https://files.pythonhosted.org/packages/25/19/4fc08dac19297ac58135c03770b42377be211622fd0147f015f78d47cd31/propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", size = 217280 },
- { url = "https://files.pythonhosted.org/packages/7e/76/c79276a43df2096ce2aba07ce47576832b1174c0c480fe6b04bd70120e59/propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", size = 220088 },
- { url = "https://files.pythonhosted.org/packages/c3/9a/8a8cf428a91b1336b883f09c8b884e1734c87f724d74b917129a24fe2093/propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", size = 233008 },
- { url = "https://files.pythonhosted.org/packages/25/7b/768a8969abd447d5f0f3333df85c6a5d94982a1bc9a89c53c154bf7a8b11/propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", size = 237719 },
- { url = "https://files.pythonhosted.org/packages/ed/0d/e5d68ccc7976ef8b57d80613ac07bbaf0614d43f4750cf953f0168ef114f/propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", size = 227729 },
- { url = "https://files.pythonhosted.org/packages/05/64/17eb2796e2d1c3d0c431dc5f40078d7282f4645af0bb4da9097fbb628c6c/propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", size = 40473 },
- { url = "https://files.pythonhosted.org/packages/83/c5/e89fc428ccdc897ade08cd7605f174c69390147526627a7650fb883e0cd0/propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", size = 44921 },
- { url = "https://files.pythonhosted.org/packages/7c/46/a41ca1097769fc548fc9216ec4c1471b772cc39720eb47ed7e38ef0006a9/propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", size = 80800 },
- { url = "https://files.pythonhosted.org/packages/75/4f/93df46aab9cc473498ff56be39b5f6ee1e33529223d7a4d8c0a6101a9ba2/propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", size = 46443 },
- { url = "https://files.pythonhosted.org/packages/0b/17/308acc6aee65d0f9a8375e36c4807ac6605d1f38074b1581bd4042b9fb37/propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", size = 45676 },
- { url = "https://files.pythonhosted.org/packages/65/44/626599d2854d6c1d4530b9a05e7ff2ee22b790358334b475ed7c89f7d625/propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", size = 246191 },
- { url = "https://files.pythonhosted.org/packages/f2/df/5d996d7cb18df076debae7d76ac3da085c0575a9f2be6b1f707fe227b54c/propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", size = 251791 },
- { url = "https://files.pythonhosted.org/packages/2e/6d/9f91e5dde8b1f662f6dd4dff36098ed22a1ef4e08e1316f05f4758f1576c/propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", size = 253434 },
- { url = "https://files.pythonhosted.org/packages/3c/e9/1b54b7e26f50b3e0497cd13d3483d781d284452c2c50dd2a615a92a087a3/propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", size = 248150 },
- { url = "https://files.pythonhosted.org/packages/a7/ef/a35bf191c8038fe3ce9a414b907371c81d102384eda5dbafe6f4dce0cf9b/propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", size = 233568 },
- { url = "https://files.pythonhosted.org/packages/97/d9/d00bb9277a9165a5e6d60f2142cd1a38a750045c9c12e47ae087f686d781/propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", size = 229874 },
- { url = "https://files.pythonhosted.org/packages/8e/78/c123cf22469bdc4b18efb78893e69c70a8b16de88e6160b69ca6bdd88b5d/propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", size = 225857 },
- { url = "https://files.pythonhosted.org/packages/31/1b/fd6b2f1f36d028820d35475be78859d8c89c8f091ad30e377ac49fd66359/propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", size = 227604 },
- { url = "https://files.pythonhosted.org/packages/99/36/b07be976edf77a07233ba712e53262937625af02154353171716894a86a6/propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", size = 238430 },
- { url = "https://files.pythonhosted.org/packages/0d/64/5822f496c9010e3966e934a011ac08cac8734561842bc7c1f65586e0683c/propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", size = 244814 },
- { url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 },
- { url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 },
- { url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 },
- { url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 },
- { url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 },
- { url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 },
- { url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 },
- { url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 },
- { url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 },
- { url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 },
- { url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 },
- { url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 },
- { url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 },
- { url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 },
- { url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 },
- { url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 },
- { url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 },
- { url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 },
- { url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 },
- { url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 },
+ { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 },
+ { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 },
+ { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 },
+ { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 },
+ { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 },
+ { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 },
+ { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 },
+ { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 },
+ { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 },
+ { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 },
+ { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 },
+ { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 },
+ { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 },
+ { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 },
+ { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 },
+ { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 },
+ { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 },
+ { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 },
+ { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 },
+ { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 },
+ { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 },
+ { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 },
+ { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 },
+ { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 },
+ { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 },
+ { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 },
+ { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 },
+ { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 },
+ { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 },
+ { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 },
+ { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 },
+ { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 },
+ { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 },
+ { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 },
+ { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 },
+ { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 },
+ { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 },
+ { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 },
+ { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 },
+ { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 },
+ { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 },
+ { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 },
+ { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 },
+ { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 },
+ { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 },
+ { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 },
+ { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 },
+ { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 },
+ { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 },
]
[[package]]
@@ -951,16 +952,16 @@ wheels = [
[[package]]
name = "pygments"
-version = "2.18.0"
+version = "2.19.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 }
+sdist = { url = "https://files.pythonhosted.org/packages/d3/c0/9c9832e5be227c40e1ce774d493065f83a91d6430baa7e372094e9683a45/pygments-2.19.0.tar.gz", hash = "sha256:afc4146269910d4bdfabcd27c24923137a74d562a23a320a41a55ad303e19783", size = 4967733 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 },
+ { url = "https://files.pythonhosted.org/packages/20/dc/fde3e7ac4d279a331676829af4afafd113b34272393d73f610e8f0329221/pygments-2.19.0-py3-none-any.whl", hash = "sha256:4755e6e64d22161d5b61432c0600c923c5927214e7c956e31c23923c89251a9b", size = 1225305 },
]
[[package]]
name = "pytest"
-version = "8.3.3"
+version = "8.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -968,21 +969,21 @@ dependencies = [
{ name = "packaging" },
{ name = "pluggy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 }
+sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 },
+ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
]
[[package]]
name = "pytest-asyncio"
-version = "0.24.0"
+version = "0.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/04/0477a4bdd176ad678d148c075f43620b3f7a060ff61c7da48500b1fa8a75/pytest_asyncio-0.25.1.tar.gz", hash = "sha256:79be8a72384b0c917677e00daa711e07db15259f4d23203c59012bcd989d4aee", size = 53760 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 },
+ { url = "https://files.pythonhosted.org/packages/81/fb/efc7226b384befd98d0e00d8c4390ad57f33c8fde00094b85c5e07897def/pytest_asyncio-0.25.1-py3-none-any.whl", hash = "sha256:c84878849ec63ff2ca509423616e071ef9cd8cc93c053aa33b5b8fb70a990671", size = 19357 },
]
[[package]]
@@ -1000,15 +1001,15 @@ wheels = [
[[package]]
name = "pytest-freezer"
-version = "0.4.8"
+version = "0.4.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "freezegun" },
{ name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/69/fa/a93d40dd50f712c276a5a15f9c075bee932cc4d28c376e60b4a35904976d/pytest_freezer-0.4.8.tar.gz", hash = "sha256:8ee2f724b3ff3540523fa355958a22e6f4c1c819928b78a7a183ae4248ce6ee6", size = 3212 }
+sdist = { url = "https://files.pythonhosted.org/packages/81/f0/98dcbc5324064360b19850b14c84cea9ca50785d921741dbfc442346e925/pytest_freezer-0.4.9.tar.gz", hash = "sha256:21bf16bc9cc46bf98f94382c4b5c3c389be7056ff0be33029111ae11b3f1c82a", size = 3177 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d8/4e/ba488639516a341810aeaeb4b32b70abb0923e53f7c4d14d673dc114d35a/pytest_freezer-0.4.8-py3-none-any.whl", hash = "sha256:644ce7ddb8ba52b92a1df0a80a699bad2b93514c55cf92e9f2517b68ebe74814", size = 3228 },
+ { url = "https://files.pythonhosted.org/packages/c1/e9/30252bc05bcf67200a17f4f0b4cc7598f0a68df4fa9fa356193aa899f145/pytest_freezer-0.4.9-py3-none-any.whl", hash = "sha256:8b6c50523b7d4aec4590b52bfa5ff766d772ce506e2bf4846c88041ea9ccae59", size = 3192 },
]
[[package]]
@@ -1088,7 +1089,7 @@ wheels = [
[[package]]
name = "python-kasa"
-version = "0.8.0"
+version = "0.9.1"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -1265,11 +1266,11 @@ wheels = [
[[package]]
name = "six"
-version = "1.16.0"
+version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
@@ -1381,14 +1382,14 @@ wheels = [
[[package]]
name = "sphinxcontrib-programoutput"
-version = "0.17"
+version = "0.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/49/fe/8a6d8763674b3d3814a6008a83eb8002b6da188710dd7f4654ec77b4a8ac/sphinxcontrib-programoutput-0.17.tar.gz", hash = "sha256:300ee9b8caee8355d25cc74b4d1c7efd12e608d2ad165e3141d31e6fbc152b7f", size = 24067 }
+sdist = { url = "https://files.pythonhosted.org/packages/3f/c0/834af2290f8477213ec0dd60e90104f5644aa0c37b1a0d6f0a2b5efe03c4/sphinxcontrib_programoutput-0.18.tar.gz", hash = "sha256:09e68b6411d937a80b6085f4fdeaa42e0dc5555480385938465f410589d2eed8", size = 26333 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/30/ee/b7be4b3f45f4e36bfa6c444cd234098e0d09880379c67a43e6bb9ab99a86/sphinxcontrib_programoutput-0.17-py2.py3-none-any.whl", hash = "sha256:0ef1c1d9159dbe7103077748214305eb4e0138e861feb71c0c346afc5fe97f84", size = 22131 },
+ { url = "https://files.pythonhosted.org/packages/04/2c/7aec6e0580f666d4f61474a50c4995a98abfff27d827f0e7bc8c4fa528f5/sphinxcontrib_programoutput-0.18-py3-none-any.whl", hash = "sha256:8a651bc85de69a808a064ff0e48d06c12b9347da4fe5fdb1e94914b01e1b0c36", size = 20346 },
]
[[package]]
@@ -1429,11 +1430,41 @@ wheels = [
[[package]]
name = "tomli"
-version = "2.1.0"
+version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1e/e4/1b6cbcc82d8832dd0ce34767d5c560df8a3547ad8cbc427f34601415930a/tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", size = 16622 }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/de/f7/4da0ffe1892122c9ea096c57f64c2753ae5dd3ce85488802d11b0992cc6d/tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391", size = 13750 },
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
]
[[package]]
@@ -1456,25 +1487,25 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.2.3"
+version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
+ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
[[package]]
name = "virtualenv"
-version = "20.28.0"
+version = "20.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/bf/75/53316a5a8050069228a2f6d11f32046cfa94fbb6cc3f08703f59b873de2e/virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa", size = 7650368 }
+sdist = { url = "https://files.pythonhosted.org/packages/50/39/689abee4adc85aad2af8174bb195a819d0be064bf55fcc73b49d2b28ae77/virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329", size = 7650532 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/10/f9/0919cf6f1432a8c4baa62511f8f8da8225432d22e83e3476f5be1a1edc6e/virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", size = 4276702 },
+ { url = "https://files.pythonhosted.org/packages/51/8f/dfb257ca6b4e27cb990f1631142361e4712badab8e3ca8dc134d96111515/virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb", size = 4276719 },
]
[[package]]
@@ -1506,62 +1537,62 @@ wheels = [
[[package]]
name = "yarl"
-version = "1.18.0"
+version = "1.18.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5e/4b/53db4ecad4d54535aff3dfda1f00d6363d79455f62b11b8ca97b82746bd2/yarl-1.18.0.tar.gz", hash = "sha256:20d95535e7d833889982bfe7cc321b7f63bf8879788fee982c76ae2b24cfb715", size = 180098 }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/06/45/6ad7135d1c4ad3a6a49e2c37dc78a1805a7871879c03c3495d64c9605d49/yarl-1.18.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b8e8c516dc4e1a51d86ac975b0350735007e554c962281c432eaa5822aa9765c", size = 141283 },
- { url = "https://files.pythonhosted.org/packages/45/6d/24b70ae33107d6eba303ed0ebfdf1164fe2219656e7594ca58628ebc0f1d/yarl-1.18.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e6b4466714a73f5251d84b471475850954f1fa6acce4d3f404da1d55d644c34", size = 94082 },
- { url = "https://files.pythonhosted.org/packages/8a/0e/da720989be11b662ca847ace58f468b52310a9b03e52ac62c144755f9d75/yarl-1.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c893f8c1a6d48b25961e00922724732d00b39de8bb0b451307482dc87bddcd74", size = 92017 },
- { url = "https://files.pythonhosted.org/packages/f5/76/e5c91681fa54658943cb88673fb19b3355c3a8ae911a33a2621b6320990d/yarl-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13aaf2bdbc8c86ddce48626b15f4987f22e80d898818d735b20bd58f17292ee8", size = 340359 },
- { url = "https://files.pythonhosted.org/packages/cf/77/02cf72f09dea20980dea4ebe40dfb2c24916b864aec869a19f715428e0f0/yarl-1.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd21c0128e301851de51bc607b0a6da50e82dc34e9601f4b508d08cc89ee7929", size = 356336 },
- { url = "https://files.pythonhosted.org/packages/17/66/83a88d04e4fc243dd26109f3e3d6412f67819ab1142dadbce49706ef4df4/yarl-1.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205de377bd23365cd85562c9c6c33844050a93661640fda38e0567d2826b50df", size = 353730 },
- { url = "https://files.pythonhosted.org/packages/76/77/0b205a532d22756ab250ab21924d362f910a23d641c82faec1c4ad7f6077/yarl-1.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed69af4fe2a0949b1ea1d012bf065c77b4c7822bad4737f17807af2adb15a73c", size = 343882 },
- { url = "https://files.pythonhosted.org/packages/0b/47/2081ddce3da6096889c3947bdc21907d0fa15939909b10219254fe116841/yarl-1.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e1c18890091aa3cc8a77967943476b729dc2016f4cfe11e45d89b12519d4a93", size = 335873 },
- { url = "https://files.pythonhosted.org/packages/25/3c/437304394494e757ae927c9a81bacc4bcdf7351a1d4e811d95b02cb6dbae/yarl-1.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91b8fb9427e33f83ca2ba9501221ffaac1ecf0407f758c4d2f283c523da185ee", size = 347725 },
- { url = "https://files.pythonhosted.org/packages/c6/fb/fa6c642bc052fbe6370ed5da765579650510157dea354fe9e8177c3bc34a/yarl-1.18.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:536a7a8a53b75b2e98ff96edb2dfb91a26b81c4fed82782035767db5a465be46", size = 346161 },
- { url = "https://files.pythonhosted.org/packages/b0/09/8c0cf68a0fcfe3b060c9e5857bb35735bc72a4cf4075043632c636d007e9/yarl-1.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a64619a9c47c25582190af38e9eb382279ad42e1f06034f14d794670796016c0", size = 349924 },
- { url = "https://files.pythonhosted.org/packages/bf/4b/1efe10fd51e2cedf53195d688fa270efbcd64a015c61d029d49c20bf0af7/yarl-1.18.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c73a6bbc97ba1b5a0c3c992ae93d721c395bdbb120492759b94cc1ac71bc6350", size = 361865 },
- { url = "https://files.pythonhosted.org/packages/0b/1b/2b5efd6df06bf938f7e154dee8e2ab22d148f3311a92bf4da642aaaf2fc5/yarl-1.18.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a173401d7821a2a81c7b47d4e7d5c4021375a1441af0c58611c1957445055056", size = 366030 },
- { url = "https://files.pythonhosted.org/packages/f8/db/786a5684f79278e62271038a698f56a51960f9e643be5d3eff82712f0b1c/yarl-1.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7520e799b1f84e095cce919bd6c23c9d49472deeef25fe1ef960b04cca51c3fc", size = 358902 },
- { url = "https://files.pythonhosted.org/packages/91/2f/437d0de062f1a3e3cb17573971b3832232443241133580c2ba3da5001d06/yarl-1.18.0-cp311-cp311-win32.whl", hash = "sha256:c4cb992d8090d5ae5f7afa6754d7211c578be0c45f54d3d94f7781c495d56716", size = 84138 },
- { url = "https://files.pythonhosted.org/packages/9d/85/035719a9266bce85ecde820aa3f8c46f3b18c3d7ba9ff51367b2fa4ae2a2/yarl-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:52c136f348605974c9b1c878addd6b7a60e3bf2245833e370862009b86fa4689", size = 90765 },
- { url = "https://files.pythonhosted.org/packages/23/36/c579b80a5c76c0d41c8e08baddb3e6940dfc20569db579a5691392c52afa/yarl-1.18.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ece25e2251c28bab737bdf0519c88189b3dd9492dc086a1d77336d940c28ced", size = 142376 },
- { url = "https://files.pythonhosted.org/packages/0c/5f/e247dc7c0607a0c505fea6c839721844bee55686dfb183c7d7b8ef8a9cb1/yarl-1.18.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:454902dc1830d935c90b5b53c863ba2a98dcde0fbaa31ca2ed1ad33b2a7171c6", size = 94692 },
- { url = "https://files.pythonhosted.org/packages/eb/e1/3081b578a6f21961711b9a1c49c2947abb3b0d0dd9537378fb06777ce8ee/yarl-1.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01be8688fc211dc237e628fcc209dda412d35de7642453059a0553747018d075", size = 92527 },
- { url = "https://files.pythonhosted.org/packages/2f/fa/d9e1b9fbafa4cc82cd3980b5314741b33c2fe16308d725449a23aed32021/yarl-1.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d26f1fa9fa2167bb238f6f4b20218eb4e88dd3ef21bb8f97439fa6b5313e30d", size = 332096 },
- { url = "https://files.pythonhosted.org/packages/93/b6/dd27165114317875838e216214fb86338dc63d2e50855a8f2a12de2a7fe5/yarl-1.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b234a4a9248a9f000b7a5dfe84b8cb6210ee5120ae70eb72a4dcbdb4c528f72f", size = 342047 },
- { url = "https://files.pythonhosted.org/packages/fc/9f/bad434b5279ae7a356844e14dc771c3d29eb928140bbc01621af811c8a27/yarl-1.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe94d1de77c4cd8caff1bd5480e22342dbd54c93929f5943495d9c1e8abe9f42", size = 341712 },
- { url = "https://files.pythonhosted.org/packages/9a/9f/63864f43d131ba8c8cdf1bde5dd3f02f0eff8a7c883a5d7fad32f204fda5/yarl-1.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4c90c5363c6b0a54188122b61edb919c2cd1119684999d08cd5e538813a28e", size = 336654 },
- { url = "https://files.pythonhosted.org/packages/20/30/b4542bbd9be73de155213207eec019f6fe6495885f7dd59aa1ff705a041b/yarl-1.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a98ecadc5a241c9ba06de08127ee4796e1009555efd791bac514207862b43d", size = 325484 },
- { url = "https://files.pythonhosted.org/packages/69/bc/e2a9808ec26989cf0d1b98fe7b3cc45c1c6506b5ea4fe43ece5991f28f34/yarl-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9106025c7f261f9f5144f9aa7681d43867eed06349a7cfb297a1bc804de2f0d1", size = 344213 },
- { url = "https://files.pythonhosted.org/packages/e2/17/0ee5a68886aca1a8071b0d24a1e1c0fd9970dead2ef2d5e26e027fb7ce88/yarl-1.18.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f275ede6199d0f1ed4ea5d55a7b7573ccd40d97aee7808559e1298fe6efc8dbd", size = 340517 },
- { url = "https://files.pythonhosted.org/packages/fd/db/1fe4ef38ee852bff5ec8f5367d718b3a7dac7520f344b8e50306f68a2940/yarl-1.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f7edeb1dcc7f50a2c8e08b9dc13a413903b7817e72273f00878cb70e766bdb3b", size = 346234 },
- { url = "https://files.pythonhosted.org/packages/b4/ee/5e5bccdb821eb9949ba66abb4d19e3299eee00282e37b42f65236120e892/yarl-1.18.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c083f6dd6951b86e484ebfc9c3524b49bcaa9c420cb4b2a78ef9f7a512bfcc85", size = 359625 },
- { url = "https://files.pythonhosted.org/packages/3f/43/95a64d9e7ab4aa1c34fc5ea0edb35b581bc6ad33fd960a8ae34c2040b319/yarl-1.18.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:80741ec5b471fbdfb997821b2842c59660a1c930ceb42f8a84ba8ca0f25a66aa", size = 364239 },
- { url = "https://files.pythonhosted.org/packages/40/19/09ce976c624c9d3cc898f0be5035ddef0c0759d85b2313321cfe77b69915/yarl-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1a3297b9cad594e1ff0c040d2881d7d3a74124a3c73e00c3c71526a1234a9f7", size = 357599 },
- { url = "https://files.pythonhosted.org/packages/7d/35/6f33fd29791af2ec161aebe8abe63e788c2b74a6c7e8f29c92e5f5e96849/yarl-1.18.0-cp312-cp312-win32.whl", hash = "sha256:cd6ab7d6776c186f544f893b45ee0c883542b35e8a493db74665d2e594d3ca75", size = 83832 },
- { url = "https://files.pythonhosted.org/packages/4e/8e/cdb40ef98597be107de67b11e2f1f23f911e0f1416b938885d17a338e304/yarl-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:039c299a0864d1f43c3e31570045635034ea7021db41bf4842693a72aca8df3a", size = 90132 },
- { url = "https://files.pythonhosted.org/packages/2b/77/2196b657c66f97adaef0244e9e015f30eac0df59c31ad540f79ce328feed/yarl-1.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6fb64dd45453225f57d82c4764818d7a205ee31ce193e9f0086e493916bd4f72", size = 140512 },
- { url = "https://files.pythonhosted.org/packages/0e/d8/2bb6e26fddba5c01bad284e4571178c651b97e8e06318efcaa16e07eb9fd/yarl-1.18.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3adaaf9c6b1b4fc258584f4443f24d775a2086aee82d1387e48a8b4f3d6aecf6", size = 93875 },
- { url = "https://files.pythonhosted.org/packages/54/e4/99fbb884dd9f814fb0037dc1783766bb9edcd57b32a76f3ec5ac5c5772d7/yarl-1.18.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:da206d1ec78438a563c5429ab808a2b23ad7bc025c8adbf08540dde202be37d5", size = 91705 },
- { url = "https://files.pythonhosted.org/packages/3b/a2/5bd86eca9449e6b15d3b08005cf4e58e3da972240c2bee427b358c311549/yarl-1.18.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:576d258b21c1db4c6449b1c572c75d03f16a482eb380be8003682bdbe7db2f28", size = 333325 },
- { url = "https://files.pythonhosted.org/packages/94/50/a218da5f159cd985685bc72c500bb1a7fd2d60035d2339b8a9d9e1f99194/yarl-1.18.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e547c0a375c4bfcdd60eef82e7e0e8698bf84c239d715f5c1278a73050393", size = 344121 },
- { url = "https://files.pythonhosted.org/packages/a4/e3/830ae465811198b4b5ebecd674b5b3dca4d222af2155eb2144bfe190bbb8/yarl-1.18.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3818eabaefb90adeb5e0f62f047310079d426387991106d4fbf3519eec7d90a", size = 345163 },
- { url = "https://files.pythonhosted.org/packages/7a/74/05c4326877ca541eee77b1ef74b7ac8081343d3957af8f9291ca6eca6fec/yarl-1.18.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f72421246c21af6a92fbc8c13b6d4c5427dfd949049b937c3b731f2f9076bd", size = 339130 },
- { url = "https://files.pythonhosted.org/packages/29/42/842f35aa1dae25d132119ee92185e8c75d8b9b7c83346506bd31e9fa217f/yarl-1.18.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fa7d37f2ada0f42e0723632993ed422f2a679af0e200874d9d861720a54f53e", size = 326418 },
- { url = "https://files.pythonhosted.org/packages/f9/ed/65c0514f2d1e8b92a61f564c914381d078766cab38b5fbde355b3b3af1fb/yarl-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:42ba84e2ac26a3f252715f8ec17e6fdc0cbf95b9617c5367579fafcd7fba50eb", size = 345204 },
- { url = "https://files.pythonhosted.org/packages/23/31/351f64f0530c372fa01160f38330f44478e7bf3092f5ce2bfcb91605561d/yarl-1.18.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6a49ad0102c0f0ba839628d0bf45973c86ce7b590cdedf7540d5b1833ddc6f00", size = 341652 },
- { url = "https://files.pythonhosted.org/packages/49/aa/0c6e666c218d567727c1d040d01575685e7f9b18052fd68a59c9f61fe5d9/yarl-1.18.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96404e8d5e1bbe36bdaa84ef89dc36f0e75939e060ca5cd45451aba01db02902", size = 347257 },
- { url = "https://files.pythonhosted.org/packages/36/0b/33a093b0e13bb8cd0f27301779661ff325270b6644929001f8f33307357d/yarl-1.18.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a0509475d714df8f6d498935b3f307cd122c4ca76f7d426c7e1bb791bcd87eda", size = 359735 },
- { url = "https://files.pythonhosted.org/packages/a8/92/dcc0b37c48632e71ffc2b5f8b0509347a0bde55ab5862ff755dce9dd56c4/yarl-1.18.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ff116f0285b5c8b3b9a2680aeca29a858b3b9e0402fc79fd850b32c2bcb9f8b", size = 365982 },
- { url = "https://files.pythonhosted.org/packages/0e/39/30e2a24a7a6c628dccb13eb6c4a03db5f6cd1eb2c6cda56a61ddef764c11/yarl-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2580c1d7e66e6d29d6e11855e3b1c6381971e0edd9a5066e6c14d79bc8967af", size = 360128 },
- { url = "https://files.pythonhosted.org/packages/76/13/12b65dca23b1fb8ae44269a4d24048fd32ac90b445c985b0a46fdfa30cfe/yarl-1.18.0-cp313-cp313-win32.whl", hash = "sha256:14408cc4d34e202caba7b5ac9cc84700e3421a9e2d1b157d744d101b061a4a88", size = 309888 },
- { url = "https://files.pythonhosted.org/packages/f6/60/478d3d41a4bf0b9e7dca74d870d114e775d1ff7156b7d1e0e9972e8f97fd/yarl-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1db1537e9cb846eb0ff206eac667f627794be8b71368c1ab3207ec7b6f8c5afc", size = 315459 },
- { url = "https://files.pythonhosted.org/packages/30/9c/3f7ab894a37b1520291247cbc9ea6756228d098dae5b37eec848d404a204/yarl-1.18.0-py3-none-any.whl", hash = "sha256:dbf53db46f7cf176ee01d8d98c39381440776fcda13779d269a8ba664f69bec0", size = 44840 },
+ { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 },
+ { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 },
+ { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 },
+ { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 },
+ { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 },
+ { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 },
+ { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 },
+ { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 },
+ { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 },
+ { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 },
+ { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 },
+ { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 },
+ { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 },
+ { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 },
+ { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 },
+ { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 },
+ { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 },
+ { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 },
+ { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 },
+ { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 },
+ { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 },
+ { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 },
+ { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 },
+ { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 },
+ { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 },
+ { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 },
+ { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 },
+ { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 },
+ { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 },
+ { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 },
+ { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 },
+ { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 },
+ { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 },
+ { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 },
+ { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 },
+ { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 },
+ { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 },
+ { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 },
+ { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 },
+ { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 },
+ { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 },
+ { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 },
+ { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 },
+ { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 },
+ { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 },
+ { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 },
+ { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 },
+ { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 },
+ { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 },
]