mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 03:03:35 +00:00
Merge remote-tracking branch 'upstream/master' into feat/light_module_feats
This commit is contained in:
commit
feed5d18a1
132
CHANGELOG.md
132
CHANGELOG.md
@ -1,5 +1,137 @@
|
||||
# Changelog
|
||||
|
||||
## [0.8.1](https://github.com/python-kasa/python-kasa/tree/0.8.1) (2024-12-06)
|
||||
|
||||
This patch release fixes some issues with newly supported smartcam devices.
|
||||
|
||||
[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)
|
||||
|
||||
**Release highlights:**
|
||||
|
||||
- **Initial support for devices using the Tapo camera protocol, i.e. Tapo cameras and the Tapo H200 hub.**
|
||||
- New camera functionality such as exposing RTSP streaming urls and camera pan/tilt.
|
||||
- New way of testing module support for individual features with `has_feature` and `get_feature`.
|
||||
- Adding voltage and current monitoring to `smart` devices.
|
||||
- Migration from pydantic to mashumaro for serialization.
|
||||
|
||||
Special thanks to @ryenitcher and @Puxtril for their new contributions to the improvement of the project! Also thanks to everyone who has helped with testing, contributing fixtures, and reporting issues!
|
||||
|
||||
**Breaking change notes:**
|
||||
|
||||
- Removed support for python <3.11. If you haven't got a compatible version try [uv](https://docs.astral.sh/uv/).
|
||||
- Renamed `device_config.to_dict()` to `device_config.to_dict_control_credentials()`. `to_dict()` is still available but takes no parameters.
|
||||
- From the `iot.Cloud` module the `iot.CloudInfo` class attributes have been converted to snake case.
|
||||
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
- Migrate iot cloud module to mashumaro [\#1282](https://github.com/python-kasa/python-kasa/pull/1282) (@sdb9696)
|
||||
- Replace custom deviceconfig serialization with mashumaru [\#1274](https://github.com/python-kasa/python-kasa/pull/1274) (@sdb9696)
|
||||
- Remove support for python \<3.11 [\#1273](https://github.com/python-kasa/python-kasa/pull/1273) (@sdb9696)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Update cli modify presets to support smart devices [\#1295](https://github.com/python-kasa/python-kasa/pull/1295) (@sdb9696)
|
||||
- Use credentials\_hash for smartcamera rtsp url [\#1293](https://github.com/python-kasa/python-kasa/pull/1293) (@sdb9696)
|
||||
- Add voltage and current monitoring to smart Devices [\#1281](https://github.com/python-kasa/python-kasa/pull/1281) (@ryenitcher)
|
||||
- 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)
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
**Added support for devices:**
|
||||
|
||||
- Add HS200 \(US\) Smart Fixture [\#1303](https://github.com/python-kasa/python-kasa/pull/1303) (@ZeliardM)
|
||||
- Add smartcamera devices to supported docs [\#1257](https://github.com/python-kasa/python-kasa/pull/1257) (@sdb9696)
|
||||
- Add P110M\(AU\) fixture [\#1244](https://github.com/python-kasa/python-kasa/pull/1244) (@rytilahti)
|
||||
- Add L630 fixture [\#1240](https://github.com/python-kasa/python-kasa/pull/1240) (@rytilahti)
|
||||
- Add EP40M Fixture [\#1238](https://github.com/python-kasa/python-kasa/pull/1238) (@ryenitcher)
|
||||
- Add KS220 Fixture [\#1237](https://github.com/python-kasa/python-kasa/pull/1237) (@ryenitcher)
|
||||
|
||||
**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)
|
||||
|
||||
**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)
|
||||
- Move iot fixtures into iot subfolder [\#1299](https://github.com/python-kasa/python-kasa/pull/1299) (@sdb9696)
|
||||
- Annotate fan\_speed\_level of Fan interface [\#1298](https://github.com/python-kasa/python-kasa/pull/1298) (@sdb9696)
|
||||
- Add PIR ADC Values to Test Fixtures [\#1296](https://github.com/python-kasa/python-kasa/pull/1296) (@ryenitcher)
|
||||
- Exclude \_\_getattr\_\_ for deprecated attributes from type checkers [\#1294](https://github.com/python-kasa/python-kasa/pull/1294) (@sdb9696)
|
||||
- Simplify omit http\_client in DeviceConfig serialization [\#1292](https://github.com/python-kasa/python-kasa/pull/1292) (@sdb9696)
|
||||
- Add SMART Voltage Monitoring to Fixtures [\#1290](https://github.com/python-kasa/python-kasa/pull/1290) (@ryenitcher)
|
||||
- Remove pydantic dependency [\#1289](https://github.com/python-kasa/python-kasa/pull/1289) (@sdb9696)
|
||||
- Do not print out all the fixture names at the start of test runs [\#1287](https://github.com/python-kasa/python-kasa/pull/1287) (@sdb9696)
|
||||
- dump\_devinfo: iot light strip commands [\#1286](https://github.com/python-kasa/python-kasa/pull/1286) (@sdb9696)
|
||||
- Migrate TurnOnBehaviours to mashumaro [\#1285](https://github.com/python-kasa/python-kasa/pull/1285) (@sdb9696)
|
||||
- dump\_devinfo: query smartlife.iot.common.cloud for fw updates [\#1284](https://github.com/python-kasa/python-kasa/pull/1284) (@rytilahti)
|
||||
- Migrate RuleModule to mashumaro [\#1283](https://github.com/python-kasa/python-kasa/pull/1283) (@sdb9696)
|
||||
- Update sphinx dependency to 6.2 to fix docs build [\#1280](https://github.com/python-kasa/python-kasa/pull/1280) (@sdb9696)
|
||||
- Update DiscoveryResult to use mashu Annotated Alias [\#1279](https://github.com/python-kasa/python-kasa/pull/1279) (@sdb9696)
|
||||
- Extend dump\_devinfo iot queries [\#1278](https://github.com/python-kasa/python-kasa/pull/1278) (@sdb9696)
|
||||
- Migrate triggerlogs to mashumaru [\#1277](https://github.com/python-kasa/python-kasa/pull/1277) (@sdb9696)
|
||||
- Migrate smart firmware module to mashumaro [\#1276](https://github.com/python-kasa/python-kasa/pull/1276) (@sdb9696)
|
||||
- Migrate IotLightPreset to mashumaru [\#1275](https://github.com/python-kasa/python-kasa/pull/1275) (@sdb9696)
|
||||
- Allow callable coroutines for feature setters [\#1272](https://github.com/python-kasa/python-kasa/pull/1272) (@sdb9696)
|
||||
- Fix deprecated SSLContext\(\) usage [\#1271](https://github.com/python-kasa/python-kasa/pull/1271) (@sdb9696)
|
||||
- Use \_get\_device\_info methods for smart and iot devs in devtools [\#1265](https://github.com/python-kasa/python-kasa/pull/1265) (@sdb9696)
|
||||
- Remove experimental support [\#1256](https://github.com/python-kasa/python-kasa/pull/1256) (@sdb9696)
|
||||
- Move protocol modules into protocols package [\#1254](https://github.com/python-kasa/python-kasa/pull/1254) (@sdb9696)
|
||||
- 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)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Expose Fan speed range from the library [\#1008](https://github.com/python-kasa/python-kasa/issues/1008)
|
||||
- \[META\] 0.7 series - module support for SMART devices, support for introspectable device features and refactoring the library [\#783](https://github.com/python-kasa/python-kasa/issues/783)
|
||||
|
||||
## [0.7.7](https://github.com/python-kasa/python-kasa/tree/0.7.7) (2024-11-04)
|
||||
|
||||
[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.7.6...0.7.7)
|
||||
|
23
README.md
23
README.md
@ -182,29 +182,29 @@ The following devices have been tested and confirmed as working. If your device
|
||||
<!--SUPPORTED_START-->
|
||||
### Supported Kasa devices
|
||||
|
||||
- **Plugs**: EP10, EP25<sup>\*</sup>, HS100<sup>\*\*</sup>, HS103, HS105, HS110, KP100, KP105, KP115, KP125, KP125M<sup>\*</sup>, KP401
|
||||
- **Power Strips**: EP40, EP40M<sup>\*</sup>, HS107, HS300, KP200, KP303, KP400
|
||||
- **Wall Switches**: ES20M, HS200<sup>\*\*</sup>, HS210, HS220<sup>\*\*</sup>, KP405, KS200M, KS205<sup>\*</sup>, KS220, KS220M, KS225<sup>\*</sup>, KS230, KS240<sup>\*</sup>
|
||||
- **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, 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<sup>\*</sup>
|
||||
- **Hub-Connected Devices<sup>\*\*\*</sup>**: KE100<sup>\*</sup>
|
||||
- **Hubs**: KH100[^1]
|
||||
- **Hub-Connected Devices[^3]**: KE100[^1]
|
||||
|
||||
### Supported Tapo<sup>\*</sup> devices
|
||||
### Supported Tapo[^1] devices
|
||||
|
||||
- **Plugs**: P100, P110, P110M, P115, P125M, P135, TP15
|
||||
- **Power Strips**: P300, P304M, TP25
|
||||
- **Wall Switches**: S500D, S505, S505D
|
||||
- **Bulbs**: L510B, L510E, L530E, L630
|
||||
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
|
||||
- **Cameras**: C210, TC65
|
||||
- **Cameras**: C210, C520WS, TC65
|
||||
- **Hubs**: H100, H200
|
||||
- **Hub-Connected Devices<sup>\*\*\*</sup>**: S200B, S200D, T100, T110, T300, T310, T315
|
||||
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
|
||||
|
||||
<!--SUPPORTED_END-->
|
||||
<sup>\*</sup> Model requires authentication<br>
|
||||
<sup>\*\*</sup> Newer versions require authentication<br>
|
||||
<sup>\*\*\*</sup> Devices may work across TAPO/KASA branded hubs
|
||||
[^1]: Model requires authentication
|
||||
[^2]: Newer versions require authentication
|
||||
[^3]: Devices may work across TAPO/KASA branded hubs
|
||||
|
||||
See [supported devices in our documentation](SUPPORTED.md) for more detailed information about tested hardware and software versions.
|
||||
|
||||
@ -227,6 +227,7 @@ See [supported devices in our documentation](SUPPORTED.md) for more detailed inf
|
||||
### Other related projects
|
||||
|
||||
* [PyTapo - Python library for communication with Tapo Cameras](https://github.com/JurajNyiri/pytapo)
|
||||
* [Home Assistant integration](https://github.com/JurajNyiri/HomeAssistant-Tapo-Control)
|
||||
* [Tapo P100 (Tapo plugs, Tapo bulbs)](https://github.com/fishbigger/TapoP100)
|
||||
* [Home Assistant integration](https://github.com/fishbigger/HomeAssistant-Tapo-P100-Control)
|
||||
* [plugp100, another tapo library](https://github.com/petretiandrea/plugp100)
|
||||
|
@ -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
|
||||
|
51
SUPPORTED.md
51
SUPPORTED.md
@ -10,18 +10,18 @@ The following devices have been tested and confirmed as working. If your device
|
||||
<!--SUPPORTED_START-->
|
||||
## Kasa devices
|
||||
|
||||
Some newer Kasa devices require authentication. These are marked with <sup>*</sup> in the list below.<br>Hub-Connected Devices may work across TAPO/KASA branded hubs even if they don't work across the native apps.
|
||||
Some newer Kasa devices require authentication. These are marked with [^1] in the list below.<br>Hub-Connected Devices may work across TAPO/KASA branded hubs even if they don't work across the native apps.
|
||||
|
||||
### Plugs
|
||||
|
||||
- **EP10**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
||||
- **EP25**
|
||||
- Hardware: 2.6 (US) / Firmware: 1.0.1<sup>\*</sup>
|
||||
- Hardware: 2.6 (US) / Firmware: 1.0.2<sup>\*</sup>
|
||||
- Hardware: 2.6 (US) / Firmware: 1.0.1[^1]
|
||||
- Hardware: 2.6 (US) / Firmware: 1.0.2[^1]
|
||||
- **HS100**
|
||||
- Hardware: 1.0 (UK) / Firmware: 1.2.6
|
||||
- Hardware: 4.1 (UK) / Firmware: 1.1.0<sup>\*</sup>
|
||||
- Hardware: 4.1 (UK) / Firmware: 1.1.0[^1]
|
||||
- Hardware: 1.0 (US) / Firmware: 1.2.5
|
||||
- Hardware: 2.0 (US) / Firmware: 1.5.6
|
||||
- **HS103**
|
||||
@ -46,8 +46,8 @@ Some newer Kasa devices require authentication. These are marked with <sup>*</su
|
||||
- **KP125**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.6
|
||||
- **KP125M**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.1.3<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.2.3<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.1.3[^1]
|
||||
- Hardware: 1.0 (US) / Firmware: 1.2.3[^1]
|
||||
- **KP401**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.0
|
||||
|
||||
@ -56,7 +56,7 @@ Some newer Kasa devices require authentication. These are marked with <sup>*</su
|
||||
- **EP40**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
||||
- **EP40M**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.1.0<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.1.0[^1]
|
||||
- **HS107**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.8
|
||||
- **HS300**
|
||||
@ -86,38 +86,40 @@ Some newer Kasa devices require authentication. These are marked with <sup>*</su
|
||||
- Hardware: 3.0 (US) / Firmware: 1.1.5
|
||||
- Hardware: 5.0 (US) / Firmware: 1.0.11
|
||||
- Hardware: 5.0 (US) / Firmware: 1.0.2
|
||||
- Hardware: 5.26 (US) / Firmware: 1.0.3<sup>\*</sup>
|
||||
- Hardware: 5.26 (US) / Firmware: 1.0.3[^1]
|
||||
- **HS210**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.5.8
|
||||
- Hardware: 2.0 (US) / Firmware: 1.1.5
|
||||
- **HS220**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.5.7
|
||||
- Hardware: 2.0 (US) / Firmware: 1.0.3
|
||||
- Hardware: 3.26 (US) / Firmware: 1.0.1<sup>\*</sup>
|
||||
- Hardware: 3.26 (US) / Firmware: 1.0.1[^1]
|
||||
- **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
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.12
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.8
|
||||
- **KS205**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.2<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.1.0<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.2[^1]
|
||||
- Hardware: 1.0 (US) / Firmware: 1.1.0[^1]
|
||||
- **KS220**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.13
|
||||
- **KS220M**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.4
|
||||
- **KS225**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.2<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.1.0<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.2[^1]
|
||||
- Hardware: 1.0 (US) / Firmware: 1.1.0[^1]
|
||||
- **KS230**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.14
|
||||
- **KS240**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.4<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.5<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.7<sup>\*</sup>
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.4[^1]
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.5[^1]
|
||||
- Hardware: 1.0 (US) / Firmware: 1.0.7[^1]
|
||||
|
||||
### Bulbs
|
||||
|
||||
@ -161,16 +163,16 @@ Some newer Kasa devices require authentication. These are marked with <sup>*</su
|
||||
### Hubs
|
||||
|
||||
- **KH100**
|
||||
- Hardware: 1.0 (EU) / Firmware: 1.2.3<sup>\*</sup>
|
||||
- Hardware: 1.0 (EU) / Firmware: 1.5.12<sup>\*</sup>
|
||||
- Hardware: 1.0 (UK) / Firmware: 1.5.6<sup>\*</sup>
|
||||
- Hardware: 1.0 (EU) / Firmware: 1.2.3[^1]
|
||||
- Hardware: 1.0 (EU) / Firmware: 1.5.12[^1]
|
||||
- Hardware: 1.0 (UK) / Firmware: 1.5.6[^1]
|
||||
|
||||
### Hub-Connected Devices
|
||||
|
||||
- **KE100**
|
||||
- Hardware: 1.0 (EU) / Firmware: 2.4.0<sup>\*</sup>
|
||||
- Hardware: 1.0 (EU) / Firmware: 2.8.0<sup>\*</sup>
|
||||
- Hardware: 1.0 (UK) / Firmware: 2.8.0<sup>\*</sup>
|
||||
- Hardware: 1.0 (EU) / Firmware: 2.4.0[^1]
|
||||
- Hardware: 1.0 (EU) / Firmware: 2.8.0[^1]
|
||||
- Hardware: 1.0 (UK) / Firmware: 2.8.0[^1]
|
||||
|
||||
|
||||
## Tapo devices
|
||||
@ -189,8 +191,10 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
|
||||
- Hardware: 1.0 (UK) / Firmware: 1.3.0
|
||||
- **P110M**
|
||||
- Hardware: 1.0 (AU) / Firmware: 1.2.3
|
||||
- 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**
|
||||
@ -254,6 +258,8 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
|
||||
- **C210**
|
||||
- Hardware: 2.0 (EU) / Firmware: 1.4.2
|
||||
- Hardware: 2.0 (EU) / Firmware: 1.4.3
|
||||
- **C520WS**
|
||||
- Hardware: 1.0 (US) / Firmware: 1.2.8
|
||||
- **TC65**
|
||||
- Hardware: 1.0 / Firmware: 1.3.9
|
||||
|
||||
@ -292,3 +298,4 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
|
||||
|
||||
|
||||
<!--SUPPORTED_END-->
|
||||
[^1]: Model requires authentication
|
||||
|
@ -115,6 +115,10 @@ def scrub(res):
|
||||
"encrypt_info",
|
||||
"local_ip",
|
||||
"username",
|
||||
# vacuum
|
||||
"board_sn",
|
||||
"custom_sn",
|
||||
"location",
|
||||
]
|
||||
|
||||
for k, v in res.items():
|
||||
@ -153,10 +157,18 @@ def scrub(res):
|
||||
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"]:
|
||||
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:
|
||||
|
@ -142,7 +142,7 @@ def _supported_text(
|
||||
for brand, types in supported.items():
|
||||
preamble_text = (
|
||||
"Some newer Kasa devices require authentication. "
|
||||
+ "These are marked with <sup>*</sup> in the list below."
|
||||
+ "These are marked with [^1] in the list below."
|
||||
if brand == "kasa"
|
||||
else "All Tapo devices require authentication."
|
||||
)
|
||||
@ -151,7 +151,7 @@ def _supported_text(
|
||||
+ "hubs even if they don't work across the native apps."
|
||||
)
|
||||
brand_text = brand.capitalize()
|
||||
brand_auth = r"<sup>\*</sup>" if brand == "tapo" else ""
|
||||
brand_auth = r"[^1]" if brand == "tapo" else ""
|
||||
types_text = ""
|
||||
for supported_type, models in sorted(
|
||||
# Sort by device type order in the enum
|
||||
@ -166,9 +166,7 @@ def _supported_text(
|
||||
for version in sorted(versions):
|
||||
region_text = f" ({version.region})" if version.region else ""
|
||||
auth_count += 1 if version.auth else 0
|
||||
vauth_flag = (
|
||||
r"<sup>\*</sup>" if version.auth and brand == "kasa" else ""
|
||||
)
|
||||
vauth_flag = r"[^1]" if version.auth and brand == "kasa" else ""
|
||||
if version_template:
|
||||
versions_text += versst.substitute(
|
||||
hw=version.hw,
|
||||
@ -177,11 +175,7 @@ def _supported_text(
|
||||
auth_flag=vauth_flag,
|
||||
)
|
||||
if brand == "kasa" and auth_count > 0:
|
||||
auth_flag = (
|
||||
r"<sup>\*</sup>"
|
||||
if auth_count == len(versions)
|
||||
else r"<sup>\*\*</sup>"
|
||||
)
|
||||
auth_flag = r"[^1]" if auth_count == len(versions) else r"[^2]"
|
||||
else:
|
||||
auth_flag = ""
|
||||
if model_template:
|
||||
@ -191,11 +185,7 @@ def _supported_text(
|
||||
else:
|
||||
models_list.append(f"{model}{auth_flag}")
|
||||
models_text = models_text if models_text else ", ".join(models_list)
|
||||
type_asterix = (
|
||||
r"<sup>\*\*\*</sup>"
|
||||
if supported_type == "Hub-Connected Devices"
|
||||
else ""
|
||||
)
|
||||
type_asterix = r"[^3]" if supported_type == "Hub-Connected Devices" else ""
|
||||
types_text += typest.substitute(
|
||||
type_=supported_type, type_asterix=type_asterix, models=models_text
|
||||
)
|
||||
|
@ -425,4 +425,28 @@ COMPONENT_REQUESTS = {
|
||||
"dimmer_calibration": [],
|
||||
"fan_control": [],
|
||||
"overheat_protection": [],
|
||||
# Vacuum components
|
||||
"clean": [
|
||||
SmartRequest.get_raw_request("getCleanRecords"),
|
||||
SmartRequest.get_raw_request("getVacStatus"),
|
||||
],
|
||||
"battery": [SmartRequest.get_raw_request("getBatteryInfo")],
|
||||
"consumables": [SmartRequest.get_raw_request("getConsumablesInfo")],
|
||||
"direction_control": [],
|
||||
"button_and_led": [],
|
||||
"speaker": [
|
||||
SmartRequest.get_raw_request("getSupportVoiceLanguage"),
|
||||
SmartRequest.get_raw_request("getCurrentVoiceLanguage"),
|
||||
],
|
||||
"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")],
|
||||
"mop": [SmartRequest.get_raw_request("getMopState")],
|
||||
"do_not_disturb": [SmartRequest.get_raw_request("getDoNotDisturb")],
|
||||
"charge_pose_clean": [],
|
||||
"continue_breakpoint_sweep": [],
|
||||
"goto_point": [],
|
||||
}
|
||||
|
@ -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: <Action>\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: <Action>\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: <Action>\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: <Action>\nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nOverheated: False\nDevice time: 2024-02-23 02:40:15+01:00
|
||||
"""
|
||||
|
@ -36,9 +36,11 @@ from kasa.exceptions import (
|
||||
)
|
||||
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.iotprotocol import _deprecated_TPLinkSmartHomeProtocol # noqa: F401
|
||||
from kasa.smartcam.modules.camera import StreamResolution
|
||||
from kasa.transports import BaseTransport
|
||||
|
||||
__version__ = version("python-kasa")
|
||||
@ -72,6 +74,9 @@ __all__ = [
|
||||
"DeviceConnectionParameters",
|
||||
"DeviceEncryptionType",
|
||||
"DeviceFamily",
|
||||
"ThermostatState",
|
||||
"Thermostat",
|
||||
"StreamResolution",
|
||||
]
|
||||
|
||||
from . import iot
|
||||
|
@ -14,8 +14,17 @@ from kasa import (
|
||||
Discover,
|
||||
UnsupportedDeviceError,
|
||||
)
|
||||
from kasa.discover import ConnectAttempt, DiscoveryResult
|
||||
from kasa.discover import (
|
||||
NEW_DISCOVERY_REDACTORS,
|
||||
ConnectAttempt,
|
||||
DiscoveredRaw,
|
||||
DiscoveryResult,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@ -63,7 +72,9 @@ async def detail(ctx):
|
||||
await ctx.parent.invoke(state)
|
||||
echo()
|
||||
|
||||
discovered = await _discover(ctx, print_discovered, print_unsupported)
|
||||
discovered = await _discover(
|
||||
ctx, print_discovered=print_discovered, print_unsupported=print_unsupported
|
||||
)
|
||||
if ctx.parent.parent.params["host"]:
|
||||
return discovered
|
||||
|
||||
@ -76,6 +87,33 @@ async def detail(ctx):
|
||||
return discovered
|
||||
|
||||
|
||||
@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 raw(ctx, redact: bool):
|
||||
"""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):
|
||||
@ -101,10 +139,17 @@ async def list(ctx):
|
||||
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)
|
||||
return await _discover(
|
||||
ctx,
|
||||
print_discovered=print_discovered,
|
||||
print_unsupported=print_unsupported,
|
||||
do_echo=False,
|
||||
)
|
||||
|
||||
|
||||
async def _discover(ctx, print_discovered, print_unsupported, *, do_echo=True):
|
||||
async def _discover(
|
||||
ctx, *, print_discovered=None, print_unsupported=None, print_raw=None, do_echo=True
|
||||
):
|
||||
params = ctx.parent.parent.params
|
||||
target = params["target"]
|
||||
username = params["username"]
|
||||
@ -125,6 +170,7 @@ async def _discover(ctx, print_discovered, print_unsupported, *, do_echo=True):
|
||||
timeout=timeout,
|
||||
discovery_timeout=discovery_timeout,
|
||||
on_unsupported=print_unsupported,
|
||||
on_discovered_raw=print_raw,
|
||||
)
|
||||
if do_echo:
|
||||
echo(f"Discovering devices on {target} for {discovery_timeout} seconds")
|
||||
@ -136,6 +182,7 @@ 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():
|
||||
@ -201,8 +248,8 @@ def _echo_discovery_info(discovery_info) -> None:
|
||||
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:
|
||||
@ -230,10 +277,12 @@ def _echo_discovery_info(discovery_info) -> None:
|
||||
_conditional_echo("Supports IOT Cloud", dr.is_support_iot_cloud)
|
||||
_conditional_echo("OBD Src", dr.owner)
|
||||
_conditional_echo("Factory Default", dr.factory_default)
|
||||
_conditional_echo("Encrypt Type", dr.mgt_encrypt_schm.encrypt_type)
|
||||
_conditional_echo("Encrypt Type", dr.encrypt_type)
|
||||
_conditional_echo("Supports HTTPS", dr.mgt_encrypt_schm.is_support_https)
|
||||
_conditional_echo("HTTP Port", dr.mgt_encrypt_schm.http_port)
|
||||
if mgt_encrypt_schm := dr.mgt_encrypt_schm:
|
||||
_conditional_echo("Encrypt Type", mgt_encrypt_schm.encrypt_type)
|
||||
_conditional_echo("Supports HTTPS", mgt_encrypt_schm.is_support_https)
|
||||
_conditional_echo("HTTP Port", mgt_encrypt_schm.http_port)
|
||||
_conditional_echo("Login version", mgt_encrypt_schm.lv)
|
||||
_conditional_echo("Encrypt info", pf(dr.encrypt_info) if dr.encrypt_info else None)
|
||||
_conditional_echo("Decrypted", pf(dr.decrypted_data) if dr.decrypted_data else None)
|
||||
|
||||
|
@ -75,6 +75,7 @@ def _legacy_type_to_class(_type: str) -> Any:
|
||||
"time": None,
|
||||
"schedule": None,
|
||||
"usage": None,
|
||||
"energy": "usage",
|
||||
# device commands runnnable at top level
|
||||
"state": "device",
|
||||
"on": "device",
|
||||
@ -307,6 +308,7 @@ async def cli(
|
||||
if type == "camera":
|
||||
encrypt_type = "AES"
|
||||
https = True
|
||||
login_version = 2
|
||||
device_family = "SMART.IPCAMERA"
|
||||
|
||||
from kasa.device import Device
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
import asyncclick as click
|
||||
@ -21,21 +20,6 @@ from .common import (
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--index", type=int, required=False)
|
||||
@click.option("--name", type=str, required=False)
|
||||
@click.option("--year", type=click.DateTime(["%Y"]), default=None, required=False)
|
||||
@click.option("--month", type=click.DateTime(["%Y-%m"]), default=None, required=False)
|
||||
@click.option("--erase", is_flag=True)
|
||||
@click.pass_context
|
||||
async def emeter(ctx: click.Context, index, name, year, month, erase):
|
||||
"""Query emeter for historical consumption."""
|
||||
logging.warning("Deprecated, use 'kasa energy'")
|
||||
return await ctx.invoke(
|
||||
energy, child_index=index, child=name, year=year, month=month, erase=erase
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--year", type=click.DateTime(["%Y"]), default=None, required=False)
|
||||
@click.option("--month", type=click.DateTime(["%Y-%m"]), default=None, required=False)
|
||||
@ -46,7 +30,7 @@ async def energy(dev: Device, year, month, erase):
|
||||
|
||||
Daily and monthly data provided in CSV format.
|
||||
"""
|
||||
echo("[bold]== Emeter ==[/bold]")
|
||||
echo("[bold]== Energy ==[/bold]")
|
||||
if not (energy := dev.modules.get(Module.Energy)):
|
||||
error("Device has no energy module.")
|
||||
return
|
||||
@ -71,7 +55,7 @@ async def energy(dev: Device, year, month, erase):
|
||||
usage_data = await energy.get_daily_stats(year=month.year, month=month.month)
|
||||
else:
|
||||
# Call with no argument outputs summary data and returns
|
||||
emeter_status = await energy.get_status()
|
||||
emeter_status = energy.status
|
||||
|
||||
echo("Current: {} A".format(emeter_status["current"]))
|
||||
echo("Voltage: {} V".format(emeter_status["voltage"]))
|
||||
|
@ -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="),
|
||||
}
|
||||
|
21
kasa/device_factory.py
Executable file → Normal file
21
kasa/device_factory.py
Executable file → Normal file
@ -12,6 +12,7 @@ from .deviceconfig import DeviceConfig
|
||||
from .exceptions import KasaException, UnsupportedDeviceError
|
||||
from .iot import (
|
||||
IotBulb,
|
||||
IotCamera,
|
||||
IotDevice,
|
||||
IotDimmer,
|
||||
IotLightStrip,
|
||||
@ -32,6 +33,8 @@ from .transports import (
|
||||
BaseTransport,
|
||||
KlapTransport,
|
||||
KlapTransportV2,
|
||||
LinkieTransportV2,
|
||||
SslTransport,
|
||||
XorTransport,
|
||||
)
|
||||
from .transports.sslaestransport import SslAesTransport
|
||||
@ -137,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)]
|
||||
|
||||
@ -155,8 +159,10 @@ def get_device_class_from_family(
|
||||
"SMART.KASAHUB": SmartDevice,
|
||||
"SMART.KASASWITCH": SmartDevice,
|
||||
"SMART.IPCAMERA.HTTPS": SmartCamDevice,
|
||||
"SMART.TAPOROBOVAC": SmartDevice,
|
||||
"IOT.SMARTPLUGSWITCH": IotPlug,
|
||||
"IOT.SMARTBULB": IotBulb,
|
||||
"IOT.IPCAMERA": IotCamera,
|
||||
}
|
||||
lookup_key = f"{device_type}{'.HTTPS' if https else ''}"
|
||||
if (
|
||||
@ -176,20 +182,31 @@ def get_protocol(
|
||||
"""Return the protocol from the connection name."""
|
||||
protocol_name = config.connection_type.device_family.value.split(".")[0]
|
||||
ctype = config.connection_type
|
||||
|
||||
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)
|
||||
supported_device_protocols: dict[
|
||||
str, tuple[type[BaseProtocol], type[BaseTransport]]
|
||||
] = {
|
||||
"IOT.XOR": (IotProtocol, XorTransport),
|
||||
"IOT.KLAP": (IotProtocol, KlapTransport),
|
||||
"IOT.XOR.HTTPS.2": (IotProtocol, LinkieTransportV2),
|
||||
"SMART.AES": (SmartProtocol, AesTransport),
|
||||
"SMART.KLAP": (SmartProtocol, KlapTransportV2),
|
||||
"SMART.AES.HTTPS": (SmartCamProtocol, SslAesTransport),
|
||||
"SMART.AES.2": (SmartProtocol, AesTransport),
|
||||
"SMART.KLAP.2": (SmartProtocol, KlapTransportV2),
|
||||
"SMART.AES.HTTPS.2": (SmartCamProtocol, SslAesTransport),
|
||||
"SMART.AES.HTTPS": (SmartProtocol, SslTransport),
|
||||
}
|
||||
if not (prot_tran_cls := supported_device_protocols.get(protocol_transport_key)):
|
||||
return None
|
||||
|
@ -21,6 +21,7 @@ class DeviceType(Enum):
|
||||
Hub = "hub"
|
||||
Fan = "fan"
|
||||
Thermostat = "thermostat"
|
||||
Vacuum = "vacuum"
|
||||
Unknown = "unknown"
|
||||
|
||||
@staticmethod
|
||||
|
@ -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"
|
||||
@ -77,6 +78,7 @@ class DeviceFamily(Enum):
|
||||
SmartTapoHub = "SMART.TAPOHUB"
|
||||
SmartKasaHub = "SMART.KASAHUB"
|
||||
SmartIpCamera = "SMART.IPCAMERA"
|
||||
SmartTapoRobovac = "SMART.TAPOROBOVAC"
|
||||
|
||||
|
||||
class _DeviceConfigBaseMixin(DataClassJSONMixin):
|
||||
|
132
kasa/discover.py
132
kasa/discover.py
@ -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
|
||||
@ -147,15 +148,35 @@ class ConnectAttempt(NamedTuple):
|
||||
device: type
|
||||
|
||||
|
||||
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]
|
||||
|
||||
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": ""},
|
||||
}
|
||||
|
||||
|
||||
@ -213,6 +234,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,
|
||||
@ -237,6 +259,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
|
||||
@ -326,12 +349,23 @@ 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
|
||||
@ -388,6 +422,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,
|
||||
@ -418,6 +453,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
|
||||
@ -440,6 +477,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,
|
||||
@ -473,6 +511,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.
|
||||
@ -490,6 +529,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.
|
||||
"""
|
||||
@ -526,6 +568,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
|
||||
)
|
||||
@ -595,10 +638,12 @@ class Discover:
|
||||
for encrypt in Device.EncryptionType
|
||||
for device_family in main_device_families
|
||||
for https in (True, False)
|
||||
for login_version in (None, 2)
|
||||
if (
|
||||
conn_params := DeviceConnectionParameters(
|
||||
device_family=device_family,
|
||||
encryption_type=encrypt,
|
||||
login_version=login_version,
|
||||
https=https,
|
||||
)
|
||||
)
|
||||
@ -643,7 +688,11 @@ class Discover:
|
||||
"""Find SmartDevice subclass for device described by passed data."""
|
||||
if "result" in info:
|
||||
discovery_result = DiscoveryResult.from_dict(info["result"])
|
||||
https = discovery_result.mgt_encrypt_schm.is_support_https
|
||||
https = (
|
||||
discovery_result.mgt_encrypt_schm.is_support_https
|
||||
if discovery_result.mgt_encrypt_schm
|
||||
else False
|
||||
)
|
||||
dev_class = get_device_class_from_family(
|
||||
discovery_result.device_type, https=https
|
||||
)
|
||||
@ -657,27 +706,36 @@ 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
|
||||
@ -701,20 +759,25 @@ class Discover:
|
||||
|
||||
discovery_result.decrypted_data = json_loads(decrypted_data)
|
||||
|
||||
@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_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"])
|
||||
@ -743,11 +806,19 @@ 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
|
||||
encrypt_schm = discovery_result.mgt_encrypt_schm
|
||||
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 (
|
||||
@ -755,6 +826,13 @@ class Discover:
|
||||
):
|
||||
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_} "
|
||||
@ -765,13 +843,13 @@ class Discover:
|
||||
config.connection_type = DeviceConnectionParameters.from_values(
|
||||
type_,
|
||||
encrypt_type,
|
||||
discovery_result.mgt_encrypt_schm.lv,
|
||||
discovery_result.mgt_encrypt_schm.is_support_https,
|
||||
login_version,
|
||||
encrypt_schm.is_support_https,
|
||||
)
|
||||
except KasaException as ex:
|
||||
raise UnsupportedDeviceError(
|
||||
f"Unsupported device {config.host} of type {type_} "
|
||||
+ f"with encrypt_type {discovery_result.mgt_encrypt_schm.encrypt_type}",
|
||||
+ f"with encrypt_type {encrypt_schm.encrypt_type}",
|
||||
discovery_result=discovery_result.to_dict(),
|
||||
host=config.host,
|
||||
) from ex
|
||||
@ -854,7 +932,7 @@ class DiscoveryResult(_DiscoveryBaseMixin):
|
||||
device_id: str
|
||||
ip: str
|
||||
mac: str
|
||||
mgt_encrypt_schm: EncryptionScheme
|
||||
mgt_encrypt_schm: EncryptionScheme | None = None
|
||||
device_name: str | None = None
|
||||
encrypt_info: EncryptionInfo | None = None
|
||||
encrypt_type: list[str] | None = None
|
||||
|
@ -24,7 +24,6 @@ State (state): True
|
||||
Signal Level (signal_level): 2
|
||||
RSSI (rssi): -52
|
||||
SSID (ssid): #MASKED_SSID#
|
||||
Overheated (overheated): False
|
||||
Reboot (reboot): <Action>
|
||||
Brightness (brightness): 100
|
||||
Cloud connection (cloud_connection): True
|
||||
@ -39,6 +38,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
|
||||
Overheated (overheated): False
|
||||
Device time (device_time): 2024-02-23 02:40:15+01:00
|
||||
|
||||
To see whether a device supports a feature, check for the existence of it:
|
||||
|
@ -6,6 +6,7 @@ from .led import Led
|
||||
from .light import Light, LightState
|
||||
from .lighteffect import LightEffect
|
||||
from .lightpreset import LightPreset
|
||||
from .thermostat import Thermostat, ThermostatState
|
||||
from .time import Time
|
||||
|
||||
__all__ = [
|
||||
@ -16,5 +17,7 @@ __all__ = [
|
||||
"LightEffect",
|
||||
"LightState",
|
||||
"LightPreset",
|
||||
"Thermostat",
|
||||
"ThermostatState",
|
||||
"Time",
|
||||
]
|
||||
|
65
kasa/interfaces/thermostat.py
Normal file
65
kasa/interfaces/thermostat.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Interact with a TPLink Thermostat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from ..module import FeatureAttribute, Module
|
||||
|
||||
|
||||
class ThermostatState(Enum):
|
||||
"""Thermostat state."""
|
||||
|
||||
Heating = "heating"
|
||||
Calibrating = "progress_calibration"
|
||||
Idle = "idle"
|
||||
Off = "off"
|
||||
Unknown = "unknown"
|
||||
|
||||
|
||||
class Thermostat(Module, ABC):
|
||||
"""Base class for TP-Link Thermostat."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def state(self) -> bool:
|
||||
"""Return thermostat state."""
|
||||
|
||||
@abstractmethod
|
||||
async def set_state(self, enabled: bool) -> dict:
|
||||
"""Set thermostat state."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def mode(self) -> ThermostatState:
|
||||
"""Return thermostat state."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def target_temperature(self) -> Annotated[float, FeatureAttribute()]:
|
||||
"""Return target temperature."""
|
||||
|
||||
@abstractmethod
|
||||
async def set_target_temperature(
|
||||
self, target: float
|
||||
) -> Annotated[dict, FeatureAttribute()]:
|
||||
"""Set target temperature."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def temperature(self) -> Annotated[float, FeatureAttribute()]:
|
||||
"""Return current humidity in percentage."""
|
||||
return self._device.sys_info["current_temp"]
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def temperature_unit(self) -> Literal["celsius", "fahrenheit"]:
|
||||
"""Return current temperature unit."""
|
||||
|
||||
@abstractmethod
|
||||
async def set_temperature_unit(
|
||||
self, unit: Literal["celsius", "fahrenheit"]
|
||||
) -> dict:
|
||||
"""Set the device temperature unit."""
|
@ -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",
|
||||
]
|
||||
|
42
kasa/iot/iotcamera.py
Normal file
42
kasa/iot/iotcamera.py
Normal file
@ -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
|
@ -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.
|
||||
|
||||
@ -304,14 +314,14 @@ class IotDevice(Device):
|
||||
_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()
|
||||
|
||||
@ -705,10 +715,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,6 +741,7 @@ class IotDevice(Device):
|
||||
return DeviceType.LightStrip
|
||||
|
||||
return DeviceType.Bulb
|
||||
|
||||
_LOGGER.warning("Unknown device type %s, falling back to plug", type_)
|
||||
return DeviceType.Plug
|
||||
|
||||
@ -736,7 +750,7 @@ class IotDevice(Device):
|
||||
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
||||
) -> _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
|
||||
|
14
kasa/json.py
14
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
|
||||
|
||||
|
@ -14,9 +14,17 @@ Light, AutoOff, Firmware etc.
|
||||
>>> print(dev.alias)
|
||||
Living Room Bulb
|
||||
|
||||
To see whether a device supports functionality check for the existence of the module:
|
||||
To see whether a device supports a group of functionality
|
||||
check for the existence of the module:
|
||||
|
||||
>>> if light := dev.modules.get("Light"):
|
||||
>>> print(light.brightness)
|
||||
100
|
||||
|
||||
To see whether a device supports specific functionality, you can check whether the
|
||||
module has that feature:
|
||||
|
||||
>>> if light.has_feature("hsv"):
|
||||
>>> print(light.hsv)
|
||||
HSV(hue=0, saturation=100, value=100)
|
||||
|
||||
@ -70,6 +78,9 @@ ModuleT = TypeVar("ModuleT", bound="Module")
|
||||
class FeatureAttribute:
|
||||
"""Class for annotating attributes bound to feature."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "FeatureAttribute"
|
||||
|
||||
|
||||
class Module(ABC):
|
||||
"""Base class implemention for all modules.
|
||||
@ -85,6 +96,7 @@ class Module(ABC):
|
||||
Led: Final[ModuleName[interfaces.Led]] = ModuleName("Led")
|
||||
Light: Final[ModuleName[interfaces.Light]] = ModuleName("Light")
|
||||
LightPreset: Final[ModuleName[interfaces.LightPreset]] = ModuleName("LightPreset")
|
||||
Thermostat: Final[ModuleName[interfaces.Thermostat]] = ModuleName("Thermostat")
|
||||
Time: Final[ModuleName[interfaces.Time]] = ModuleName("Time")
|
||||
|
||||
# IOT only Modules
|
||||
|
@ -24,9 +24,11 @@ from .lightpreset import LightPreset
|
||||
from .lightstripeffect import LightStripEffect
|
||||
from .lighttransition import LightTransition
|
||||
from .motionsensor import MotionSensor
|
||||
from .overheatprotection import OverheatProtection
|
||||
from .reportmode import ReportMode
|
||||
from .temperaturecontrol import TemperatureControl
|
||||
from .temperaturesensor import TemperatureSensor
|
||||
from .thermostat import Thermostat
|
||||
from .time import Time
|
||||
from .triggerlogs import TriggerLogs
|
||||
from .waterleaksensor import WaterleakSensor
|
||||
@ -61,5 +63,7 @@ __all__ = [
|
||||
"MotionSensor",
|
||||
"TriggerLogs",
|
||||
"FrostProtection",
|
||||
"Thermostat",
|
||||
"SmartLightEffect",
|
||||
"OverheatProtection",
|
||||
]
|
||||
|
@ -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."""
|
||||
|
@ -75,8 +75,12 @@ class Energy(SmartModule, EnergyInterface):
|
||||
|
||||
async def get_status(self) -> EmeterStatus:
|
||||
"""Return real-time statistics."""
|
||||
res = await self.call("get_energy_usage")
|
||||
return self._get_status_from_energy(res["get_energy_usage"])
|
||||
if "get_emeter_data" in self.data:
|
||||
res = await self.call("get_emeter_data")
|
||||
return EmeterStatus(res["get_emeter_data"])
|
||||
else:
|
||||
res = await self.call("get_energy_usage")
|
||||
return self._get_status_from_energy(res["get_energy_usage"])
|
||||
|
||||
@property
|
||||
@raise_if_update_error
|
||||
|
@ -24,6 +24,7 @@ class LightTransition(SmartModule):
|
||||
REQUIRED_COMPONENT = "on_off_gradually"
|
||||
QUERY_GETTER_NAME = "get_on_off_gradually_info"
|
||||
MINIMUM_UPDATE_INTERVAL_SECS = 60
|
||||
# v3 added max_duration, we default to 60 when it's not available
|
||||
MAXIMUM_DURATION = 60
|
||||
|
||||
# Key in sysinfo that indicates state can be retrieved from there.
|
||||
@ -144,10 +145,22 @@ class LightTransition(SmartModule):
|
||||
return await self.call("set_on_off_gradually_info", {"enable": enable})
|
||||
else:
|
||||
on = await self.call(
|
||||
"set_on_off_gradually_info", {"on_state": {"enable": enable}}
|
||||
"set_on_off_gradually_info",
|
||||
{
|
||||
"on_state": {
|
||||
"enable": enable,
|
||||
"duration": self._on_state["duration"],
|
||||
}
|
||||
},
|
||||
)
|
||||
off = await self.call(
|
||||
"set_on_off_gradually_info", {"off_state": {"enable": enable}}
|
||||
"set_on_off_gradually_info",
|
||||
{
|
||||
"off_state": {
|
||||
"enable": enable,
|
||||
"duration": self._off_state["duration"],
|
||||
}
|
||||
},
|
||||
)
|
||||
return {**on, **off}
|
||||
|
||||
@ -167,7 +180,6 @@ class LightTransition(SmartModule):
|
||||
@property
|
||||
def _turn_on_transition_max(self) -> int:
|
||||
"""Maximum turn on duration."""
|
||||
# v3 added max_duration, we default to 60 when it's not available
|
||||
return self._on_state["max_duration"]
|
||||
|
||||
@allow_update_after
|
||||
@ -184,7 +196,7 @@ class LightTransition(SmartModule):
|
||||
if seconds <= 0:
|
||||
return await self.call(
|
||||
"set_on_off_gradually_info",
|
||||
{"on_state": {"enable": False}},
|
||||
{"on_state": {"enable": False, "duration": self._on_state["duration"]}},
|
||||
)
|
||||
|
||||
return await self.call(
|
||||
@ -220,7 +232,12 @@ class LightTransition(SmartModule):
|
||||
if seconds <= 0:
|
||||
return await self.call(
|
||||
"set_on_off_gradually_info",
|
||||
{"off_state": {"enable": False}},
|
||||
{
|
||||
"off_state": {
|
||||
"enable": False,
|
||||
"duration": self._off_state["duration"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return await self.call(
|
||||
|
41
kasa/smart/modules/overheatprotection.py
Normal file
41
kasa/smart/modules/overheatprotection.py
Normal file
@ -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 {}
|
@ -3,24 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from ...feature import Feature
|
||||
from ...interfaces.thermostat import ThermostatState
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatState(Enum):
|
||||
"""Thermostat state."""
|
||||
|
||||
Heating = "heating"
|
||||
Calibrating = "progress_calibration"
|
||||
Idle = "idle"
|
||||
Off = "off"
|
||||
Unknown = "unknown"
|
||||
|
||||
|
||||
class TemperatureControl(SmartModule):
|
||||
"""Implementation of temperature module."""
|
||||
|
||||
@ -56,7 +46,6 @@ class TemperatureControl(SmartModule):
|
||||
category=Feature.Category.Config,
|
||||
)
|
||||
)
|
||||
|
||||
self._add_feature(
|
||||
Feature(
|
||||
self._device,
|
||||
@ -69,7 +58,6 @@ class TemperatureControl(SmartModule):
|
||||
type=Feature.Type.Switch,
|
||||
)
|
||||
)
|
||||
|
||||
self._add_feature(
|
||||
Feature(
|
||||
self._device,
|
||||
|
74
kasa/smart/modules/thermostat.py
Normal file
74
kasa/smart/modules/thermostat.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""Module for a Thermostat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from ...feature import Feature
|
||||
from ...interfaces.thermostat import Thermostat as ThermostatInterface
|
||||
from ...interfaces.thermostat import ThermostatState
|
||||
from ...module import FeatureAttribute, Module
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
|
||||
class Thermostat(SmartModule, ThermostatInterface):
|
||||
"""Implementation of a Thermostat."""
|
||||
|
||||
@property
|
||||
def _all_features(self) -> dict[str, Feature]:
|
||||
"""Get the features for this module and any sub modules."""
|
||||
ret: dict[str, Feature] = {}
|
||||
if temp_control := self._device.modules.get(Module.TemperatureControl):
|
||||
ret.update(**temp_control._module_features)
|
||||
if temp_sensor := self._device.modules.get(Module.TemperatureSensor):
|
||||
ret.update(**temp_sensor._module_features)
|
||||
return ret
|
||||
|
||||
def query(self) -> dict:
|
||||
"""Query to execute during the update cycle."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def state(self) -> bool:
|
||||
"""Return thermostat state."""
|
||||
return self._device.modules[Module.TemperatureControl].state
|
||||
|
||||
async def set_state(self, enabled: bool) -> dict:
|
||||
"""Set thermostat state."""
|
||||
return await self._device.modules[Module.TemperatureControl].set_state(enabled)
|
||||
|
||||
@property
|
||||
def mode(self) -> ThermostatState:
|
||||
"""Return thermostat state."""
|
||||
return self._device.modules[Module.TemperatureControl].mode
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> Annotated[float, FeatureAttribute()]:
|
||||
"""Return target temperature."""
|
||||
return self._device.modules[Module.TemperatureControl].target_temperature
|
||||
|
||||
async def set_target_temperature(
|
||||
self, target: float
|
||||
) -> Annotated[dict, FeatureAttribute()]:
|
||||
"""Set target temperature."""
|
||||
return await self._device.modules[
|
||||
Module.TemperatureControl
|
||||
].set_target_temperature(target)
|
||||
|
||||
@property
|
||||
def temperature(self) -> Annotated[float, FeatureAttribute()]:
|
||||
"""Return current humidity in percentage."""
|
||||
return self._device.modules[Module.TemperatureSensor].temperature
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> Literal["celsius", "fahrenheit"]:
|
||||
"""Return current temperature unit."""
|
||||
return self._device.modules[Module.TemperatureSensor].temperature_unit
|
||||
|
||||
async def set_temperature_unit(
|
||||
self, unit: Literal["celsius", "fahrenheit"]
|
||||
) -> dict:
|
||||
"""Set the device temperature unit."""
|
||||
return await self._device.modules[
|
||||
Module.TemperatureSensor
|
||||
].set_temperature_unit(unit)
|
@ -24,6 +24,7 @@ from .modules import (
|
||||
DeviceModule,
|
||||
Firmware,
|
||||
Light,
|
||||
Thermostat,
|
||||
Time,
|
||||
)
|
||||
from .smartmodule import SmartModule
|
||||
@ -166,7 +167,14 @@ class SmartDevice(Device):
|
||||
self._last_update, "get_child_device_list", {}
|
||||
):
|
||||
for info in child_info["child_device_list"]:
|
||||
self._children[info["device_id"]]._update_internal_state(info)
|
||||
child_id = info["device_id"]
|
||||
if child_id not in self._children:
|
||||
_LOGGER.debug(
|
||||
"Skipping child update for %s, probably unsupported device",
|
||||
child_id,
|
||||
)
|
||||
continue
|
||||
self._children[child_id]._update_internal_state(info)
|
||||
|
||||
def _update_internal_info(self, info_resp: dict) -> None:
|
||||
"""Update the internal device info."""
|
||||
@ -341,9 +349,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.",
|
||||
@ -361,6 +368,11 @@ class SmartDevice(Device):
|
||||
or Module.ColorTemperature in self._modules
|
||||
):
|
||||
self._modules[Light.__name__] = Light(self, "light")
|
||||
if (
|
||||
Module.TemperatureControl in self._modules
|
||||
and Module.TemperatureSensor in self._modules
|
||||
):
|
||||
self._modules[Thermostat.__name__] = Thermostat(self, "thermostat")
|
||||
|
||||
async def _initialize_features(self) -> None:
|
||||
"""Initialize device features."""
|
||||
@ -427,19 +439,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:
|
||||
@ -759,10 +758,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(
|
||||
@ -796,6 +796,8 @@ class SmartDevice(Device):
|
||||
return DeviceType.Sensor
|
||||
if "ENERGY" in device_type:
|
||||
return DeviceType.Thermostat
|
||||
if "ROBOVAC" in device_type:
|
||||
return DeviceType.Vacuum
|
||||
_LOGGER.warning("Unknown device type, falling back to plug")
|
||||
return DeviceType.Plug
|
||||
|
||||
|
@ -54,8 +54,8 @@ 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
|
||||
|
||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from enum import StrEnum
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from ...credentials import Credentials
|
||||
@ -15,6 +16,14 @@ 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):
|
||||
@ -64,7 +73,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,17 +87,27 @@ class Camera(SmartCamModule):
|
||||
:return: rtsp url with escaped credentials or None if no credentials or
|
||||
camera is off.
|
||||
"""
|
||||
if not self.is_on:
|
||||
streams = {
|
||||
StreamResolution.HD: "stream1",
|
||||
StreamResolution.SD: "stream2",
|
||||
}
|
||||
if (stream := streams.get(stream_resolution)) is None:
|
||||
return None
|
||||
dev = self._device
|
||||
|
||||
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"
|
||||
|
||||
return f"rtsp://{username}:{password}@{self._device.host}:{LOCAL_STREAMING_PORT}/{stream}"
|
||||
|
||||
def onvif_url(self) -> str | None:
|
||||
"""Return the onvif url."""
|
||||
return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service"
|
||||
|
||||
async def set_state(self, on: bool) -> dict:
|
||||
"""Set the device state."""
|
||||
|
@ -68,7 +68,14 @@ class SmartCamDevice(SmartDevice):
|
||||
self._last_update, "getChildDeviceList", {}
|
||||
):
|
||||
for info in child_info["child_device_list"]:
|
||||
self._children[info["device_id"]]._update_internal_state(info)
|
||||
child_id = info["device_id"]
|
||||
if child_id not in self._children:
|
||||
_LOGGER.debug(
|
||||
"Skipping child update for %s, probably unsupported device",
|
||||
child_id,
|
||||
)
|
||||
continue
|
||||
self._children[child_id]._update_internal_state(info)
|
||||
|
||||
async def _initialize_smart_child(
|
||||
self, info: dict, child_components: dict
|
||||
@ -100,20 +107,29 @@ class SmartCamDevice(SmartDevice):
|
||||
resp = await self.protocol.query(child_info_query)
|
||||
self.internal_state.update(resp)
|
||||
|
||||
children_components = {
|
||||
smart_children_components = {
|
||||
child["device_id"]: {
|
||||
comp["id"]: int(comp["ver_code"]) for comp in child["component_list"]
|
||||
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:
|
||||
child_id = info["device_id"]
|
||||
(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, children_components[child_id]
|
||||
info, child_components
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("Child device type not supported: %s", info)
|
||||
@ -191,6 +207,7 @@ class SmartCamDevice(SmartDevice):
|
||||
"mac": basic_info["mac"],
|
||||
"hwId": basic_info.get("hw_id"),
|
||||
"oem_id": basic_info["oem_id"],
|
||||
"device_id": basic_info["dev_id"],
|
||||
}
|
||||
|
||||
@property
|
||||
|
@ -3,14 +3,18 @@
|
||||
from .aestransport import AesEncyptionSession, AesTransport
|
||||
from .basetransport import BaseTransport
|
||||
from .klaptransport import KlapTransport, KlapTransportV2
|
||||
from .linkietransport import LinkieTransportV2
|
||||
from .ssltransport import SslTransport
|
||||
from .xortransport import XorEncryption, XorTransport
|
||||
|
||||
__all__ = [
|
||||
"AesTransport",
|
||||
"AesEncyptionSession",
|
||||
"SslTransport",
|
||||
"BaseTransport",
|
||||
"KlapTransport",
|
||||
"KlapTransportV2",
|
||||
"LinkieTransportV2",
|
||||
"XorTransport",
|
||||
"XorEncryption",
|
||||
]
|
||||
|
143
kasa/transports/linkietransport.py
Normal file
143
kasa/transports/linkietransport.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""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."""
|
||||
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
|
233
kasa/transports/ssltransport.py
Normal file
233
kasa/transports/ssltransport.py
Normal file
@ -0,0 +1,233 @@
|
||||
"""Implementation of the clear-text passthrough ssl transport.
|
||||
|
||||
This transport does not encrypt the passthrough payloads at all, but requires a login.
|
||||
This has been seen on some devices (like robovacs).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
from enum import Enum, auto
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from kasa.credentials import DEFAULT_CREDENTIALS, Credentials, get_default_credentials
|
||||
from kasa.deviceconfig import DeviceConfig
|
||||
from kasa.exceptions import (
|
||||
SMART_AUTHENTICATION_ERRORS,
|
||||
SMART_RETRYABLE_ERRORS,
|
||||
AuthenticationError,
|
||||
DeviceError,
|
||||
KasaException,
|
||||
SmartErrorCode,
|
||||
_RetryableError,
|
||||
)
|
||||
from kasa.httpclient import HttpClient
|
||||
from kasa.json import dumps as json_dumps
|
||||
from kasa.json import loads as json_loads
|
||||
from kasa.transports import BaseTransport
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ONE_DAY_SECONDS = 86400
|
||||
SESSION_EXPIRE_BUFFER_SECONDS = 60 * 20
|
||||
|
||||
|
||||
def _md5_hash(payload: bytes) -> str:
|
||||
return hashlib.md5(payload).hexdigest().upper() # noqa: S324
|
||||
|
||||
|
||||
class TransportState(Enum):
|
||||
"""Enum for transport state."""
|
||||
|
||||
LOGIN_REQUIRED = auto() # Login needed
|
||||
ESTABLISHED = auto() # Ready to send requests
|
||||
|
||||
|
||||
class SslTransport(BaseTransport):
|
||||
"""Implementation of the cleartext transport protocol.
|
||||
|
||||
This transport uses HTTPS without any further payload encryption.
|
||||
"""
|
||||
|
||||
DEFAULT_PORT: int = 4433
|
||||
COMMON_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
BACKOFF_SECONDS_AFTER_LOGIN_ERROR = 1
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
config: DeviceConfig,
|
||||
) -> None:
|
||||
super().__init__(config=config)
|
||||
|
||||
if (
|
||||
not self._credentials or self._credentials.username is None
|
||||
) and not self._credentials_hash:
|
||||
self._credentials = Credentials()
|
||||
|
||||
if self._credentials:
|
||||
self._login_params = self._get_login_params(self._credentials)
|
||||
else:
|
||||
self._login_params = json_loads(
|
||||
base64.b64decode(self._credentials_hash.encode()).decode() # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
self._default_credentials: Credentials | None = None
|
||||
self._http_client: HttpClient = HttpClient(config)
|
||||
|
||||
self._state = TransportState.LOGIN_REQUIRED
|
||||
self._session_expire_at: float | None = None
|
||||
|
||||
self._app_url = URL(f"https://{self._host}:{self._port}/app")
|
||||
|
||||
_LOGGER.debug("Created ssltransport for %s", self._host)
|
||||
|
||||
@property
|
||||
def default_port(self) -> int:
|
||||
"""Default port for the transport."""
|
||||
return self.DEFAULT_PORT
|
||||
|
||||
@property
|
||||
def credentials_hash(self) -> str:
|
||||
"""The hashed credentials used by the transport."""
|
||||
return base64.b64encode(json_dumps(self._login_params).encode()).decode()
|
||||
|
||||
def _get_login_params(self, credentials: Credentials) -> dict[str, str]:
|
||||
"""Get the login parameters based on the login_version."""
|
||||
un, pw = self.hash_credentials(credentials)
|
||||
return {"password": pw, "username": un}
|
||||
|
||||
@staticmethod
|
||||
def hash_credentials(credentials: Credentials) -> tuple[str, str]:
|
||||
"""Hash the credentials."""
|
||||
un = credentials.username
|
||||
pw = _md5_hash(credentials.password.encode())
|
||||
return un, pw
|
||||
|
||||
async def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None:
|
||||
"""Handle response errors to request reauth etc."""
|
||||
error_code = SmartErrorCode(resp_dict.get("error_code")) # type: ignore[arg-type]
|
||||
if error_code == SmartErrorCode.SUCCESS:
|
||||
return
|
||||
|
||||
msg = f"{msg}: {self._host}: {error_code.name}({error_code.value})"
|
||||
|
||||
if error_code in SMART_RETRYABLE_ERRORS:
|
||||
raise _RetryableError(msg, error_code=error_code)
|
||||
|
||||
if error_code in SMART_AUTHENTICATION_ERRORS:
|
||||
await self.reset()
|
||||
raise AuthenticationError(msg, error_code=error_code)
|
||||
|
||||
raise DeviceError(msg, error_code=error_code)
|
||||
|
||||
async def send_request(self, request: str) -> dict[str, Any]:
|
||||
"""Send request."""
|
||||
url = self._app_url
|
||||
|
||||
_LOGGER.debug("Sending %s to %s", request, url)
|
||||
|
||||
status_code, resp_dict = await self._http_client.post(
|
||||
url,
|
||||
json=request,
|
||||
headers=self.COMMON_HEADERS,
|
||||
)
|
||||
|
||||
if status_code != 200:
|
||||
raise KasaException(
|
||||
f"{self._host} responded with an unexpected "
|
||||
+ f"status code {status_code}"
|
||||
)
|
||||
|
||||
_LOGGER.debug("Response with %s: %r", status_code, resp_dict)
|
||||
|
||||
await self._handle_response_error_code(resp_dict, "Error sending request")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
resp_dict = cast(dict[str, Any], resp_dict)
|
||||
|
||||
return resp_dict
|
||||
|
||||
async def perform_login(self) -> None:
|
||||
"""Login to the device."""
|
||||
try:
|
||||
await self.try_login(self._login_params)
|
||||
except AuthenticationError as aex:
|
||||
try:
|
||||
if aex.error_code is not SmartErrorCode.LOGIN_ERROR:
|
||||
raise aex
|
||||
|
||||
_LOGGER.debug("Login failed, going to try default credentials")
|
||||
if self._default_credentials is None:
|
||||
self._default_credentials = get_default_credentials(
|
||||
DEFAULT_CREDENTIALS["TAPO"]
|
||||
)
|
||||
await asyncio.sleep(self.BACKOFF_SECONDS_AFTER_LOGIN_ERROR)
|
||||
|
||||
await self.try_login(self._get_login_params(self._default_credentials))
|
||||
_LOGGER.debug(
|
||||
"%s: logged in with default credentials",
|
||||
self._host,
|
||||
)
|
||||
except AuthenticationError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise KasaException(
|
||||
"Unable to login and trying default "
|
||||
+ f"login raised another exception: {ex}",
|
||||
ex,
|
||||
) from ex
|
||||
|
||||
async def try_login(self, login_params: dict[str, Any]) -> None:
|
||||
"""Try to login with supplied login_params."""
|
||||
login_request = {
|
||||
"method": "login",
|
||||
"params": login_params,
|
||||
}
|
||||
request = json_dumps(login_request)
|
||||
_LOGGER.debug("Going to send login request")
|
||||
|
||||
resp_dict = await self.send_request(request)
|
||||
await self._handle_response_error_code(resp_dict, "Error logging in")
|
||||
|
||||
login_token = resp_dict["result"]["token"]
|
||||
self._app_url = self._app_url.with_query(f"token={login_token}")
|
||||
self._state = TransportState.ESTABLISHED
|
||||
self._session_expire_at = (
|
||||
time.time() + ONE_DAY_SECONDS - SESSION_EXPIRE_BUFFER_SECONDS
|
||||
)
|
||||
|
||||
def _session_expired(self) -> bool:
|
||||
"""Return true if session has expired."""
|
||||
return (
|
||||
self._session_expire_at is None
|
||||
or self._session_expire_at - time.time() <= 0
|
||||
)
|
||||
|
||||
async def send(self, request: str) -> dict[str, Any]:
|
||||
"""Send the request."""
|
||||
_LOGGER.info("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()
|
||||
|
||||
return await self.send_request(request)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the http client and reset internal state."""
|
||||
await self.reset()
|
||||
await self._http_client.close()
|
||||
|
||||
async def reset(self) -> None:
|
||||
"""Reset internal login state."""
|
||||
self._state = TransportState.LOGIN_REQUIRED
|
||||
self._app_url = URL(f"https://{self._host}:{self._port}/app")
|
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "python-kasa"
|
||||
version = "0.7.7"
|
||||
version = "0.8.1"
|
||||
description = "Python API for TP-Link Kasa and Tapo devices"
|
||||
license = {text = "GPL-3.0-or-later"}
|
||||
authors = [ { name = "python-kasa developers" }]
|
||||
@ -25,7 +25,13 @@ classifiers = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
speedups = ["orjson>=3.9.1", "kasa-crypt>=0.2.0"]
|
||||
docs = ["sphinx~=6.2", "sphinx_rtd_theme~=2.0", "sphinxcontrib-programoutput~=0.0", "myst-parser", "docutils>=0.17"]
|
||||
docs = [
|
||||
"sphinx_rtd_theme~=2.0",
|
||||
"sphinxcontrib-programoutput~=0.0",
|
||||
"myst-parser",
|
||||
"docutils>=0.17",
|
||||
"sphinx>=7.4.7",
|
||||
]
|
||||
shell = ["ptpython", "rich"]
|
||||
|
||||
[project.urls]
|
||||
|
@ -98,6 +98,7 @@ PLUGS = {
|
||||
SWITCHES_IOT = {
|
||||
"HS200",
|
||||
"HS210",
|
||||
"KS200",
|
||||
"KS200M",
|
||||
}
|
||||
SWITCHES_SMART = {
|
||||
@ -217,6 +218,9 @@ no_emeter = parametrize(
|
||||
model_filter=ALL_DEVICES - WITH_EMETER,
|
||||
protocol_filter={"SMART", "IOT"},
|
||||
)
|
||||
has_emeter_smart = parametrize(
|
||||
"has emeter smart", model_filter=WITH_EMETER_SMART, protocol_filter={"SMART"}
|
||||
)
|
||||
has_emeter_iot = parametrize(
|
||||
"has emeter iot", model_filter=WITH_EMETER_IOT, protocol_filter={"IOT"}
|
||||
)
|
||||
|
@ -22,6 +22,29 @@ class DiscoveryResponse(TypedDict):
|
||||
error_code: int
|
||||
|
||||
|
||||
UNSUPPORTED_HOMEWIFISYSTEM = {
|
||||
"error_code": 0,
|
||||
"result": {
|
||||
"channel_2g": "10",
|
||||
"channel_5g": "44",
|
||||
"device_id": "REDACTED_51f72a752213a6c45203530",
|
||||
"device_model": "X20",
|
||||
"device_type": "HOMEWIFISYSTEM",
|
||||
"factory_default": False,
|
||||
"group_id": "REDACTED_07d902da02fa9beab8a64",
|
||||
"group_name": "I01BU0tFRF9TU0lEIw==", # '#MASKED_SSID#'
|
||||
"hardware_version": "3.0",
|
||||
"ip": "192.168.1.192",
|
||||
"mac": "24:2F:D0:00:00:00",
|
||||
"master_device_id": "REDACTED_51f72a752213a6c45203530",
|
||||
"need_account_digest": True,
|
||||
"owner": "REDACTED_341c020d7e8bda184e56a90",
|
||||
"role": "master",
|
||||
"tmp_port": [20001],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _make_unsupported(
|
||||
device_family,
|
||||
encrypt_type,
|
||||
@ -75,13 +98,14 @@ UNSUPPORTED_DEVICES = {
|
||||
"unable_to_parse": _make_unsupported(
|
||||
"SMART.TAPOBULB",
|
||||
"FOO",
|
||||
omit_keys={"mgt_encrypt_schm": None},
|
||||
omit_keys={"device_id": None},
|
||||
),
|
||||
"invalidinstance": _make_unsupported(
|
||||
"IOT.SMARTPLUGSWITCH",
|
||||
"KLAP",
|
||||
https=True,
|
||||
),
|
||||
"homewifi": UNSUPPORTED_HOMEWIFISYSTEM,
|
||||
}
|
||||
|
||||
|
||||
@ -106,6 +130,8 @@ 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"}),
|
||||
|
@ -449,6 +449,17 @@ class FakeSmartTransport(BaseTransport):
|
||||
info["get_preset_rules"]["states"][params["index"]] = params["state"]
|
||||
return {"error_code": 0}
|
||||
|
||||
def _set_temperature_unit(self, info, params):
|
||||
"""Set or remove values as per the device behaviour."""
|
||||
unit = params["temp_unit"]
|
||||
if unit not in {"celsius", "fahrenheit"}:
|
||||
raise ValueError(f"Invalid value for temperature unit {unit}")
|
||||
if "temp_unit" not in info["get_device_info"]:
|
||||
return {"error_code": SmartErrorCode.UNKNOWN_METHOD_ERROR}
|
||||
else:
|
||||
info["get_device_info"]["temp_unit"] = unit
|
||||
return {"error_code": 0}
|
||||
|
||||
def _update_sysinfo_key(self, info: dict, key: str, value: str) -> dict:
|
||||
"""Update a single key in the main system info.
|
||||
|
||||
@ -551,6 +562,8 @@ class FakeSmartTransport(BaseTransport):
|
||||
return self._set_preset_rules(info, params)
|
||||
elif method == "edit_preset_rules":
|
||||
return self._edit_preset_rules(info, params)
|
||||
elif method == "set_temperature_unit":
|
||||
return self._set_temperature_unit(info, params)
|
||||
elif method == "set_on_off_gradually_info":
|
||||
return self._set_on_off_gradually_info(info, params)
|
||||
elif method == "set_child_protection":
|
||||
|
63
tests/fixtures/iot/KS200(US)_1.0_1.0.8.json
vendored
Normal file
63
tests/fixtures/iot/KS200(US)_1.0_1.0.8.json
vendored
Normal file
@ -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": "#MASKED_NAME#"
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
86
tests/fixtures/iotcam/EC60(US)_4.0_2.3.22.json
vendored
Normal file
86
tests/fixtures/iotcam/EC60(US)_4.0_2.3.22.json
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
614
tests/fixtures/smart/P110M(EU)_1.0_1.2.3.json
vendored
Normal file
614
tests/fixtures/smart/P110M(EU)_1.0_1.2.3.json
vendored
Normal file
@ -0,0 +1,614 @@
|
||||
{
|
||||
"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": "matter",
|
||||
"ver_code": 2
|
||||
},
|
||||
{
|
||||
"id": "current_protection",
|
||||
"ver_code": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": ""
|
||||
},
|
||||
"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": false,
|
||||
"random_range": 120,
|
||||
"time": 180
|
||||
},
|
||||
"get_connect_cloud_state": {
|
||||
"status": 1
|
||||
},
|
||||
"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": "",
|
||||
"charging_status": "normal",
|
||||
"default_states": {
|
||||
"state": {},
|
||||
"type": "last_states"
|
||||
},
|
||||
"device_id": "0000000000000000000000000000000000000000",
|
||||
"device_on": false,
|
||||
"fw_id": "00000000000000000000000000000000",
|
||||
"fw_ver": "1.2.3 Build 240617 Rel.153525",
|
||||
"has_set_location_info": false,
|
||||
"hw_id": "00000000000000000000000000000000",
|
||||
"hw_ver": "1.0",
|
||||
"ip": "127.0.0.123",
|
||||
"lang": "",
|
||||
"latitude": 0,
|
||||
"longitude": 0,
|
||||
"mac": "F0-A7-31-00-00-00",
|
||||
"model": "P110M",
|
||||
"nickname": "I01BU0tFRF9OQU1FIw==",
|
||||
"oem_id": "00000000000000000000000000000000",
|
||||
"on_time": 0,
|
||||
"overcurrent_status": "normal",
|
||||
"overheat_status": "normal",
|
||||
"power_protection_status": "normal",
|
||||
"region": "CET",
|
||||
"rssi": -33,
|
||||
"signal_level": 3,
|
||||
"specs": "",
|
||||
"ssid": "I01BU0tFRF9TU0lEIw==",
|
||||
"time_diff": 60,
|
||||
"type": "SMART.TAPOPLUG"
|
||||
},
|
||||
"get_device_time": {
|
||||
"region": "CET",
|
||||
"time_diff": 60,
|
||||
"timestamp": 1732361090
|
||||
},
|
||||
"get_device_usage": {
|
||||
"power_usage": {
|
||||
"past30": 7892,
|
||||
"past7": 1549,
|
||||
"today": 0
|
||||
},
|
||||
"saved_power": {
|
||||
"past30": 9381,
|
||||
"past7": 1362,
|
||||
"today": 0
|
||||
},
|
||||
"time_usage": {
|
||||
"past30": 17273,
|
||||
"past7": 2911,
|
||||
"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": 0,
|
||||
"energy_wh": 1469,
|
||||
"power_mw": 0,
|
||||
"voltage_mv": 233509
|
||||
},
|
||||
"get_emeter_vgain_igain": {
|
||||
"igain": 11299,
|
||||
"vgain": 124300
|
||||
},
|
||||
"get_energy_usage": {
|
||||
"current_power": 0,
|
||||
"electricity_charge": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"local_time": "2024-11-23 12:24:51",
|
||||
"month_energy": 6266,
|
||||
"month_runtime": 12705,
|
||||
"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_led_info": {
|
||||
"bri_config": {
|
||||
"bri_type": "overall",
|
||||
"overall_bri": 50
|
||||
},
|
||||
"led_rule": "always",
|
||||
"led_status": false,
|
||||
"night_mode": {
|
||||
"end_time": 420,
|
||||
"night_mode_type": "sunrise_sunset",
|
||||
"start_time": 1140,
|
||||
"sunrise_offset": 0,
|
||||
"sunset_offset": 0
|
||||
}
|
||||
},
|
||||
"get_matter_setup_info": {
|
||||
"setup_code": "00000000000",
|
||||
"setup_payload": "00:000000-000000000000"
|
||||
},
|
||||
"get_max_power": {
|
||||
"max_power": 3896
|
||||
},
|
||||
"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": 2,
|
||||
"ssid": "I01BU0tFRF9TU0lEIw=="
|
||||
},
|
||||
{
|
||||
"bssid": "000000000000",
|
||||
"channel": 0,
|
||||
"cipher_type": 0,
|
||||
"key_type": "none",
|
||||
"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": 0,
|
||||
"key_type": "none",
|
||||
"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": 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": 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=="
|
||||
}
|
||||
],
|
||||
"start_index": 0,
|
||||
"sum": 22,
|
||||
"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": "P110M",
|
||||
"device_type": "SMART.TAPOPLUG",
|
||||
"is_klap": true
|
||||
}
|
||||
}
|
||||
}
|
640
tests/fixtures/smart/P115(US)_1.0_1.1.3.json
vendored
Normal file
640
tests/fixtures/smart/P115(US)_1.0_1.1.3.json
vendored
Normal file
@ -0,0 +1,640 @@
|
||||
{
|
||||
"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": {
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
1026
tests/fixtures/smartcam/C520WS(US)_1.0_1.2.8.json
vendored
Normal file
1026
tests/fixtures/smartcam/C520WS(US)_1.0_1.2.8.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,25 +12,23 @@ from voluptuous import (
|
||||
|
||||
from kasa import Device, DeviceType, EmeterStatus, Module
|
||||
from kasa.interfaces.energy import Energy
|
||||
from kasa.iot import IotDevice, IotStrip
|
||||
from kasa.iot import IotStrip
|
||||
from kasa.iot.modules.emeter import Emeter
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smart.modules import Energy as SmartEnergyModule
|
||||
from kasa.smart.smartmodule import SmartModule
|
||||
|
||||
from .conftest import has_emeter, has_emeter_iot, no_emeter
|
||||
from tests.conftest import has_emeter_iot, no_emeter_iot
|
||||
|
||||
CURRENT_CONSUMPTION_SCHEMA = Schema(
|
||||
Any(
|
||||
{
|
||||
"voltage": Any(All(float, Range(min=0, max=300)), None),
|
||||
"power": Any(Coerce(float), None),
|
||||
"total": Any(Coerce(float), None),
|
||||
"current": Any(All(float), None),
|
||||
"voltage_mv": Any(All(float, Range(min=0, max=300000)), int, None),
|
||||
"power_mw": Any(Coerce(float), None),
|
||||
"total_wh": Any(Coerce(float), None),
|
||||
"current_ma": Any(All(float), int, None),
|
||||
"energy_wh": Any(Coerce(float), None),
|
||||
"total_wh": Any(Coerce(float), None),
|
||||
"voltage": Any(All(float, Range(min=0, max=300)), None),
|
||||
"power": Any(Coerce(float), None),
|
||||
"current": Any(All(float), None),
|
||||
"total": Any(Coerce(float), None),
|
||||
"energy": Any(Coerce(float), None),
|
||||
"slot_id": Any(Coerce(int), None),
|
||||
},
|
||||
None,
|
||||
@ -38,33 +36,30 @@ CURRENT_CONSUMPTION_SCHEMA = Schema(
|
||||
)
|
||||
|
||||
|
||||
@no_emeter
|
||||
@no_emeter_iot
|
||||
async def test_no_emeter(dev):
|
||||
assert not dev.has_emeter
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_realtime()
|
||||
# Only iot devices support the historical stats so other
|
||||
# devices will not implement the methods below
|
||||
if isinstance(dev, IotDevice):
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_daily()
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_monthly()
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.erase_emeter_stats()
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_daily()
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_monthly()
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.erase_emeter_stats()
|
||||
|
||||
|
||||
@has_emeter
|
||||
@has_emeter_iot
|
||||
async def test_get_emeter_realtime(dev):
|
||||
if isinstance(dev, SmartDevice):
|
||||
mod = SmartEnergyModule(dev, str(Module.Energy))
|
||||
if not await mod._check_supported():
|
||||
pytest.skip(f"Energy module not supported for {dev}.")
|
||||
|
||||
emeter = dev.modules[Module.Energy]
|
||||
|
||||
current_emeter = await emeter.get_status()
|
||||
# Check realtime query gets the same value as status property
|
||||
# iot _query_helper strips out the error code from module responses.
|
||||
# but it's not stripped out of the _modular_update queries.
|
||||
assert current_emeter == {k: v for k, v in emeter.status.items() if k != "err_code"}
|
||||
CURRENT_CONSUMPTION_SCHEMA(current_emeter)
|
||||
|
||||
|
||||
@ -130,7 +125,7 @@ async def test_emeter_status(dev):
|
||||
|
||||
|
||||
@pytest.mark.skip("not clearing your stats..")
|
||||
@has_emeter
|
||||
@has_emeter_iot
|
||||
async def test_erase_emeter_stats(dev):
|
||||
emeter = dev.modules[Module.Energy]
|
||||
|
||||
@ -185,37 +180,22 @@ async def test_emeter_daily():
|
||||
assert emeter.consumption_today == 0.500
|
||||
|
||||
|
||||
@has_emeter
|
||||
@has_emeter_iot
|
||||
async def test_supported(dev: Device):
|
||||
if isinstance(dev, SmartDevice):
|
||||
mod = SmartEnergyModule(dev, str(Module.Energy))
|
||||
if not await mod._check_supported():
|
||||
pytest.skip(f"Energy module not supported for {dev}.")
|
||||
energy_module = dev.modules.get(Module.Energy)
|
||||
assert energy_module
|
||||
|
||||
if isinstance(dev, IotDevice):
|
||||
info = (
|
||||
dev._last_update
|
||||
if not isinstance(dev, IotStrip)
|
||||
else dev.children[0].internal_state
|
||||
)
|
||||
emeter = info[energy_module._module]["get_realtime"]
|
||||
has_total = "total" in emeter or "total_wh" in emeter
|
||||
has_voltage_current = "voltage" in emeter or "voltage_mv" in emeter
|
||||
assert (
|
||||
energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is has_total
|
||||
)
|
||||
assert (
|
||||
energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT)
|
||||
is has_voltage_current
|
||||
)
|
||||
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
|
||||
else:
|
||||
assert isinstance(energy_module, SmartModule)
|
||||
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
|
||||
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
|
||||
if energy_module.supported_version < 2:
|
||||
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is False
|
||||
else:
|
||||
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is True
|
||||
info = (
|
||||
dev._last_update
|
||||
if not isinstance(dev, IotStrip)
|
||||
else dev.children[0].internal_state
|
||||
)
|
||||
emeter = info[energy_module._module]["get_realtime"]
|
||||
has_total = "total" in emeter or "total_wh" in emeter
|
||||
has_voltage_current = "voltage" in emeter or "voltage_mv" in emeter
|
||||
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is has_total
|
||||
assert (
|
||||
energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT)
|
||||
is has_voltage_current
|
||||
)
|
||||
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
|
320
tests/iot/test_iotbulb.py
Normal file
320
tests/iot/test_iotbulb.py
Normal file
@ -0,0 +1,320 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from voluptuous import (
|
||||
All,
|
||||
Boolean,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
from kasa import Device, IotLightPreset, KasaException, LightState, Module
|
||||
from kasa.iot import IotBulb, IotDimmer
|
||||
from kasa.iot.modules import LightPreset as IotLightPresetModule
|
||||
from tests.conftest import (
|
||||
bulb_iot,
|
||||
color_bulb_iot,
|
||||
dimmable_iot,
|
||||
handle_turn_on,
|
||||
non_dimmable_iot,
|
||||
turn_on,
|
||||
variable_temp_iot,
|
||||
)
|
||||
from tests.iot.test_iotdevice import SYSINFO_SCHEMA
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_bulb_sysinfo(dev: Device):
|
||||
assert dev.sys_info is not None
|
||||
SYSINFO_SCHEMA_BULB(dev.sys_info)
|
||||
|
||||
assert dev.model is not None
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_light_state_without_update(dev: IotBulb, monkeypatch):
|
||||
monkeypatch.setitem(dev._last_update["system"]["get_sysinfo"], "light_state", None)
|
||||
with pytest.raises(KasaException):
|
||||
print(dev.light_state)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_get_light_state(dev: IotBulb):
|
||||
LIGHT_STATE_SCHEMA(await dev.get_light_state())
|
||||
|
||||
|
||||
@color_bulb_iot
|
||||
async def test_set_hsv_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
await light.set_hsv(10, 10, 100, transition=1000)
|
||||
|
||||
set_light_state.assert_called_with(
|
||||
{"hue": 10, "saturation": 10, "brightness": 100, "color_temp": 0},
|
||||
transition=1000,
|
||||
)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_light_set_state(dev: IotBulb, mocker):
|
||||
"""Testing setting LightState on the light module."""
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
set_light_state = mocker.spy(dev, "_set_light_state")
|
||||
state = LightState(light_on=True)
|
||||
await light.set_state(state)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 1}, transition=None)
|
||||
state = LightState(light_on=False)
|
||||
await light.set_state(state)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 0}, transition=None)
|
||||
|
||||
|
||||
@variable_temp_iot
|
||||
async def test_set_color_temp_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
await light.set_color_temp(2700, transition=100)
|
||||
|
||||
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
|
||||
|
||||
|
||||
@variable_temp_iot
|
||||
@pytest.mark.xdist_group(name="caplog")
|
||||
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)
|
||||
assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text
|
||||
|
||||
|
||||
@dimmable_iot
|
||||
@turn_on
|
||||
async def test_dimmable_brightness(dev: IotBulb, turn_on):
|
||||
assert isinstance(dev, IotBulb | IotDimmer)
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
await handle_turn_on(dev, turn_on)
|
||||
assert dev._is_dimmable
|
||||
|
||||
await light.set_brightness(50)
|
||||
await dev.update()
|
||||
assert light.brightness == 50
|
||||
|
||||
await light.set_brightness(10)
|
||||
await dev.update()
|
||||
assert light.brightness == 10
|
||||
|
||||
with pytest.raises(TypeError, match="Brightness must be an integer"):
|
||||
await light.set_brightness("foo") # type: ignore[arg-type]
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_turn_on_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
await dev.turn_on(transition=1000)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 1}, transition=1000)
|
||||
|
||||
await dev.turn_off(transition=100)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 0}, transition=100)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
await light.set_brightness(10, transition=1000)
|
||||
|
||||
set_light_state.assert_called_with({"brightness": 10, "on_off": 1}, transition=1000)
|
||||
|
||||
|
||||
@dimmable_iot
|
||||
async def test_invalid_brightness(dev: IotBulb):
|
||||
assert dev._is_dimmable
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=re.escape("Invalid brightness value: 110 (valid range: 0-100%)"),
|
||||
):
|
||||
await light.set_brightness(110)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=re.escape("Invalid brightness value: -100 (valid range: 0-100%)"),
|
||||
):
|
||||
await light.set_brightness(-100)
|
||||
|
||||
|
||||
@non_dimmable_iot
|
||||
async def test_non_dimmable(dev: IotBulb):
|
||||
assert not dev._is_dimmable
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
with pytest.raises(KasaException):
|
||||
assert light.brightness == 0
|
||||
with pytest.raises(KasaException):
|
||||
await light.set_brightness(100)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_ignore_default_not_set_without_color_mode_change_turn_on(
|
||||
dev: IotBulb, mocker
|
||||
):
|
||||
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
|
||||
# When turning back without settings, ignore default to restore the state
|
||||
await dev.turn_on()
|
||||
args, kwargs = query_helper.call_args_list[0]
|
||||
assert args[2] == {"on_off": 1, "ignore_default": 0}
|
||||
|
||||
await dev.turn_off()
|
||||
args, kwargs = query_helper.call_args_list[1]
|
||||
assert args[2] == {"on_off": 0, "ignore_default": 1}
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_list_presets(dev: IotBulb):
|
||||
light_preset = dev.modules.get(Module.LightPreset)
|
||||
assert light_preset
|
||||
assert isinstance(light_preset, IotLightPresetModule)
|
||||
presets = light_preset._deprecated_presets
|
||||
# Light strip devices may list some light effects along with normal presets but these
|
||||
# are handled by the LightEffect module so exclude preferred states with id
|
||||
raw_presets = [
|
||||
pstate for pstate in dev.sys_info["preferred_state"] if "id" not in pstate
|
||||
]
|
||||
assert len(presets) == len(raw_presets)
|
||||
|
||||
for preset, raw in zip(presets, raw_presets, strict=False):
|
||||
assert preset.index == raw["index"]
|
||||
assert preset.brightness == raw["brightness"]
|
||||
assert preset.hue == raw["hue"]
|
||||
assert preset.saturation == raw["saturation"]
|
||||
assert preset.color_temp == raw["color_temp"]
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_modify_preset(dev: IotBulb, mocker):
|
||||
"""Verify that modifying preset calls the and exceptions are raised properly."""
|
||||
if (
|
||||
not (light_preset := dev.modules.get(Module.LightPreset))
|
||||
or not light_preset._deprecated_presets
|
||||
):
|
||||
pytest.skip("Some strips do not support presets")
|
||||
|
||||
assert isinstance(light_preset, IotLightPresetModule)
|
||||
data: dict[str, int | None] = {
|
||||
"index": 0,
|
||||
"brightness": 10,
|
||||
"hue": 0,
|
||||
"saturation": 0,
|
||||
"color_temp": 0,
|
||||
}
|
||||
preset = IotLightPreset(**data) # type: ignore[call-arg, arg-type]
|
||||
|
||||
assert preset.index == 0
|
||||
assert preset.brightness == 10
|
||||
assert preset.hue == 0
|
||||
assert preset.saturation == 0
|
||||
assert preset.color_temp == 0
|
||||
|
||||
await light_preset._deprecated_save_preset(preset)
|
||||
await dev.update()
|
||||
assert light_preset._deprecated_presets[0].brightness == 10
|
||||
|
||||
with pytest.raises(KasaException):
|
||||
await light_preset._deprecated_save_preset(
|
||||
IotLightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
@pytest.mark.parametrize(
|
||||
("preset", "payload"),
|
||||
[
|
||||
(
|
||||
IotLightPreset(index=0, hue=0, brightness=1, saturation=0), # type: ignore[call-arg]
|
||||
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
|
||||
),
|
||||
(
|
||||
IotLightPreset(index=0, brightness=1, id="testid", mode=2, custom=0), # type: ignore[call-arg]
|
||||
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_modify_preset_payloads(dev: IotBulb, preset, payload, mocker):
|
||||
"""Test that modify preset payloads ignore none values."""
|
||||
if (
|
||||
not (light_preset := dev.modules.get(Module.LightPreset))
|
||||
or not light_preset._deprecated_presets
|
||||
):
|
||||
pytest.skip("Some strips do not support presets")
|
||||
|
||||
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
|
||||
await light_preset._deprecated_save_preset(preset)
|
||||
query_helper.assert_called_with(dev.LIGHT_SERVICE, "set_preferred_state", payload)
|
||||
|
||||
|
||||
LIGHT_STATE_SCHEMA = Schema(
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": int,
|
||||
"hue": All(int, Range(min=0, max=360)),
|
||||
"mode": str,
|
||||
"on_off": Boolean,
|
||||
"saturation": All(int, Range(min=0, max=100)),
|
||||
"length": Optional(int),
|
||||
"transition": Optional(int),
|
||||
"dft_on_state": Optional(
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": All(int, Range(min=0, max=9000)),
|
||||
"hue": All(int, Range(min=0, max=360)),
|
||||
"mode": str,
|
||||
"saturation": All(int, Range(min=0, max=100)),
|
||||
"groups": Optional(list[int]),
|
||||
}
|
||||
),
|
||||
"err_code": int,
|
||||
}
|
||||
)
|
||||
|
||||
SYSINFO_SCHEMA_BULB = SYSINFO_SCHEMA.extend(
|
||||
{
|
||||
"ctrl_protocols": Optional(dict),
|
||||
"description": Optional(str), # Seen on LBxxx, similar to dev_name
|
||||
"dev_state": str,
|
||||
"disco_ver": str,
|
||||
"heapsize": int,
|
||||
"is_color": Boolean,
|
||||
"is_dimmable": Boolean,
|
||||
"is_factory": Boolean,
|
||||
"is_variable_color_temp": Boolean,
|
||||
"light_state": LIGHT_STATE_SCHEMA,
|
||||
"preferred_state": [
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": int,
|
||||
"hue": All(int, Range(min=0, max=360)),
|
||||
"index": int,
|
||||
"saturation": All(int, Range(min=0, max=100)),
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_turn_on_behaviours(dev: IotBulb):
|
||||
behavior = await dev.get_turn_on_behavior()
|
||||
assert behavior
|
@ -19,10 +19,9 @@ from voluptuous import (
|
||||
from kasa import DeviceType, KasaException, Module
|
||||
from kasa.iot import IotDevice
|
||||
from kasa.iot.iotmodule import _merge_dict
|
||||
|
||||
from .conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
|
||||
from .device_fixtures import device_iot, has_emeter_iot, no_emeter_iot
|
||||
from .fakeprotocol_iot import FakeIotProtocol
|
||||
from tests.conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
|
||||
from tests.device_fixtures import device_iot, has_emeter_iot, no_emeter_iot
|
||||
from tests.fakeprotocol_iot import FakeIotProtocol
|
||||
|
||||
TZ_SCHEMA = Schema(
|
||||
{"zone_str": str, "dst_offset": int, "index": All(int, Range(min=0)), "tz_str": str}
|
@ -2,8 +2,7 @@ import pytest
|
||||
|
||||
from kasa import DeviceType, Module
|
||||
from kasa.iot import IotDimmer
|
||||
|
||||
from .conftest import dimmer_iot, handle_turn_on, turn_on
|
||||
from tests.conftest import dimmer_iot, handle_turn_on, turn_on
|
||||
|
||||
|
||||
@dimmer_iot
|
@ -3,8 +3,7 @@ import pytest
|
||||
from kasa import DeviceType, Module
|
||||
from kasa.iot import IotLightStrip
|
||||
from kasa.iot.modules import LightEffect
|
||||
|
||||
from .conftest import lightstrip_iot
|
||||
from tests.conftest import lightstrip_iot
|
||||
|
||||
|
||||
@lightstrip_iot
|
@ -29,8 +29,8 @@ from kasa.transports.basetransport import BaseTransport
|
||||
from kasa.transports.klaptransport import KlapTransport, KlapTransportV2
|
||||
from kasa.transports.xortransport import XorEncryption, XorTransport
|
||||
|
||||
from .conftest import device_iot
|
||||
from .fakeprotocol_iot import FakeIotTransport
|
||||
from ..conftest import device_iot
|
||||
from ..fakeprotocol_iot import FakeIotTransport
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
@ -12,8 +12,8 @@ from kasa.exceptions import (
|
||||
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from .conftest import device_smart
|
||||
from .fakeprotocol_smart import FakeSmartTransport
|
||||
from ..conftest import device_smart
|
||||
from ..fakeprotocol_smart import FakeSmartTransport
|
||||
|
||||
DUMMY_QUERY = {"foobar": {"foo": "bar", "bar": "foo"}}
|
||||
DUMMY_MULTIPLE_QUERY = {
|
@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
@ -25,10 +24,6 @@ autooff = parametrize(
|
||||
("auto_off_at", "auto_off_at", datetime | None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 10),
|
||||
reason="Subscripted generics cannot be used with class and instance checks",
|
||||
)
|
||||
async def test_autooff_features(
|
||||
dev: SmartDevice, feature: str, prop_name: str, type: type
|
||||
):
|
||||
|
21
tests/smart/modules/test_energy.py
Normal file
21
tests/smart/modules/test_energy.py
Normal file
@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from kasa import Module, SmartDevice
|
||||
from kasa.interfaces.energy import Energy
|
||||
from kasa.smart.modules import Energy as SmartEnergyModule
|
||||
from tests.conftest import has_emeter_smart
|
||||
|
||||
|
||||
@has_emeter_smart
|
||||
async def test_supported(dev: SmartDevice):
|
||||
energy_module = dev.modules.get(Module.Energy)
|
||||
if not energy_module:
|
||||
pytest.skip(f"Energy module not supported for {dev}.")
|
||||
|
||||
assert isinstance(energy_module, SmartEnergyModule)
|
||||
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
|
||||
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
|
||||
if energy_module.supported_version < 2:
|
||||
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is False
|
||||
else:
|
||||
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is True
|
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, cast
|
||||
@ -11,18 +12,20 @@ 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 .conftest import (
|
||||
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
|
||||
|
||||
|
||||
@device_smart
|
||||
@ -51,6 +54,31 @@ 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"<DeviceType.Unknown at {DISCOVERY_MOCK_IP} - update() needed>"
|
||||
|
||||
discovery_result = copy.deepcopy(discovery_mock.discovery_data["result"])
|
||||
dev.update_from_discover_info(discovery_result)
|
||||
assert dev.device_type is DeviceType.Unknown
|
||||
assert (
|
||||
repr(dev)
|
||||
== f"<DeviceType.Unknown at {DISCOVERY_MOCK_IP} - None (None) - update() needed>"
|
||||
)
|
||||
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"<DeviceType.Plug at {DISCOVERY_MOCK_IP} - None (None) - update() needed>"
|
||||
)
|
||||
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."""
|
||||
@ -435,3 +463,68 @@ async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixt
|
||||
):
|
||||
await new_dev.update()
|
||||
assert new_dev.is_cloud_connected is False
|
||||
|
||||
|
||||
@variable_temp_smart
|
||||
async def test_smart_temp_range(dev: Device):
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
assert light.valid_temperature_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
|
0
tests/smartcam/modules/__init__.py
Normal file
0
tests/smartcam/modules/__init__.py
Normal file
@ -4,15 +4,13 @@ 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 camera_smartcam, device_smartcam
|
||||
|
||||
|
||||
@device_smartcam
|
||||
@ -37,6 +35,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 +83,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
|
||||
|
||||
@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"
|
61
tests/smartcam/test_smartcamdevice.py
Normal file
61
tests/smartcam/test_smartcamdevice.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""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")
|
||||
|
||||
state = dev.is_on
|
||||
await dev.set_state(not state)
|
||||
await dev.update()
|
||||
assert dev.is_on is not state
|
||||
|
||||
|
||||
@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
|
@ -1,44 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from voluptuous import (
|
||||
All,
|
||||
Boolean,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
from kasa import Device, DeviceType, IotLightPreset, KasaException, LightState, Module
|
||||
from kasa.iot import IotBulb, IotDimmer
|
||||
from kasa.iot.modules import LightPreset as IotLightPresetModule
|
||||
|
||||
from .conftest import (
|
||||
from kasa import Device, DeviceType, KasaException, Module
|
||||
from tests.conftest import handle_turn_on, turn_on
|
||||
from tests.device_fixtures import (
|
||||
bulb,
|
||||
bulb_iot,
|
||||
color_bulb,
|
||||
color_bulb_iot,
|
||||
dimmable_iot,
|
||||
handle_turn_on,
|
||||
non_color_bulb,
|
||||
non_dimmable_iot,
|
||||
non_variable_temp,
|
||||
turn_on,
|
||||
variable_temp,
|
||||
variable_temp_iot,
|
||||
variable_temp_smart,
|
||||
)
|
||||
from .test_iotdevice import SYSINFO_SCHEMA
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_bulb_sysinfo(dev: Device):
|
||||
assert dev.sys_info is not None
|
||||
SYSINFO_SCHEMA_BULB(dev.sys_info)
|
||||
|
||||
assert dev.model is not None
|
||||
|
||||
|
||||
@bulb
|
||||
@ -47,18 +19,6 @@ async def test_state_attributes(dev: Device):
|
||||
assert isinstance(dev.state_information["Cloud connection"], bool)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_light_state_without_update(dev: IotBulb, monkeypatch):
|
||||
monkeypatch.setitem(dev._last_update["system"]["get_sysinfo"], "light_state", None)
|
||||
with pytest.raises(KasaException):
|
||||
print(dev.light_state)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_get_light_state(dev: IotBulb):
|
||||
LIGHT_STATE_SCHEMA(await dev.get_light_state())
|
||||
|
||||
|
||||
@color_bulb
|
||||
@turn_on
|
||||
async def test_hsv(dev: Device, turn_on):
|
||||
@ -81,35 +41,6 @@ async def test_hsv(dev: Device, turn_on):
|
||||
assert brightness == 1
|
||||
|
||||
|
||||
@color_bulb_iot
|
||||
async def test_set_hsv_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
await light.set_hsv(10, 10, 100, transition=1000)
|
||||
|
||||
set_light_state.assert_called_with(
|
||||
{"hue": 10, "saturation": 10, "brightness": 100, "color_temp": 0},
|
||||
transition=1000,
|
||||
)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_light_set_state(dev: IotBulb, mocker):
|
||||
"""Testing setting LightState on the light module."""
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
set_light_state = mocker.spy(dev, "_set_light_state")
|
||||
state = LightState(light_on=True)
|
||||
await light.set_state(state)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 1}, transition=None)
|
||||
state = LightState(light_on=False)
|
||||
await light.set_state(state)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 0}, transition=None)
|
||||
|
||||
|
||||
@color_bulb
|
||||
@turn_on
|
||||
@pytest.mark.parametrize(
|
||||
@ -221,33 +152,6 @@ async def test_try_set_colortemp(dev: Device, turn_on):
|
||||
assert light.color_temp == 2700
|
||||
|
||||
|
||||
@variable_temp_iot
|
||||
async def test_set_color_temp_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
await light.set_color_temp(2700, transition=100)
|
||||
|
||||
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
|
||||
|
||||
|
||||
@variable_temp_iot
|
||||
@pytest.mark.xdist_group(name="caplog")
|
||||
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)
|
||||
assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text
|
||||
|
||||
|
||||
@variable_temp_smart
|
||||
async def test_smart_temp_range(dev: Device):
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
assert light.valid_temperature_range
|
||||
|
||||
|
||||
@variable_temp
|
||||
async def test_out_of_range_temperature(dev: Device):
|
||||
light = dev.modules.get(Module.Light)
|
||||
@ -276,231 +180,6 @@ async def test_non_variable_temp(dev: Device):
|
||||
print(light.color_temp)
|
||||
|
||||
|
||||
@dimmable_iot
|
||||
@turn_on
|
||||
async def test_dimmable_brightness(dev: IotBulb, turn_on):
|
||||
assert isinstance(dev, IotBulb | IotDimmer)
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
await handle_turn_on(dev, turn_on)
|
||||
assert dev._is_dimmable
|
||||
|
||||
await light.set_brightness(50)
|
||||
await dev.update()
|
||||
assert light.brightness == 50
|
||||
|
||||
await light.set_brightness(10)
|
||||
await dev.update()
|
||||
assert light.brightness == 10
|
||||
|
||||
with pytest.raises(TypeError, match="Brightness must be an integer"):
|
||||
await light.set_brightness("foo") # type: ignore[arg-type]
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_turn_on_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
await dev.turn_on(transition=1000)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 1}, transition=1000)
|
||||
|
||||
await dev.turn_off(transition=100)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 0}, transition=100)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
await light.set_brightness(10, transition=1000)
|
||||
|
||||
set_light_state.assert_called_with({"brightness": 10, "on_off": 1}, transition=1000)
|
||||
|
||||
|
||||
@dimmable_iot
|
||||
async def test_invalid_brightness(dev: IotBulb):
|
||||
assert dev._is_dimmable
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=re.escape("Invalid brightness value: 110 (valid range: 0-100%)"),
|
||||
):
|
||||
await light.set_brightness(110)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=re.escape("Invalid brightness value: -100 (valid range: 0-100%)"),
|
||||
):
|
||||
await light.set_brightness(-100)
|
||||
|
||||
|
||||
@non_dimmable_iot
|
||||
async def test_non_dimmable(dev: IotBulb):
|
||||
assert not dev._is_dimmable
|
||||
light = dev.modules.get(Module.Light)
|
||||
assert light
|
||||
with pytest.raises(KasaException):
|
||||
assert light.brightness == 0
|
||||
with pytest.raises(KasaException):
|
||||
await light.set_brightness(100)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_ignore_default_not_set_without_color_mode_change_turn_on(
|
||||
dev: IotBulb, mocker
|
||||
):
|
||||
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
|
||||
# When turning back without settings, ignore default to restore the state
|
||||
await dev.turn_on()
|
||||
args, kwargs = query_helper.call_args_list[0]
|
||||
assert args[2] == {"on_off": 1, "ignore_default": 0}
|
||||
|
||||
await dev.turn_off()
|
||||
args, kwargs = query_helper.call_args_list[1]
|
||||
assert args[2] == {"on_off": 0, "ignore_default": 1}
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_list_presets(dev: IotBulb):
|
||||
light_preset = dev.modules.get(Module.LightPreset)
|
||||
assert light_preset
|
||||
assert isinstance(light_preset, IotLightPresetModule)
|
||||
presets = light_preset._deprecated_presets
|
||||
# Light strip devices may list some light effects along with normal presets but these
|
||||
# are handled by the LightEffect module so exclude preferred states with id
|
||||
raw_presets = [
|
||||
pstate for pstate in dev.sys_info["preferred_state"] if "id" not in pstate
|
||||
]
|
||||
assert len(presets) == len(raw_presets)
|
||||
|
||||
for preset, raw in zip(presets, raw_presets, strict=False):
|
||||
assert preset.index == raw["index"]
|
||||
assert preset.brightness == raw["brightness"]
|
||||
assert preset.hue == raw["hue"]
|
||||
assert preset.saturation == raw["saturation"]
|
||||
assert preset.color_temp == raw["color_temp"]
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_modify_preset(dev: IotBulb, mocker):
|
||||
"""Verify that modifying preset calls the and exceptions are raised properly."""
|
||||
if (
|
||||
not (light_preset := dev.modules.get(Module.LightPreset))
|
||||
or not light_preset._deprecated_presets
|
||||
):
|
||||
pytest.skip("Some strips do not support presets")
|
||||
|
||||
assert isinstance(light_preset, IotLightPresetModule)
|
||||
data: dict[str, int | None] = {
|
||||
"index": 0,
|
||||
"brightness": 10,
|
||||
"hue": 0,
|
||||
"saturation": 0,
|
||||
"color_temp": 0,
|
||||
}
|
||||
preset = IotLightPreset(**data) # type: ignore[call-arg, arg-type]
|
||||
|
||||
assert preset.index == 0
|
||||
assert preset.brightness == 10
|
||||
assert preset.hue == 0
|
||||
assert preset.saturation == 0
|
||||
assert preset.color_temp == 0
|
||||
|
||||
await light_preset._deprecated_save_preset(preset)
|
||||
await dev.update()
|
||||
assert light_preset._deprecated_presets[0].brightness == 10
|
||||
|
||||
with pytest.raises(KasaException):
|
||||
await light_preset._deprecated_save_preset(
|
||||
IotLightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
|
||||
@bulb_iot
|
||||
@pytest.mark.parametrize(
|
||||
("preset", "payload"),
|
||||
[
|
||||
(
|
||||
IotLightPreset(index=0, hue=0, brightness=1, saturation=0), # type: ignore[call-arg]
|
||||
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
|
||||
),
|
||||
(
|
||||
IotLightPreset(index=0, brightness=1, id="testid", mode=2, custom=0), # type: ignore[call-arg]
|
||||
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_modify_preset_payloads(dev: IotBulb, preset, payload, mocker):
|
||||
"""Test that modify preset payloads ignore none values."""
|
||||
if (
|
||||
not (light_preset := dev.modules.get(Module.LightPreset))
|
||||
or not light_preset._deprecated_presets
|
||||
):
|
||||
pytest.skip("Some strips do not support presets")
|
||||
|
||||
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
|
||||
await light_preset._deprecated_save_preset(preset)
|
||||
query_helper.assert_called_with(dev.LIGHT_SERVICE, "set_preferred_state", payload)
|
||||
|
||||
|
||||
LIGHT_STATE_SCHEMA = Schema(
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": int,
|
||||
"hue": All(int, Range(min=0, max=360)),
|
||||
"mode": str,
|
||||
"on_off": Boolean,
|
||||
"saturation": All(int, Range(min=0, max=100)),
|
||||
"length": Optional(int),
|
||||
"transition": Optional(int),
|
||||
"dft_on_state": Optional(
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": All(int, Range(min=0, max=9000)),
|
||||
"hue": All(int, Range(min=0, max=360)),
|
||||
"mode": str,
|
||||
"saturation": All(int, Range(min=0, max=100)),
|
||||
"groups": Optional(list[int]),
|
||||
}
|
||||
),
|
||||
"err_code": int,
|
||||
}
|
||||
)
|
||||
|
||||
SYSINFO_SCHEMA_BULB = SYSINFO_SCHEMA.extend(
|
||||
{
|
||||
"ctrl_protocols": Optional(dict),
|
||||
"description": Optional(str), # Seen on LBxxx, similar to dev_name
|
||||
"dev_state": str,
|
||||
"disco_ver": str,
|
||||
"heapsize": int,
|
||||
"is_color": Boolean,
|
||||
"is_dimmable": Boolean,
|
||||
"is_factory": Boolean,
|
||||
"is_variable_color_temp": Boolean,
|
||||
"light_state": LIGHT_STATE_SCHEMA,
|
||||
"preferred_state": [
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": int,
|
||||
"hue": All(int, Range(min=0, max=360)),
|
||||
"index": int,
|
||||
"saturation": All(int, Range(min=0, max=100)),
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bulb
|
||||
def test_device_type_bulb(dev: Device):
|
||||
assert dev.device_type in {DeviceType.Bulb, DeviceType.LightStrip}
|
||||
|
||||
|
||||
@bulb_iot
|
||||
async def test_turn_on_behaviours(dev: IotBulb):
|
||||
behavior = await dev.get_turn_on_behavior()
|
||||
assert behavior
|
||||
|
@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from unittest.mock import ANY
|
||||
from unittest.mock import ANY, PropertyMock, patch
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import asyncclick as click
|
||||
@ -40,10 +40,11 @@ from kasa.cli.light import (
|
||||
)
|
||||
from kasa.cli.main import TYPES, _legacy_type_to_class, cli, cmd_command, raw_command
|
||||
from kasa.cli.time import time
|
||||
from kasa.cli.usage import emeter, energy
|
||||
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
|
||||
|
||||
@ -126,6 +127,36 @@ async def test_list_devices(discovery_mock, runner):
|
||||
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()
|
||||
|
||||
|
||||
@new_discovery
|
||||
async def test_list_auth_failed(discovery_mock, mocker, runner):
|
||||
"""Test that device update is called on main."""
|
||||
@ -432,38 +463,45 @@ async def test_time_set(dev: Device, mocker, runner):
|
||||
|
||||
|
||||
async def test_emeter(dev: Device, mocker, runner):
|
||||
res = await runner.invoke(emeter, obj=dev)
|
||||
mocker.patch("kasa.Discover.discover_single", return_value=dev)
|
||||
base_cmd = ["--host", "dummy", "energy"]
|
||||
res = await runner.invoke(cli, base_cmd, obj=dev)
|
||||
if not (energy := dev.modules.get(Module.Energy)):
|
||||
assert "Device has no energy module." in res.output
|
||||
return
|
||||
|
||||
assert "== Emeter ==" in res.output
|
||||
assert "== Energy ==" in res.output
|
||||
|
||||
if dev.device_type is not DeviceType.Strip:
|
||||
res = await runner.invoke(emeter, ["--index", "0"], obj=dev)
|
||||
res = await runner.invoke(cli, [*base_cmd, "--index", "0"], obj=dev)
|
||||
assert f"Device: {dev.host} does not have children" in res.output
|
||||
res = await runner.invoke(emeter, ["--name", "mock"], obj=dev)
|
||||
res = await runner.invoke(cli, [*base_cmd, "--name", "mock"], obj=dev)
|
||||
assert f"Device: {dev.host} does not have children" in res.output
|
||||
|
||||
if dev.device_type is DeviceType.Strip and len(dev.children) > 0:
|
||||
child_energy = dev.children[0].modules.get(Module.Energy)
|
||||
assert child_energy
|
||||
realtime_emeter = mocker.patch.object(child_energy, "get_status")
|
||||
realtime_emeter.return_value = EmeterStatus({"voltage_mv": 122066})
|
||||
|
||||
res = await runner.invoke(emeter, ["--index", "0"], obj=dev)
|
||||
assert "Voltage: 122.066 V" in res.output
|
||||
realtime_emeter.assert_called()
|
||||
assert realtime_emeter.call_count == 1
|
||||
with patch.object(
|
||||
type(child_energy), "status", new_callable=PropertyMock
|
||||
) as child_status:
|
||||
child_status.return_value = EmeterStatus({"voltage_mv": 122066})
|
||||
|
||||
res = await runner.invoke(emeter, ["--name", dev.children[0].alias], obj=dev)
|
||||
assert "Voltage: 122.066 V" in res.output
|
||||
assert realtime_emeter.call_count == 2
|
||||
res = await runner.invoke(cli, [*base_cmd, "--index", "0"], obj=dev)
|
||||
assert "Voltage: 122.066 V" in res.output
|
||||
child_status.assert_called()
|
||||
assert child_status.call_count == 1
|
||||
|
||||
res = await runner.invoke(
|
||||
cli, [*base_cmd, "--name", dev.children[0].alias], obj=dev
|
||||
)
|
||||
assert "Voltage: 122.066 V" in res.output
|
||||
assert child_status.call_count == 2
|
||||
|
||||
if isinstance(dev, IotDevice):
|
||||
monthly = mocker.patch.object(energy, "get_monthly_stats")
|
||||
monthly.return_value = {1: 1234}
|
||||
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)
|
||||
res = await runner.invoke(cli, [*base_cmd, "--year", "1900"], obj=dev)
|
||||
if not isinstance(dev, IotDevice):
|
||||
assert "Device does not support historical statistics" in res.output
|
||||
return
|
||||
@ -474,7 +512,7 @@ async def test_emeter(dev: Device, mocker, runner):
|
||||
if isinstance(dev, IotDevice):
|
||||
daily = mocker.patch.object(energy, "get_daily_stats")
|
||||
daily.return_value = {1: 1234}
|
||||
res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
|
||||
res = await runner.invoke(cli, [*base_cmd, "--month", "1900-12"], obj=dev)
|
||||
if not isinstance(dev, IotDevice):
|
||||
assert "Device has no historical statistics" in res.output
|
||||
return
|
||||
@ -685,6 +723,8 @@ async def test_credentials(discovery_mock, mocker, runner):
|
||||
dr.device_type,
|
||||
"--encrypt-type",
|
||||
dr.mgt_encrypt_schm.encrypt_type,
|
||||
"--login-version",
|
||||
dr.mgt_encrypt_schm.lv or 1,
|
||||
],
|
||||
)
|
||||
assert res.exit_code == 0
|
||||
@ -722,6 +762,7 @@ async def test_without_device_type(dev, mocker, runner):
|
||||
timeout=5,
|
||||
discovery_timeout=7,
|
||||
on_unsupported=ANY,
|
||||
on_discovered_raw=ANY,
|
||||
)
|
||||
|
||||
|
||||
|
@ -4,7 +4,7 @@ from zoneinfo import ZoneInfo
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Device, LightState, Module
|
||||
from kasa import Device, LightState, Module, ThermostatState
|
||||
|
||||
from .device_fixtures import (
|
||||
bulb_iot,
|
||||
@ -57,6 +57,12 @@ light_preset = parametrize_combine([light_preset_smart, bulb_iot])
|
||||
|
||||
light = parametrize_combine([bulb_smart, bulb_iot, dimmable])
|
||||
|
||||
temp_control_smart = parametrize(
|
||||
"has temp control smart",
|
||||
component_filter="temp_control",
|
||||
protocol_filter={"SMART.CHILD"},
|
||||
)
|
||||
|
||||
|
||||
@led
|
||||
async def test_led_module(dev: Device, mocker: MockerFixture):
|
||||
@ -325,6 +331,39 @@ async def test_light_preset_save(dev: Device, mocker: MockerFixture):
|
||||
assert new_preset_state.color_temp == new_preset.color_temp
|
||||
|
||||
|
||||
@temp_control_smart
|
||||
async def test_thermostat(dev: Device, mocker: MockerFixture):
|
||||
"""Test saving a new preset value."""
|
||||
therm_mod = next(get_parent_and_child_modules(dev, Module.Thermostat))
|
||||
assert therm_mod
|
||||
|
||||
await therm_mod.set_state(False)
|
||||
await dev.update()
|
||||
assert therm_mod.state is False
|
||||
assert therm_mod.mode is ThermostatState.Off
|
||||
|
||||
await therm_mod.set_target_temperature(10)
|
||||
await dev.update()
|
||||
assert therm_mod.state is True
|
||||
assert therm_mod.mode is ThermostatState.Heating
|
||||
assert therm_mod.target_temperature == 10
|
||||
|
||||
target_temperature_feature = therm_mod.get_feature(therm_mod.set_target_temperature)
|
||||
temp_control = dev.modules.get(Module.TemperatureControl)
|
||||
assert temp_control
|
||||
allowed_range = temp_control.allowed_temperature_range
|
||||
assert target_temperature_feature.minimum_value == allowed_range[0]
|
||||
assert target_temperature_feature.maximum_value == allowed_range[1]
|
||||
|
||||
await therm_mod.set_temperature_unit("celsius")
|
||||
await dev.update()
|
||||
assert therm_mod.temperature_unit == "celsius"
|
||||
|
||||
await therm_mod.set_temperature_unit("fahrenheit")
|
||||
await dev.update()
|
||||
assert therm_mod.temperature_unit == "fahrenheit"
|
||||
|
||||
|
||||
async def test_set_time(dev: Device):
|
||||
"""Test setting the device time."""
|
||||
time_mod = dev.modules[Module.Time]
|
||||
|
@ -16,6 +16,7 @@ import kasa
|
||||
from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module
|
||||
from kasa.iot import (
|
||||
IotBulb,
|
||||
IotCamera,
|
||||
IotDevice,
|
||||
IotDimmer,
|
||||
IotLightStrip,
|
||||
@ -55,6 +56,11 @@ 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
|
||||
@ -113,6 +119,7 @@ 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,
|
||||
|
@ -47,7 +47,10 @@ def _get_connection_type_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.device_type,
|
||||
dr.mgt_encrypt_schm.encrypt_type,
|
||||
dr.mgt_encrypt_schm.lv,
|
||||
dr.mgt_encrypt_schm.is_support_https,
|
||||
)
|
||||
else:
|
||||
connection_type = DeviceConnectionParameters.from_values(
|
||||
|
@ -1,9 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from kasa import DeviceType
|
||||
from tests.iot.test_iotdevice import SYSINFO_SCHEMA
|
||||
|
||||
from .conftest import plug, plug_iot, plug_smart, switch_smart, wallswitch_iot
|
||||
from .test_iotdevice import SYSINFO_SCHEMA
|
||||
|
||||
# these schemas should go to the mainlib as
|
||||
# they can be useful when adding support for new features/devices
|
||||
|
0
tests/transports/__init__.py
Normal file
0
tests/transports/__init__.py
Normal file
144
tests/transports/test_linkietransport.py
Normal file
144
tests/transports/test_linkietransport.py
Normal file
@ -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)
|
374
tests/transports/test_ssltransport.py
Normal file
374
tests/transports/test_ssltransport.py
Normal file
@ -0,0 +1,374 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from base64 import b64encode
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
from typing 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 (
|
||||
AuthenticationError,
|
||||
DeviceError,
|
||||
KasaException,
|
||||
SmartErrorCode,
|
||||
_RetryableError,
|
||||
)
|
||||
from kasa.httpclient import HttpClient
|
||||
from kasa.json import dumps as json_dumps
|
||||
from kasa.json import loads as json_loads
|
||||
from kasa.transports import SslTransport
|
||||
from kasa.transports.ssltransport import TransportState, _md5_hash
|
||||
|
||||
# Transport tests are not designed for real devices
|
||||
pytestmark = [pytest.mark.requires_dummy]
|
||||
|
||||
MOCK_PWD = "correct_pwd" # noqa: S105
|
||||
MOCK_USER = "mock@example.com"
|
||||
MOCK_BAD_USER_OR_PWD = "foobar" # noqa: S105
|
||||
MOCK_TOKEN = "abcdefghijklmnopqrstuvwxyz1234)(" # noqa: S105
|
||||
|
||||
DEFAULT_CREDS = get_default_credentials(DEFAULT_CREDENTIALS["TAPO"])
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"status_code",
|
||||
"error_code",
|
||||
"username",
|
||||
"password",
|
||||
"expectation",
|
||||
),
|
||||
[
|
||||
pytest.param(
|
||||
200,
|
||||
SmartErrorCode.SUCCESS,
|
||||
MOCK_USER,
|
||||
MOCK_PWD,
|
||||
does_not_raise(),
|
||||
id="success",
|
||||
),
|
||||
pytest.param(
|
||||
200,
|
||||
SmartErrorCode.UNSPECIFIC_ERROR,
|
||||
MOCK_USER,
|
||||
MOCK_PWD,
|
||||
pytest.raises(_RetryableError),
|
||||
id="test retry",
|
||||
),
|
||||
pytest.param(
|
||||
200,
|
||||
SmartErrorCode.DEVICE_BLOCKED,
|
||||
MOCK_USER,
|
||||
MOCK_PWD,
|
||||
pytest.raises(DeviceError),
|
||||
id="test regular error",
|
||||
),
|
||||
pytest.param(
|
||||
400,
|
||||
SmartErrorCode.INTERNAL_UNKNOWN_ERROR,
|
||||
MOCK_USER,
|
||||
MOCK_PWD,
|
||||
pytest.raises(KasaException),
|
||||
id="400 error",
|
||||
),
|
||||
pytest.param(
|
||||
200,
|
||||
SmartErrorCode.LOGIN_ERROR,
|
||||
MOCK_BAD_USER_OR_PWD,
|
||||
MOCK_PWD,
|
||||
pytest.raises(AuthenticationError),
|
||||
id="bad-username",
|
||||
),
|
||||
pytest.param(
|
||||
200,
|
||||
[SmartErrorCode.LOGIN_ERROR, SmartErrorCode.SUCCESS],
|
||||
MOCK_BAD_USER_OR_PWD,
|
||||
"",
|
||||
does_not_raise(),
|
||||
id="working-fallback",
|
||||
),
|
||||
pytest.param(
|
||||
200,
|
||||
[SmartErrorCode.LOGIN_ERROR, SmartErrorCode.LOGIN_ERROR],
|
||||
MOCK_BAD_USER_OR_PWD,
|
||||
"",
|
||||
pytest.raises(AuthenticationError),
|
||||
id="fallback-fail",
|
||||
),
|
||||
pytest.param(
|
||||
200,
|
||||
SmartErrorCode.LOGIN_ERROR,
|
||||
MOCK_USER,
|
||||
MOCK_BAD_USER_OR_PWD,
|
||||
pytest.raises(AuthenticationError),
|
||||
id="bad-password",
|
||||
),
|
||||
pytest.param(
|
||||
200,
|
||||
SmartErrorCode.TRANSPORT_UNKNOWN_CREDENTIALS_ERROR,
|
||||
MOCK_USER,
|
||||
MOCK_PWD,
|
||||
pytest.raises(AuthenticationError),
|
||||
id="auth-error != login_error",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_login(
|
||||
mocker,
|
||||
status_code,
|
||||
error_code,
|
||||
username,
|
||||
password,
|
||||
expectation,
|
||||
):
|
||||
host = "127.0.0.1"
|
||||
mock_ssl_aes_device = MockSslDevice(
|
||||
host,
|
||||
status_code=status_code,
|
||||
send_error_code=error_code,
|
||||
)
|
||||
mocker.patch.object(
|
||||
aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
|
||||
)
|
||||
|
||||
transport = SslTransport(
|
||||
config=DeviceConfig(host, credentials=Credentials(username, password))
|
||||
)
|
||||
|
||||
assert transport._state is TransportState.LOGIN_REQUIRED
|
||||
with expectation:
|
||||
await transport.perform_login()
|
||||
assert transport._state is TransportState.ESTABLISHED
|
||||
|
||||
await transport.close()
|
||||
|
||||
|
||||
async def test_credentials_hash(mocker):
|
||||
host = "127.0.0.1"
|
||||
mock_ssl_aes_device = MockSslDevice(host)
|
||||
mocker.patch.object(
|
||||
aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
|
||||
)
|
||||
creds = Credentials(MOCK_USER, MOCK_PWD)
|
||||
|
||||
data = {"password": _md5_hash(MOCK_PWD.encode()), "username": MOCK_USER}
|
||||
|
||||
creds_hash = b64encode(json_dumps(data).encode()).decode()
|
||||
|
||||
# Test with credentials input
|
||||
transport = SslTransport(config=DeviceConfig(host, credentials=creds))
|
||||
assert transport.credentials_hash == creds_hash
|
||||
|
||||
# Test with credentials_hash input
|
||||
transport = SslTransport(config=DeviceConfig(host, credentials_hash=creds_hash))
|
||||
assert transport.credentials_hash == creds_hash
|
||||
|
||||
await transport.close()
|
||||
|
||||
|
||||
async def test_send(mocker):
|
||||
host = "127.0.0.1"
|
||||
mock_ssl_aes_device = MockSslDevice(host, send_error_code=SmartErrorCode.SUCCESS)
|
||||
mocker.patch.object(
|
||||
aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
|
||||
)
|
||||
|
||||
transport = SslTransport(
|
||||
config=DeviceConfig(host, credentials=Credentials(MOCK_USER, MOCK_PWD))
|
||||
)
|
||||
try_login_spy = mocker.spy(transport, "try_login")
|
||||
request = {
|
||||
"method": "get_device_info",
|
||||
"params": None,
|
||||
}
|
||||
assert transport._state is TransportState.LOGIN_REQUIRED
|
||||
|
||||
res = await transport.send(json_dumps(request))
|
||||
assert "result" in res
|
||||
try_login_spy.assert_called_once()
|
||||
assert transport._state is TransportState.ESTABLISHED
|
||||
|
||||
# Second request does not
|
||||
res = await transport.send(json_dumps(request))
|
||||
try_login_spy.assert_called_once()
|
||||
|
||||
await transport.close()
|
||||
|
||||
|
||||
async def test_no_credentials(mocker):
|
||||
"""Test transport without credentials."""
|
||||
host = "127.0.0.1"
|
||||
mock_ssl_aes_device = MockSslDevice(
|
||||
host, send_error_code=SmartErrorCode.LOGIN_ERROR
|
||||
)
|
||||
mocker.patch.object(
|
||||
aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
|
||||
)
|
||||
|
||||
transport = SslTransport(config=DeviceConfig(host))
|
||||
try_login_spy = mocker.spy(transport, "try_login")
|
||||
|
||||
with pytest.raises(AuthenticationError):
|
||||
await transport.send('{"method": "dummy"}')
|
||||
|
||||
# We get called twice
|
||||
assert try_login_spy.call_count == 2
|
||||
|
||||
await transport.close()
|
||||
|
||||
|
||||
async def test_reset(mocker):
|
||||
"""Test that transport state adjusts correctly for reset."""
|
||||
host = "127.0.0.1"
|
||||
mock_ssl_aes_device = MockSslDevice(host, send_error_code=SmartErrorCode.SUCCESS)
|
||||
mocker.patch.object(
|
||||
aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
|
||||
)
|
||||
|
||||
transport = SslTransport(
|
||||
config=DeviceConfig(host, credentials=Credentials(MOCK_USER, MOCK_PWD))
|
||||
)
|
||||
|
||||
assert transport._state is TransportState.LOGIN_REQUIRED
|
||||
assert str(transport._app_url) == "https://127.0.0.1:4433/app"
|
||||
|
||||
await transport.perform_login()
|
||||
assert transport._state is TransportState.ESTABLISHED
|
||||
assert str(transport._app_url).startswith("https://127.0.0.1:4433/app?token=")
|
||||
|
||||
await transport.close()
|
||||
assert transport._state is TransportState.LOGIN_REQUIRED
|
||||
assert str(transport._app_url) == "https://127.0.0.1:4433/app"
|
||||
|
||||
|
||||
async def test_port_override():
|
||||
"""Test that port override sets the app_url."""
|
||||
host = "127.0.0.1"
|
||||
port_override = 12345
|
||||
config = DeviceConfig(
|
||||
host, credentials=Credentials("foo", "bar"), port_override=port_override
|
||||
)
|
||||
transport = SslTransport(config=config)
|
||||
|
||||
assert str(transport._app_url) == f"https://127.0.0.1:{port_override}/app"
|
||||
|
||||
await transport.close()
|
||||
|
||||
|
||||
class MockSslDevice:
|
||||
"""Based on MockAesSslDevice."""
|
||||
|
||||
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,
|
||||
send_error_code=SmartErrorCode.INTERNAL_UNKNOWN_ERROR,
|
||||
):
|
||||
self.host = host
|
||||
self.http_client = HttpClient(DeviceConfig(self.host))
|
||||
|
||||
self._state = TransportState.LOGIN_REQUIRED
|
||||
|
||||
# test behaviour attributes
|
||||
self.status_code = status_code
|
||||
self.send_error_code = send_error_code
|
||||
|
||||
async def post(self, url: URL, params=None, json=None, data=None, *_, **__):
|
||||
if data:
|
||||
json = json_loads(data)
|
||||
_LOGGER.debug("Request %s: %s", url, json)
|
||||
res = self._post(url, json)
|
||||
_LOGGER.debug("Response %s, data: %s", res, await res.read())
|
||||
return res
|
||||
|
||||
def _post(self, url: URL, json: dict[str, Any]):
|
||||
method = json["method"]
|
||||
|
||||
if method == "login":
|
||||
if self._state is TransportState.LOGIN_REQUIRED:
|
||||
assert json.get("token") is None
|
||||
assert url == URL(f"https://{self.host}:4433/app")
|
||||
return self._return_login_response(url, json)
|
||||
else:
|
||||
_LOGGER.warning("Received login although already logged in")
|
||||
pytest.fail("non-handled re-login logic")
|
||||
|
||||
assert url == URL(f"https://{self.host}:4433/app?token={MOCK_TOKEN}")
|
||||
return self._return_send_response(url, json)
|
||||
|
||||
def _return_login_response(self, url: URL, request: dict[str, Any]):
|
||||
request_username = request["params"].get("username")
|
||||
request_password = request["params"].get("password")
|
||||
|
||||
# Handle multiple error codes
|
||||
if isinstance(self.send_error_code, list):
|
||||
error_code = self.send_error_code.pop(0)
|
||||
else:
|
||||
error_code = self.send_error_code
|
||||
|
||||
_LOGGER.debug("Using error code %s", error_code)
|
||||
|
||||
def _return_login_error():
|
||||
resp = {
|
||||
"error_code": error_code.value,
|
||||
"result": {"unknown": "payload"},
|
||||
}
|
||||
|
||||
_LOGGER.debug("Returning login error with status %s", self.status_code)
|
||||
return self._mock_response(self.status_code, resp)
|
||||
|
||||
if error_code is not SmartErrorCode.SUCCESS:
|
||||
# Bad username
|
||||
if request_username == MOCK_BAD_USER_OR_PWD:
|
||||
return _return_login_error()
|
||||
|
||||
# Bad password
|
||||
if request_password == _md5_hash(MOCK_BAD_USER_OR_PWD.encode()):
|
||||
return _return_login_error()
|
||||
|
||||
# Empty password
|
||||
if request_password == _md5_hash(b""):
|
||||
return _return_login_error()
|
||||
|
||||
self._state = TransportState.ESTABLISHED
|
||||
resp = {
|
||||
"error_code": error_code.value,
|
||||
"result": {
|
||||
"token": MOCK_TOKEN,
|
||||
},
|
||||
}
|
||||
_LOGGER.debug("Returning login success with status %s", self.status_code)
|
||||
return self._mock_response(self.status_code, resp)
|
||||
|
||||
def _return_send_response(self, url: URL, json: dict[str, Any]):
|
||||
method = json["method"]
|
||||
result = {
|
||||
"result": {method: {"dummy": "response"}},
|
||||
"error_code": self.send_error_code.value,
|
||||
}
|
||||
return self._mock_response(self.status_code, result)
|
421
uv.lock
421
uv.lock
@ -1,9 +1,5 @@
|
||||
version = 1
|
||||
requires-python = ">=3.11, <4.0"
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.13'",
|
||||
"python_full_version >= '3.13'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@ -16,7 +12,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.10.10"
|
||||
version = "3.11.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@ -24,55 +20,56 @@ dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "frozenlist" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/7e/16e57e6cf20eb62481a2f9ce8674328407187950ccc602ad07c685279141/aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", size = 7542993 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/cb/f9bb10e0cf6f01730b27d370b10cc15822bea4395acd687abc8cc5fed3ed/aiohttp-3.11.7.tar.gz", hash = "sha256:01a8aca4af3da85cea5c90141d23f4b0eee3cbecfd33b029a45a80f28c66c668", size = 7666482 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/31/3c351d17596194e5a38ef169a4da76458952b2497b4b54645b9d483cbbb0/aiohttp-3.10.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f", size = 586501 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/a8/a559d09eb08478cdead6b7ce05b0c4a133ba27fcdfa91e05d2e62867300d/aiohttp-3.10.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb", size = 398993 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/47/7736d4174613feef61d25332c3bd1a4f8ff5591fbd7331988238a7299485/aiohttp-3.10.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871", size = 390647 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/21/e9ba192a04b7160f5a8952c98a1de7cf8072ad150fa3abd454ead1ab1d7f/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c", size = 1306481 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/50/f364c01c8d0def1dc34747b2470969e216f5a37c7ece00fe558810f37013/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38", size = 1344652 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/c2/74f608e984e9b585649e2e83883facad6fa3fc1d021de87b20cc67e8e5ae/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb", size = 1378498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/a7/05a48c7c0a7a80a5591b1203bf1b64ca2ed6a2050af918d09c05852dc42b/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7", size = 1292718 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/78/a925655018747e9790350180330032e27d6e0d7ed30bde545fae42f8c49c/aiohttp-3.10.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911", size = 1251776 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/9d/85c6b69f702351d1236594745a4fdc042fc43f494c247a98dac17e004026/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092", size = 1271716 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/a7/55fc805ff9b14af818903882ece08e2235b12b73b867b521b92994c52b14/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142", size = 1266263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ec/d2be2ca7b063e4f91519d550dbc9c1cb43040174a322470deed90b3d3333/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9", size = 1321617 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/a3/b29f7920e1cd0a9a68a45dd3eb16140074d2efb1518d2e1f3e140357dc37/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1", size = 1339227 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/81/34b67235c47e232d807b4bbc42ba9b927c7ce9476872372fddcfd1e41b3d/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a", size = 1299068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/1f/26a7fe11b6ad3184f214733428353c89ae9fe3e4f605a657f5245c5e720c/aiohttp-3.10.10-cp311-cp311-win32.whl", hash = "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94", size = 362223 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/91/85dcd93f64011434359ce2666bece981f08d31bc49df33261e625b28595d/aiohttp-3.10.10-cp311-cp311-win_amd64.whl", hash = "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959", size = 381576 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/99/4c5aefe5ad06a1baf206aed6598c7cdcbc7c044c46801cd0d1ecb758cae3/aiohttp-3.10.10-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", size = 583536 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/36/8b3bc49b49cb6d2da40ee61ff15dbcc44fd345a3e6ab5bb20844df929821/aiohttp-3.10.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", size = 395693 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/77/0aa8660dcf11fa65d61712dbb458c4989de220a844bd69778dff25f2d50b/aiohttp-3.10.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", size = 390898 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/d2/b833d95deb48c75db85bf6646de0a697e7fb5d87bd27cbade4f9746b48b1/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", size = 1312060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5f/29fd5113165a0893de8efedf9b4737e0ba92dfcd791415a528f947d10299/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", size = 1350553 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/cc/f835f74b7d344428469200105236d44606cfa448be1e7c95ca52880d9bac/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", size = 1392646 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/fe/1332409d845ca601893bbf2d76935e0b93d41686e5f333841c7d7a4a770d/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", size = 1306310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/a1/25a7633a5a513278a9892e333501e2e69c83e50be4b57a62285fb7a008c3/aiohttp-3.10.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", size = 1260255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/39/30eafe89e0e2a06c25e4762844c8214c0c0cd0fd9ffc3471694a7986f421/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", size = 1271141 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/fc/33125df728b48391ef1fcb512dfb02072158cc10d041414fb79803463020/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", size = 1280244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/61/e42bf2c2934b5caa4e2ec0b5e5fd86989adb022b5ee60c2572a9d77cf6fe/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", size = 1316805 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/32/f52a5e2ae9ad3bba10e026a63a7a23abfa37c7d97aeeb9004eaa98df3ce3/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", size = 1343930 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/be/6a403b464dcab3631fe8e27b0f1d906d9e45c5e92aca97ee007e5a895560/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", size = 1306186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/fd/bb50fe781068a736a02bf5c7ad5f3ab53e39f1d1e63110da6d30f7605edc/aiohttp-3.10.10-cp312-cp312-win32.whl", hash = "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", size = 359289 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/9e/5add7e240f77ef67c275c82cc1d08afbca57b77593118c1f6e920ae8ad3f/aiohttp-3.10.10-cp312-cp312-win_amd64.whl", hash = "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", size = 379313 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/eb/618b1b76c7fe8082a71c9d62e3fe84c5b9af6703078caa9ec57850a12080/aiohttp-3.10.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", size = 576114 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/37/3126995d7869f8b30d05381b81a2d4fb4ec6ad313db788e009bc6d39c211/aiohttp-3.10.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", size = 391901 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/f2/8fdfc845be1f811c31ceb797968523813f8e1263ee3e9120d61253f6848f/aiohttp-3.10.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", size = 387418 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/d5/33d2061d36bf07e80286e04b7e0a4de37ce04b5ebfed72dba67659a05250/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", size = 1287073 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/52/affb55be16a4747740bd630b4c002dac6c5eac42f9bb64202fc3cf3f1930/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", size = 1323612 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/f2/cddb69b975387daa2182a8442566971d6410b8a0179bb4540d81c97b1611/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", size = 1368406 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/e4/afba7327da4d932da8c6e29aecaf855f9d52dace53ac15bfc8030a246f1b/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", size = 1282761 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/6b/364856faa0c9031ea76e24ef0f7fef79cddd9fa8e7dba9a1771c6acc56b5/aiohttp-3.10.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", size = 1236518 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/af/c382846f8356fe64a7b5908bb9b477457aa23b71be7ed551013b7b7d4d87/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", size = 1250344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/53/294f87fc086fd0772d0ab82497beb9df67f0f27a8b3dd5742a2656db2bc6/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414", size = 1248956 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/30/7d746717fe11bdfefb88bb6c09c5fc985d85c4632da8bb6018e273899254/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", size = 1293379 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b9/45d670a834458db67a24258e9139ba61fa3bd7d69b98ecf3650c22806f8f/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", size = 1320108 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/8c/804bb2e837a175635d2000a0659eafc15b2e9d92d3d81c8f69e141ecd0b0/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", size = 1281546 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c0/862e6a9de3d6eeb126cd9d9ea388243b70df9b871ce1a42b193b7a4a77fc/aiohttp-3.10.10-cp313-cp313-win32.whl", hash = "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", size = 357516 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/63/3e1aee3e554263f3f1011cca50d78a4894ae16ce99bf78101ac3a2f0ef74/aiohttp-3.10.10-cp313-cp313-win_amd64.whl", hash = "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", size = 376785 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -290,50 +287,50 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.4"
|
||||
version = "7.6.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@ -381,11 +378,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.19"
|
||||
version = "0.20.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/330ea8d383eb2ce973df34d1239b3b21e91cd8c865d21ff82902d952f91f/docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6", size = 2056383 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/53/a5da4f2c5739cf66290fac1431ee52aff6851c7c8ffd8264f13affd7bcdd/docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b", size = 2058365 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/69/e391bd51bc08ed9141ecd899a0ddb61ab6465309f1eb470905c0c8868081/docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc", size = 570472 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", size = 572666 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -474,11 +471,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.1"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/5f/05f0d167be94585d502b4adf8c7af31f1dc0b1c7e14f9938a88fdbbcf4a7/identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02", size = 99179 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/f5/09644a3ad803fae9eca8efa17e1f2aef380c7f0b02f7ec4e8d446e51d64a/identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd", size = 99049 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -510,14 +507,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "jedi"
|
||||
version = "0.19.1"
|
||||
version = "0.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "parso" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/99/99b493cec4bf43176b678de30f81ed003fd6a647a301b9c927280c600f0a/jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", size = 1227821 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/9f/bc63f0f0737ad7a60800bfd472a4836661adae21f9c2535f3957b1e54ceb/jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0", size = 1569361 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -556,14 +553,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "2.2.0"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/c0/59bd6d0571986f72899288a95d9d6178d0eebd70b6650f1bb3f0da90f8f7/markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1", size = 67120 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/25/2d88e8feee8e055d015343f9b86e370a1ccbec546f2865c98397aaef24af/markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30", size = 84466 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -616,26 +613,26 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mashumaro"
|
||||
version = "3.14"
|
||||
version = "3.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/47/0a450b281bef2d7e97ec02c8e1168d821e283f58e02e6c403b2bb4d73c1c/mashumaro-3.14.tar.gz", hash = "sha256:5ef6f2b963892cbe9a4ceb3441dfbea37f8c3412523f25d42e9b3a7186555f1d", size = 166160 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/c1/7b687c8b993202e2eb49e559b25599d8e85f1b6d92ce676c8801226b8bdf/mashumaro-3.15.tar.gz", hash = "sha256:32a2a38a1e942a07f2cbf9c3061cb2a247714ee53e36a5958548b66bd116d0a9", size = 188646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/35/8d63733a2c12149d0c7663c29bf626bdbeea5f0ff963afe58a42b4810981/mashumaro-3.14-py3-none-any.whl", hash = "sha256:c12a649599a8f7b1a0b35d18f12e678423c3066189f7bc7bd8dd431c5c8132c3", size = 92183 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/59/595eabaa779c87a72d65864351e0fdd2359d7d73967d5ed9f2f0c6186fa3/mashumaro-3.15-py3-none-any.whl", hash = "sha256:cdd45ef5a4d09860846a3ee37a4c2f5f4bc70eb158caa55648c4c99451ca6c4c", size = 93761 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdit-py-plugins"
|
||||
version = "0.3.5"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/e7/cc2720da8a32724b36d04c6dba5644154cdf883a1482b3bbb81959a642ed/mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a", size = 39871 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/4c/a9b222f045f98775034d243198212cbea36d3524c3ee1e8ab8c0346d6953/mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e", size = 52087 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -740,7 +737,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "myst-parser"
|
||||
version = "1.0.0"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "docutils" },
|
||||
@ -750,9 +747,9 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "sphinx" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/69/fbddb50198c6b0901a981e72ae30f1b7769d2dfac88071f7df41c946d133/myst-parser-1.0.0.tar.gz", hash = "sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae", size = 84224 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/1f/1621ef434ac5da26c30d31fcca6d588e3383344902941713640ba717fa87/myst_parser-1.0.0-py3-none-any.whl", hash = "sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c", size = 77312 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -766,46 +763,54 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.10.11"
|
||||
version = "3.10.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/3a/10320029954badc7eaa338a15ee279043436f396e965dafc169610e4933f/orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", size = 5444879 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/04/bb9f72987e7f62fb591d6c880c0caaa16238e4e530cbc3bdc84a7372d75f/orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff", size = 5438647 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/25/c869a1fbd481dcb02c70032fd6a7243de7582bc48c7cae03d6f0985a11c0/orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6", size = 266432 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/a4/2307155ee92457d28345308f7d8c0e712348404723025613adeffcb531d0/orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe", size = 151884 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/82/daf1b2596dd49fe44a1bd92367568faf6966dcb5d7f99fd437c3d0dc2de6/orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67", size = 167371 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/a8/680578e4589be5fdcfe0186bdd7dc6fe4a39d30e293a9da833cbedd5a56e/orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b", size = 154368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ce/9cb394b5b01ef34579eeca6d704b21f97248f607067ce95a24ba9ea2698e/orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d", size = 165725 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/24/55eeb05cfb36b9e950d05743e6f6fdb7d5f33ca951a27b06ea6d03371aed/orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5", size = 142522 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/0c/3a6a289e56dcc9fe67dc6b6d33c91dc5491f9ec4a03745efd739d2acf0ff/orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a", size = 146934 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/5c/a08c0e90a91e2526029a4681ff8c6fc4495b8bab77d48801144e378c7da9/orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981", size = 142904 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c9/710286a60b14e88288ca014d43befb08bb0a4a6a0f51b875f8c2f05e8205/orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55", size = 144459 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/68/ef7b920e0a09e02b1a30daca1b4864938463797995c2fabe457c1500220a/orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec", size = 136444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/f2/a712dbcef6d84ff53e13056e7dc69d9d4844bd1e35e51b7431679ddd154d/orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51", size = 266505 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/53970831786d71f98fdc13c0f80451324c9b5c20fbf42f42ef6147607ee7/orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97", size = 151745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/38/482667da1ca7ef95d44d4d2328257a144fd2752383e688637c53ed474d2a/orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19", size = 167274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/2f/5bb0a03e819781d82dadb733fde8ebbe20d1777d1a33715d45ada4d82ce8/orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0", size = 154605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e9/14cc34d45c7bd51665aff9b1bb6b83475a61c52edb0d753fffe1adc97764/orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433", size = 165874 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/c2781ecf90f99623e97c67a31e8553f38a1ecebaf3189485726ac8641576/orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5", size = 142813 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/4f/18c83f78b501b6608569b1610fcb5a25c9bb9ab6a7eb4b3a55131e0fba37/orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd", size = 146762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/19/ea80d5b575abd3f76a790409c2b7b8a60f3fc9447965c27d09613b8bddf4/orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b", size = 143186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/f5/d835fee01a0284d4b78effc24d16e7609daac2ff6b6851ca1bdd3b6194fc/orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d", size = 144489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/60/748e0e205060dec74328dfd835e47902eb5522ae011766da76bfff64e2f4/orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284", size = 136614 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/92/400970baf46b987c058469e9e779fb7a40d54a5754914d3634cca417e054/orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", size = 266402 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/fa/f126fc2d817552bd1f67466205abdcbff64eab16f6844fe6df2853528675/orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", size = 140826 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/18/9b9664d7d4af5b4fe9fe6600b7654afc0684bba528260afdde10c4a530aa/orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", size = 142593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/f9/a30c68f12778d5e58e6b5cdd26f86ee2d0babce1a475073043f46fdd8402/orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", size = 146777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/97/12047b0c0e9b391d589fb76eb40538f522edc664f650f8e352fdaaf77ff5/orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", size = 142961 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/97/d904e26c1cabf2dd6ab1b0909e9b790af28a7f0fcb9d8378d7320d4869eb/orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", size = 144486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/62/3760bd1e6e949321d99bab238d08db2b1266564d2f708af668f57109bb36/orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", size = 136361 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
version = "24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1083,7 +1088,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-kasa"
|
||||
version = "0.7.7"
|
||||
version = "0.8.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@ -1143,7 +1148,7 @@ requires-dist = [
|
||||
{ name = "orjson", marker = "extra == 'speedups'", specifier = ">=3.9.1" },
|
||||
{ name = "ptpython", marker = "extra == 'shell'" },
|
||||
{ name = "rich", marker = "extra == 'shell'" },
|
||||
{ name = "sphinx", marker = "extra == 'docs'", specifier = "~=6.2" },
|
||||
{ name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.4.7" },
|
||||
{ name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = "~=2.0" },
|
||||
{ name = "sphinxcontrib-programoutput", marker = "extra == 'docs'", specifier = "~=0.0" },
|
||||
{ name = "tzdata", marker = "platform_system == 'Windows'", specifier = ">=2024.2" },
|
||||
@ -1287,7 +1292,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "6.2.1"
|
||||
version = "7.4.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "alabaster" },
|
||||
@ -1307,9 +1312,9 @@ dependencies = [
|
||||
{ name = "sphinxcontrib-qthelp" },
|
||||
{ name = "sphinxcontrib-serializinghtml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/6d/392defcc95ca48daf62aecb89550143e97a4651275e62a3d7755efe35a3a/Sphinx-6.2.1.tar.gz", hash = "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b", size = 6681092 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d8/45ba6097c39ba44d9f0e1462fb232e13ca4ddb5aea93a385dcfa964687da/sphinx-6.2.1-py3-none-any.whl", hash = "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912", size = 3024615 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1424,11 +1429,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.2"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/e4/1b6cbcc82d8832dd0ce34767d5c560df8a3547ad8cbc427f34601415930a/tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", size = 16622 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/f7/4da0ffe1892122c9ea096c57f64c2753ae5dd3ce85488802d11b0992cc6d/tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391", size = 13750 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1460,16 +1465,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.27.1"
|
||||
version = "20.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/75/53316a5a8050069228a2f6d11f32046cfa94fbb6cc3f08703f59b873de2e/virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa", size = 7650368 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f9/0919cf6f1432a8c4baa62511f8f8da8225432d22e83e3476f5be1a1edc6e/virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", size = 4276702 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1501,62 +1506,62 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.17.1"
|
||||
version = "1.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/9c/9c0a9bfa683fc1be7fdcd9687635151544d992cccd48892dc5e0a5885a29/yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", size = 178163 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/4b/53db4ecad4d54535aff3dfda1f00d6363d79455f62b11b8ca97b82746bd2/yarl-1.18.0.tar.gz", hash = "sha256:20d95535e7d833889982bfe7cc321b7f63bf8879788fee982c76ae2b24cfb715", size = 180098 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/0f/ce6a2c8aab9946446fb27f1e28f0fd89ce84ae913ab18a92d18078a1c7ed/yarl-1.17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cbad927ea8ed814622305d842c93412cb47bd39a496ed0f96bfd42b922b4a217", size = 140727 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/df/204f7a502bdc3973cd9fc29e7dfad18ae48b3acafdaaf1ae07c0f41025aa/yarl-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fca4b4307ebe9c3ec77a084da3a9d1999d164693d16492ca2b64594340999988", size = 93560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/e1/f4d522ae0560c91a4ea31113a50f00f85083be885e1092fc6e74eb43cb1d/yarl-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff5c6771c7e3511a06555afa317879b7db8d640137ba55d6ab0d0c50425cab75", size = 91497 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/82/783d97bf4a226f1a2e59b1966f2752244c2bf4dc89bc36f61d597b8e34e5/yarl-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b29beab10211a746f9846baa39275e80034e065460d99eb51e45c9a9495bcca", size = 339446 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/ff/615600647048d81289c80907165de713fbc566d1e024789863a2f6563ba3/yarl-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a52a1ffdd824fb1835272e125385c32fd8b17fbdefeedcb4d543cc23b332d74", size = 354616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/04/bfb7adb452bd19dfe0c35354ffce8ebc3086e028e5f8270e409d17da5466/yarl-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58c8e9620eb82a189c6c40cb6b59b4e35b2ee68b1f2afa6597732a2b467d7e8f", size = 351801 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e0/efe21edacdc4a638ce911f8cabf1c77cac3f60e9819ba7d891b9ceb6e1d4/yarl-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d", size = 343381 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/f9/7bc7e69857d6fc3920ecd173592f921d5701f4a0dd3f2ae293b386cfa3bf/yarl-1.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:881764d610e3269964fc4bb3c19bb6fce55422828e152b885609ec176b41cf11", size = 337093 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/52/99da61947466275ff17d7bc04b0ac31dfb7ec699bd8d8985dffc34c3a913/yarl-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8c79e9d7e3d8a32d4824250a9c6401194fb4c2ad9a0cec8f6a96e09a582c2cc0", size = 346619 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/8a/8aaad86a35a16e485ba0e5de0d2ae55bf8dd0c9f1cccac12be4c91366b1d/yarl-1.17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:299f11b44d8d3a588234adbe01112126010bd96d9139c3ba7b3badd9829261c3", size = 344347 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/b6/97f29f626b4a1768ffc4b9b489533612cfcb8905c90f745aade7b2eaf75e/yarl-1.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc7d768260f4ba4ea01741c1b5fe3d3a6c70eb91c87f4c8761bbcce5181beafe", size = 350316 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/98/8e0e8b812479569bdc34d66dd3e2471176ca33be4ff5c272a01333c4b269/yarl-1.17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:de599af166970d6a61accde358ec9ded821234cbbc8c6413acfec06056b8e860", size = 361336 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/d3/d1507efa0a85c25285f8eb51df9afa1ba1b6e446dda781d074d775b6a9af/yarl-1.17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2b24ec55fad43e476905eceaf14f41f6478780b870eda5d08b4d6de9a60b65b4", size = 365350 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ba/ee7f1830449c96bae6f33210b7d89e8aaf3079fbdaf78ac398e50a9da404/yarl-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9fb815155aac6bfa8d86184079652c9715c812d506b22cfa369196ef4e99d1b4", size = 357689 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/85/321c563dc5afe1661108831b965c512d185c61785400f5606006507d2e18/yarl-1.17.1-cp311-cp311-win32.whl", hash = "sha256:7615058aabad54416ddac99ade09a5510cf77039a3b903e94e8922f25ed203d7", size = 83635 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/da/543a32c00860588ff1235315b68f858cea30769099c32cd22b7bb266411b/yarl-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:14bc88baa44e1f84164a392827b5defb4fa8e56b93fecac3d15315e7c8e5d8b3", size = 90218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/af/e25615c7920396219b943b9ff8b34636ae3e1ad30777649371317d7f05f8/yarl-1.17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61", size = 141839 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/5e/363d9de3495c7c66592523f05d21576a811015579e0c87dd38c7b5788afd/yarl-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d", size = 94125 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a2/b65447626227ebe36f18f63ac551790068bf42c69bb22dfa3ae986170728/yarl-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139", size = 92048 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/f5/2ef86458446f85cde10582054fd5113495ef8ce8477da35aaaf26d2970ef/yarl-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5", size = 331472 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/6b/1ba79758ba352cdf2ad4c20cab1b982dd369aa595bb0d7601fc89bf82bee/yarl-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac", size = 341260 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/41/4e07c2afca3f9ed3da5b0e38d43d0280d9b624a3d5c478c425e5ce17775c/yarl-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463", size = 340882 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c0/cd8e94618983c1b811af082e1a7ad7764edb3a6af2bc6b468e0e686238ba/yarl-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147", size = 336648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/fc/73ec4340d391ffbb8f34eb4c55429784ec9f5bd37973ce86d52d67135418/yarl-1.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7", size = 325019 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/48/da3ebf418fc239d0a156b3bdec6b17a5446f8d2dea752299c6e47b143a85/yarl-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685", size = 342841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/79/107272745a470a8167924e353a5312eb52b5a9bb58e22686adc46c94f7ec/yarl-1.17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172", size = 341433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/9c/6459668b3b8dcc11cd061fc53e12737e740fb6b1575b49c84cbffb387b3a/yarl-1.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7", size = 344927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/0b/93a17ed733aca8164fc3a01cb7d47b3f08854ce4f957cce67a6afdb388a0/yarl-1.17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da", size = 355732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/63/ead2ed6aec3c59397e135cadc66572330325a0c24cd353cd5c94f5e63463/yarl-1.17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c", size = 362123 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/bf/f6b75b4c2fcf0e7bb56edc0ed74e33f37fac45dc40e5a52a3be66b02587a/yarl-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", size = 356355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/1f/50a0257cd07eef65c8c65ad6a21f5fb230012d659e021aeb6ac8a7897bf6/yarl-1.17.1-cp312-cp312-win32.whl", hash = "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", size = 83279 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/82/fafb2c1268d63d54ec08b3a254fbe51f4ef098211501df646026717abee3/yarl-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", size = 89590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/1e/5a93e3743c20eefbc68bd89334d9c9f04f3f2334380f7bbf5e950f29511b/yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", size = 139974 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/be/4e0f6919013c7c5eaea5c31811c551ccd599d2fc80aa3dd6962f1bbdcddd/yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", size = 93364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f0/650f994bc491d0cb85df8bb45392780b90eab1e175f103a5edc61445ff67/yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", size = 91177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/e8/9945ed555d14b43ede3ae8b1bd73e31068a694cad2b9d3cad0a28486c2eb/yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", size = 333086 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/c0/7d167e48e14d26639ca066825af8da7df1d2fcdba827e3fd6341aaf22a3b/yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", size = 343661 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/81/80a266517531d4e3553aecd141800dbf48d02e23ebd52909e63598a80134/yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", size = 345196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/77/6adc482ba7f2dc6c0d9b3b492e7cd100edfac4cfc3849c7ffa26fd7beb1a/yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", size = 338743 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/cc/f0c4c0b92ff3ada517ffde2b127406c001504b225692216d969879ada89a/yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", size = 326719 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/3b/7bfc80d3376b5fa162189993a87a5a6a58057f88315bd0ea00610055b57a/yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", size = 345826 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/66/cf0b0338107a5c370205c1a572432af08f36ca12ecce127f5b558398b4fd/yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", size = 340335 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/52/b084b0eec0fd4d2490e1d33ace3320fad704c5f1f3deaa709f929d2d87fc/yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", size = 345301 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/38/9e2036d948efd3bafcdb4976cb212166fded76615f0dfc6c1492c4ce4784/yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", size = 354205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c1/13dfe1e70b86811733316221c696580725ceb1c46d4e4db852807e134310/yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", size = 360501 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/87/756e05c74cd8bf9e71537df4a2cae7e8211a9ebe0d2350a3e26949e1e41c/yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", size = 359452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b2/b2bb09c1e6d59e1c9b1b36a86caa473e22c3dbf26d1032c030e9bfb554dc/yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", size = 308904 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/27/f084d9a5668853c1f3b246620269b14ee871ef3c3cc4f3a1dd53645b68ec/yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", size = 314637 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ad/1fe7ff5f3e8869d4c5070f47b96bac2b4d15e67c100a8278d8e7876329fc/yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", size = 44352 },
|
||||
{ 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 },
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user