mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-01-09 22:37:08 +00:00
Merge remote-tracking branch 'upstream/master' into feat/light_module_feats
This commit is contained in:
commit
a7192b8a92
109
CHANGELOG.md
109
CHANGELOG.md
@ -1,8 +1,89 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [0.8.1](https://github.com/python-kasa/python-kasa/tree/0.8.1) (2024-12-06)
|
## [0.9.0](https://github.com/python-kasa/python-kasa/tree/0.9.0) (2024-12-21)
|
||||||
|
|
||||||
This patch release fixes some issues with newly supported smartcam devices.
|
[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.8.1...0.9.0)
|
||||||
|
|
||||||
|
**Release highlights:**
|
||||||
|
|
||||||
|
- Improvements to Tapo camera support:
|
||||||
|
- C100, C225, C325WB, C520WS and TC70 now supported.
|
||||||
|
- Support for motion, person, tamper, and baby cry detection.
|
||||||
|
- Initial support for Tapo robovacs.
|
||||||
|
- API extended with `FeatureAttributes` for consumers to test for [supported features](https://python-kasa.readthedocs.io/en/stable/topics.html#modules-and-features).
|
||||||
|
- Experimental support for Kasa cameras[^1]
|
||||||
|
|
||||||
|
[^1]: Currently limited to devices not yet provisioned via the Tapo app - Many thanks to @puxtril!
|
||||||
|
|
||||||
|
**Breaking changes:**
|
||||||
|
|
||||||
|
- Use DeviceInfo consistently across devices [\#1338](https://github.com/python-kasa/python-kasa/pull/1338) (@sdb9696)
|
||||||
|
|
||||||
|
**Implemented enhancements:**
|
||||||
|
|
||||||
|
- Add rssi and signal\_level to smartcam [\#1392](https://github.com/python-kasa/python-kasa/pull/1392) (@sdb9696)
|
||||||
|
- Add smartcam detection modules [\#1389](https://github.com/python-kasa/python-kasa/pull/1389) (@sdb9696)
|
||||||
|
- Add bare-bones matter modules to smart and smartcam devices [\#1371](https://github.com/python-kasa/python-kasa/pull/1371) (@sdb9696)
|
||||||
|
- Add bare bones homekit modules smart and smartcam devices [\#1370](https://github.com/python-kasa/python-kasa/pull/1370) (@sdb9696)
|
||||||
|
- Return raw discovery result in cli discover raw [\#1342](https://github.com/python-kasa/python-kasa/pull/1342) (@sdb9696)
|
||||||
|
- cli: print model, https, and lv for discover list [\#1339](https://github.com/python-kasa/python-kasa/pull/1339) (@rytilahti)
|
||||||
|
- Improve overheat reporting [\#1335](https://github.com/python-kasa/python-kasa/pull/1335) (@rytilahti)
|
||||||
|
- Provide alternative camera urls [\#1316](https://github.com/python-kasa/python-kasa/pull/1316) (@sdb9696)
|
||||||
|
- Add LinkieTransportV2 and basic IOT.IPCAMERA support [\#1270](https://github.com/python-kasa/python-kasa/pull/1270) (@Puxtril)
|
||||||
|
- Add ssltransport for robovacs [\#943](https://github.com/python-kasa/python-kasa/pull/943) (@rytilahti)
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- Tapo H200 Hub does not work with python-kasa [\#1149](https://github.com/python-kasa/python-kasa/issues/1149)
|
||||||
|
- Treat smartcam 500 errors after handshake as retryable [\#1395](https://github.com/python-kasa/python-kasa/pull/1395) (@sdb9696)
|
||||||
|
- Fix lens mask required component and state [\#1386](https://github.com/python-kasa/python-kasa/pull/1386) (@sdb9696)
|
||||||
|
- Add LensMask module to smartcam [\#1385](https://github.com/python-kasa/python-kasa/pull/1385) (@sdb9696)
|
||||||
|
- Do not error when accessing smart device\_type before update [\#1319](https://github.com/python-kasa/python-kasa/pull/1319) (@sdb9696)
|
||||||
|
- Fallback to other module data on get\_energy\_usage errors [\#1245](https://github.com/python-kasa/python-kasa/pull/1245) (@rytilahti)
|
||||||
|
|
||||||
|
**Added support for devices:**
|
||||||
|
|
||||||
|
- Add P210M\(US\) 1.0 1.0.3 fixture [\#1399](https://github.com/python-kasa/python-kasa/pull/1399) (@sdb9696)
|
||||||
|
- Add C225\(US\) 2.0 1.0.11 fixture [\#1398](https://github.com/python-kasa/python-kasa/pull/1398) (@sdb9696)
|
||||||
|
- Add P306\(US\) 1.0 1.1.2 fixture [\#1396](https://github.com/python-kasa/python-kasa/pull/1396) (@nakanaela)
|
||||||
|
- Add TC70 3.0 1.3.11 fixture [\#1390](https://github.com/python-kasa/python-kasa/pull/1390) (@sdb9696)
|
||||||
|
- Add C325WB\(EU\) 1.0 1.1.17 Fixture [\#1379](https://github.com/python-kasa/python-kasa/pull/1379) (@sdb9696)
|
||||||
|
- Add C100 4.0 1.3.14 Fixture [\#1378](https://github.com/python-kasa/python-kasa/pull/1378) (@sdb9696)
|
||||||
|
- Add KS200 \(US\) IOT Fixture and P115 \(US\) Smart Fixture [\#1355](https://github.com/python-kasa/python-kasa/pull/1355) (@ZeliardM)
|
||||||
|
- Add C520WS camera fixture [\#1352](https://github.com/python-kasa/python-kasa/pull/1352) (@Happy-Cadaver)
|
||||||
|
|
||||||
|
**Documentation updates:**
|
||||||
|
|
||||||
|
- Update docs for Tapo Lab Third-Party compatibility [\#1380](https://github.com/python-kasa/python-kasa/pull/1380) (@sdb9696)
|
||||||
|
- Add homebridge-kasa-python link to README [\#1367](https://github.com/python-kasa/python-kasa/pull/1367) (@rytilahti)
|
||||||
|
- Update docs for new FeatureAttribute behaviour [\#1365](https://github.com/python-kasa/python-kasa/pull/1365) (@sdb9696)
|
||||||
|
- Add link to related homeassistant-tapo-control [\#1333](https://github.com/python-kasa/python-kasa/pull/1333) (@rytilahti)
|
||||||
|
|
||||||
|
**Project maintenance:**
|
||||||
|
|
||||||
|
- Add P135 1.0 1.2.0 fixture [\#1397](https://github.com/python-kasa/python-kasa/pull/1397) (@sdb9696)
|
||||||
|
- Handle smartcam device blocked response [\#1393](https://github.com/python-kasa/python-kasa/pull/1393) (@sdb9696)
|
||||||
|
- Handle KeyboardInterrupts in the cli better [\#1391](https://github.com/python-kasa/python-kasa/pull/1391) (@sdb9696)
|
||||||
|
- Update C520WS fixture with new methods [\#1384](https://github.com/python-kasa/python-kasa/pull/1384) (@sdb9696)
|
||||||
|
- Miscellaneous minor fixes to dump\_devinfo [\#1382](https://github.com/python-kasa/python-kasa/pull/1382) (@sdb9696)
|
||||||
|
- Add timeout parameter to dump\_devinfo [\#1381](https://github.com/python-kasa/python-kasa/pull/1381) (@sdb9696)
|
||||||
|
- Simplify get\_protocol to prevent clashes with smartcam and robovac [\#1377](https://github.com/python-kasa/python-kasa/pull/1377) (@sdb9696)
|
||||||
|
- Add smartcam modules to package inits [\#1376](https://github.com/python-kasa/python-kasa/pull/1376) (@sdb9696)
|
||||||
|
- Enable saving of fixture files without git clone [\#1375](https://github.com/python-kasa/python-kasa/pull/1375) (@sdb9696)
|
||||||
|
- Force single for some smartcam requests [\#1374](https://github.com/python-kasa/python-kasa/pull/1374) (@sdb9696)
|
||||||
|
- Add new methods to dump\_devinfo [\#1373](https://github.com/python-kasa/python-kasa/pull/1373) (@sdb9696)
|
||||||
|
- Update cli, light modules, and docs to use FeatureAttributes [\#1364](https://github.com/python-kasa/python-kasa/pull/1364) (@sdb9696)
|
||||||
|
- Pass raw components to SmartChildDevice init [\#1363](https://github.com/python-kasa/python-kasa/pull/1363) (@sdb9696)
|
||||||
|
- Fix line endings in device\_fixtures.py [\#1361](https://github.com/python-kasa/python-kasa/pull/1361) (@sdb9696)
|
||||||
|
- Update dump\_devinfo for raw discovery json and common redactors [\#1358](https://github.com/python-kasa/python-kasa/pull/1358) (@sdb9696)
|
||||||
|
- Tweak RELEASING.md instructions for patch releases [\#1347](https://github.com/python-kasa/python-kasa/pull/1347) (@sdb9696)
|
||||||
|
- Scrub more vacuum keys [\#1328](https://github.com/python-kasa/python-kasa/pull/1328) (@rytilahti)
|
||||||
|
- Remove unnecessary check for python \<3.10 [\#1326](https://github.com/python-kasa/python-kasa/pull/1326) (@rytilahti)
|
||||||
|
- Add vacuum component queries to dump\_devinfo [\#1320](https://github.com/python-kasa/python-kasa/pull/1320) (@rytilahti)
|
||||||
|
- Handle missing mgt\_encryption\_schm in discovery [\#1318](https://github.com/python-kasa/python-kasa/pull/1318) (@sdb9696)
|
||||||
|
- Follow main package structure for tests [\#1317](https://github.com/python-kasa/python-kasa/pull/1317) (@rytilahti)
|
||||||
|
|
||||||
|
## [0.8.1](https://github.com/python-kasa/python-kasa/tree/0.8.1) (2024-12-06)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.8.0...0.8.1)
|
[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.8.0...0.8.1)
|
||||||
|
|
||||||
@ -46,28 +127,28 @@ Special thanks to @ryenitcher and @Puxtril for their new contributions to the im
|
|||||||
- Update cli feature command for actions not to require a value [\#1264](https://github.com/python-kasa/python-kasa/pull/1264) (@sdb9696)
|
- 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 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)
|
- 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)
|
- 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)
|
- 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)
|
- Allow getting Annotated features from modules [\#1018](https://github.com/python-kasa/python-kasa/pull/1018) (@sdb9696)
|
||||||
|
- Move TAPO smartcamera out of experimental package [\#1255](https://github.com/python-kasa/python-kasa/pull/1255) (@sdb9696)
|
||||||
|
- Use component queries to select smartcamera modules [\#1248](https://github.com/python-kasa/python-kasa/pull/1248) (@sdb9696)
|
||||||
- Add common Thermostat module [\#977](https://github.com/python-kasa/python-kasa/pull/977) (@sdb9696)
|
- Add common Thermostat module [\#977](https://github.com/python-kasa/python-kasa/pull/977) (@sdb9696)
|
||||||
|
|
||||||
**Fixed bugs:**
|
**Fixed bugs:**
|
||||||
|
|
||||||
- TP-Link Tapo S505D cannot disable gradual on/off [\#1309](https://github.com/python-kasa/python-kasa/issues/1309)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- Inconsistent emeter information between features and emeter cli [\#1308](https://github.com/python-kasa/python-kasa/issues/1308)
|
||||||
- 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)
|
- 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 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)
|
- 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)
|
- Make \_\_repr\_\_ work on discovery info [\#1233](https://github.com/python-kasa/python-kasa/pull/1233) (@rytilahti)
|
||||||
|
- Include duration when disabling smooth transition on/off [\#1313](https://github.com/python-kasa/python-kasa/pull/1313) (@rytilahti)
|
||||||
|
- Expose energy command to cli [\#1307](https://github.com/python-kasa/python-kasa/pull/1307) (@rytilahti)
|
||||||
|
|
||||||
**Added support for devices:**
|
**Added support for devices:**
|
||||||
|
|
||||||
@ -81,13 +162,11 @@ Special thanks to @ryenitcher and @Puxtril for their new contributions to the im
|
|||||||
**Documentation updates:**
|
**Documentation updates:**
|
||||||
|
|
||||||
- Use markdown footnotes in supported.md [\#1310](https://github.com/python-kasa/python-kasa/pull/1310) (@sdb9696)
|
- 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)
|
- Fixup contributing.md for running test against a real device [\#1236](https://github.com/python-kasa/python-kasa/pull/1236) (@sdb9696)
|
||||||
|
- Update docs for the new module attributes has/get feature [\#1301](https://github.com/python-kasa/python-kasa/pull/1301) (@sdb9696)
|
||||||
|
|
||||||
**Project maintenance:**
|
**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)
|
- 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)
|
- 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)
|
- Rename smartcamera to smartcam [\#1300](https://github.com/python-kasa/python-kasa/pull/1300) (@sdb9696)
|
||||||
@ -117,15 +196,17 @@ Special thanks to @ryenitcher and @Puxtril for their new contributions to the im
|
|||||||
- Add linkcheck to readthedocs CI [\#1253](https://github.com/python-kasa/python-kasa/pull/1253) (@rytilahti)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- Enable ruff check for ANN [\#1139](https://github.com/python-kasa/python-kasa/pull/1139) (@rytilahti)
|
||||||
|
- Rename tests/smartcamera to tests/smartcam [\#1315](https://github.com/python-kasa/python-kasa/pull/1315) (@sdb9696)
|
||||||
|
- Do not error on smartcam hub attached smartcam child devices [\#1314](https://github.com/python-kasa/python-kasa/pull/1314) (@sdb9696)
|
||||||
|
- Update smartcamera fixtures with components [\#1250](https://github.com/python-kasa/python-kasa/pull/1250) (@sdb9696)
|
||||||
|
- Fix warnings in our test suite [\#1246](https://github.com/python-kasa/python-kasa/pull/1246) (@rytilahti)
|
||||||
|
- Fix test framework running against real devices [\#1235](https://github.com/python-kasa/python-kasa/pull/1235) (@sdb9696)
|
||||||
|
- Update DiscoveryResult to use Mashumaro instead of pydantic [\#1231](https://github.com/python-kasa/python-kasa/pull/1231) (@sdb9696)
|
||||||
|
|
||||||
**Closed issues:**
|
**Closed issues:**
|
||||||
|
|
||||||
|
@ -178,6 +178,10 @@ The following devices have been tested and confirmed as working. If your device
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The hub attached Tapo buttons S200B and S200D do not currently support alerting when the button is pressed.
|
> The hub attached Tapo buttons S200B and S200D do not currently support alerting when the button is pressed.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Some firmware versions of Tapo Cameras will not authenticate unless you enable "Tapo Lab" > "Third-Party Compatibility" in the native Tapo app.
|
||||||
|
> Alternatively, you can factory reset and then prevent the device from accessing the internet.
|
||||||
|
|
||||||
<!--Do not edit text inside the SUPPORTED section below -->
|
<!--Do not edit text inside the SUPPORTED section below -->
|
||||||
<!--SUPPORTED_START-->
|
<!--SUPPORTED_START-->
|
||||||
### Supported Kasa devices
|
### Supported Kasa devices
|
||||||
@ -193,11 +197,11 @@ The following devices have been tested and confirmed as working. If your device
|
|||||||
### Supported Tapo[^1] devices
|
### Supported Tapo[^1] devices
|
||||||
|
|
||||||
- **Plugs**: P100, P110, P110M, P115, P125M, P135, TP15
|
- **Plugs**: P100, P110, P110M, P115, P125M, P135, TP15
|
||||||
- **Power Strips**: P300, P304M, TP25
|
- **Power Strips**: P210M, P300, P304M, P306, TP25
|
||||||
- **Wall Switches**: S500D, S505, S505D
|
- **Wall Switches**: S500D, S505, S505D
|
||||||
- **Bulbs**: L510B, L510E, L530E, L630
|
- **Bulbs**: L510B, L510E, L530E, L630
|
||||||
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
|
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
|
||||||
- **Cameras**: C210, C520WS, TC65
|
- **Cameras**: C100, C210, C225, C325WB, C520WS, TC65, TC70
|
||||||
- **Hubs**: H100, H200
|
- **Hubs**: H100, H200
|
||||||
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
|
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
|
||||||
|
|
||||||
@ -223,6 +227,7 @@ See [supported devices in our documentation](SUPPORTED.md) for more detailed inf
|
|||||||
|
|
||||||
* [Home Assistant](https://www.home-assistant.io/integrations/tplink/)
|
* [Home Assistant](https://www.home-assistant.io/integrations/tplink/)
|
||||||
* [MQTT access to TP-Link devices, using python-kasa](https://github.com/flavio-fernandes/mqtt2kasa)
|
* [MQTT access to TP-Link devices, using python-kasa](https://github.com/flavio-fernandes/mqtt2kasa)
|
||||||
|
* [Homebridge Kasa Python Plug-In](https://github.com/ZeliardM/homebridge-kasa-python)
|
||||||
|
|
||||||
### Other related projects
|
### Other related projects
|
||||||
|
|
||||||
|
16
SUPPORTED.md
16
SUPPORTED.md
@ -5,6 +5,9 @@ The following devices have been tested and confirmed as working. If your device
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The hub attached Tapo buttons S200B and S200D do not currently support alerting when the button is pressed.
|
> The hub attached Tapo buttons S200B and S200D do not currently support alerting when the button is pressed.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Some firmware versions of Tapo Cameras will not authenticate unless you enable "Tapo Lab" > "Third-Party Compatibility" in the native Tapo app.
|
||||||
|
> Alternatively, you can factory reset and then prevent the device from accessing the internet.
|
||||||
|
|
||||||
<!--Do not edit text inside the SUPPORTED section below -->
|
<!--Do not edit text inside the SUPPORTED section below -->
|
||||||
<!--SUPPORTED_START-->
|
<!--SUPPORTED_START-->
|
||||||
@ -199,17 +202,22 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
|
|||||||
- Hardware: 1.0 (US) / Firmware: 1.1.0
|
- Hardware: 1.0 (US) / Firmware: 1.1.0
|
||||||
- **P135**
|
- **P135**
|
||||||
- Hardware: 1.0 (US) / Firmware: 1.0.5
|
- Hardware: 1.0 (US) / Firmware: 1.0.5
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.2.0
|
||||||
- **TP15**
|
- **TP15**
|
||||||
- Hardware: 1.0 (US) / Firmware: 1.0.3
|
- Hardware: 1.0 (US) / Firmware: 1.0.3
|
||||||
|
|
||||||
### Power Strips
|
### Power Strips
|
||||||
|
|
||||||
|
- **P210M**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.3
|
||||||
- **P300**
|
- **P300**
|
||||||
- Hardware: 1.0 (EU) / Firmware: 1.0.13
|
- Hardware: 1.0 (EU) / Firmware: 1.0.13
|
||||||
- Hardware: 1.0 (EU) / Firmware: 1.0.15
|
- Hardware: 1.0 (EU) / Firmware: 1.0.15
|
||||||
- Hardware: 1.0 (EU) / Firmware: 1.0.7
|
- Hardware: 1.0 (EU) / Firmware: 1.0.7
|
||||||
- **P304M**
|
- **P304M**
|
||||||
- Hardware: 1.0 (UK) / Firmware: 1.0.3
|
- Hardware: 1.0 (UK) / Firmware: 1.0.3
|
||||||
|
- **P306**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.2
|
||||||
- **TP25**
|
- **TP25**
|
||||||
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
||||||
|
|
||||||
@ -255,13 +263,21 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
|
|||||||
|
|
||||||
### Cameras
|
### Cameras
|
||||||
|
|
||||||
|
- **C100**
|
||||||
|
- Hardware: 4.0 / Firmware: 1.3.14
|
||||||
- **C210**
|
- **C210**
|
||||||
- Hardware: 2.0 (EU) / Firmware: 1.4.2
|
- Hardware: 2.0 (EU) / Firmware: 1.4.2
|
||||||
- Hardware: 2.0 (EU) / Firmware: 1.4.3
|
- Hardware: 2.0 (EU) / Firmware: 1.4.3
|
||||||
|
- **C225**
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.11
|
||||||
|
- **C325WB**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.1.17
|
||||||
- **C520WS**
|
- **C520WS**
|
||||||
- Hardware: 1.0 (US) / Firmware: 1.2.8
|
- Hardware: 1.0 (US) / Firmware: 1.2.8
|
||||||
- **TC65**
|
- **TC65**
|
||||||
- Hardware: 1.0 / Firmware: 1.3.9
|
- Hardware: 1.0 / Firmware: 1.3.9
|
||||||
|
- **TC70**
|
||||||
|
- Hardware: 3.0 / Firmware: 1.3.11
|
||||||
|
|
||||||
### Hubs
|
### Hubs
|
||||||
|
|
||||||
|
@ -10,8 +10,6 @@ and finally execute a query to query all of them at once.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import collections.abc
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@ -19,6 +17,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -39,30 +38,80 @@ from kasa import (
|
|||||||
)
|
)
|
||||||
from kasa.device_factory import get_protocol
|
from kasa.device_factory import get_protocol
|
||||||
from kasa.deviceconfig import DeviceEncryptionType, DeviceFamily
|
from kasa.deviceconfig import DeviceEncryptionType, DeviceFamily
|
||||||
from kasa.discover import DiscoveryResult
|
from kasa.discover import (
|
||||||
|
NEW_DISCOVERY_REDACTORS,
|
||||||
|
DiscoveredRaw,
|
||||||
|
DiscoveryResult,
|
||||||
|
)
|
||||||
from kasa.exceptions import SmartErrorCode
|
from kasa.exceptions import SmartErrorCode
|
||||||
from kasa.protocols import IotProtocol
|
from kasa.protocols import IotProtocol
|
||||||
|
from kasa.protocols.iotprotocol import REDACTORS as IOT_REDACTORS
|
||||||
|
from kasa.protocols.protocol import redact_data
|
||||||
from kasa.protocols.smartcamprotocol import (
|
from kasa.protocols.smartcamprotocol import (
|
||||||
SmartCamProtocol,
|
SmartCamProtocol,
|
||||||
_ChildCameraProtocolWrapper,
|
_ChildCameraProtocolWrapper,
|
||||||
)
|
)
|
||||||
|
from kasa.protocols.smartprotocol import REDACTORS as SMART_REDACTORS
|
||||||
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
||||||
from kasa.smart import SmartChildDevice, SmartDevice
|
from kasa.smart import SmartChildDevice, SmartDevice
|
||||||
from kasa.smartcam import SmartCamDevice
|
from kasa.smartcam import SmartCamDevice
|
||||||
|
|
||||||
Call = namedtuple("Call", "module method")
|
Call = namedtuple("Call", "module method")
|
||||||
FixtureResult = namedtuple("FixtureResult", "filename, folder, data")
|
FixtureResult = namedtuple("FixtureResult", "filename, folder, data, protocol_suffix")
|
||||||
|
|
||||||
SMART_FOLDER = "tests/fixtures/smart/"
|
SMART_FOLDER = "tests/fixtures/smart/"
|
||||||
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
|
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
|
||||||
SMART_CHILD_FOLDER = "tests/fixtures/smart/child/"
|
SMART_CHILD_FOLDER = "tests/fixtures/smart/child/"
|
||||||
IOT_FOLDER = "tests/fixtures/iot/"
|
IOT_FOLDER = "tests/fixtures/iot/"
|
||||||
|
|
||||||
|
SMART_PROTOCOL_SUFFIX = "SMART"
|
||||||
|
SMARTCAM_SUFFIX = "SMARTCAM"
|
||||||
|
SMART_CHILD_SUFFIX = "SMART.CHILD"
|
||||||
|
IOT_SUFFIX = "IOT"
|
||||||
|
|
||||||
|
NO_GIT_FIXTURE_FOLDER = "kasa-fixtures"
|
||||||
|
|
||||||
ENCRYPT_TYPES = [encrypt_type.value for encrypt_type in DeviceEncryptionType]
|
ENCRYPT_TYPES = [encrypt_type.value for encrypt_type in DeviceEncryptionType]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_redactors(redactors: dict[str, Callable[[Any], Any] | None]):
|
||||||
|
"""Wrap the redactors for dump_devinfo.
|
||||||
|
|
||||||
|
Will replace all partial REDACT_ values with zeros.
|
||||||
|
If the data item is already scrubbed by dump_devinfo will leave as-is.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _wrap(key: str) -> Any:
|
||||||
|
def _wrapped(redactor: Callable[[Any], Any] | None) -> Any | None:
|
||||||
|
if redactor is None:
|
||||||
|
return lambda x: "**SCRUBBED**"
|
||||||
|
|
||||||
|
def _redact_to_zeros(x: Any) -> Any:
|
||||||
|
if isinstance(x, str) and "REDACT" in x:
|
||||||
|
return re.sub(r"\w", "0", x)
|
||||||
|
if isinstance(x, dict):
|
||||||
|
for k, v in x.items():
|
||||||
|
x[k] = _redact_to_zeros(v)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def _scrub(x: Any) -> Any:
|
||||||
|
if key in {"ip", "local_ip"}:
|
||||||
|
return "127.0.0.123"
|
||||||
|
# Already scrubbed by dump_devinfo
|
||||||
|
if isinstance(x, str) and "SCRUBBED" in x:
|
||||||
|
return x
|
||||||
|
default = redactor(x)
|
||||||
|
return _redact_to_zeros(default)
|
||||||
|
|
||||||
|
return _scrub
|
||||||
|
|
||||||
|
return _wrapped(redactors[key])
|
||||||
|
|
||||||
|
return {key: _wrap(key) for key in redactors}
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class SmartCall:
|
class SmartCall:
|
||||||
"""Class for smart and smartcam calls."""
|
"""Class for smart and smartcam calls."""
|
||||||
@ -74,115 +123,6 @@ class SmartCall:
|
|||||||
supports_multiple: bool = True
|
supports_multiple: bool = True
|
||||||
|
|
||||||
|
|
||||||
def scrub(res):
|
|
||||||
"""Remove identifiers from the given dict."""
|
|
||||||
keys_to_scrub = [
|
|
||||||
"deviceId",
|
|
||||||
"fwId",
|
|
||||||
"hwId",
|
|
||||||
"oemId",
|
|
||||||
"mac",
|
|
||||||
"mic_mac",
|
|
||||||
"latitude_i",
|
|
||||||
"longitude_i",
|
|
||||||
"latitude",
|
|
||||||
"longitude",
|
|
||||||
"la", # lat on ks240
|
|
||||||
"lo", # lon on ks240
|
|
||||||
"owner",
|
|
||||||
"device_id",
|
|
||||||
"ip",
|
|
||||||
"ssid",
|
|
||||||
"hw_id",
|
|
||||||
"fw_id",
|
|
||||||
"oem_id",
|
|
||||||
"nickname",
|
|
||||||
"alias",
|
|
||||||
"bssid",
|
|
||||||
"channel",
|
|
||||||
"original_device_id", # for child devices on strips
|
|
||||||
"parent_device_id", # for hub children
|
|
||||||
"setup_code", # matter
|
|
||||||
"setup_payload", # matter
|
|
||||||
"mfi_setup_code", # mfi_ for homekit
|
|
||||||
"mfi_setup_id",
|
|
||||||
"mfi_token_token",
|
|
||||||
"mfi_token_uuid",
|
|
||||||
"dev_id",
|
|
||||||
"device_name",
|
|
||||||
"device_alias",
|
|
||||||
"connect_ssid",
|
|
||||||
"encrypt_info",
|
|
||||||
"local_ip",
|
|
||||||
"username",
|
|
||||||
# vacuum
|
|
||||||
"board_sn",
|
|
||||||
"custom_sn",
|
|
||||||
"location",
|
|
||||||
]
|
|
||||||
|
|
||||||
for k, v in res.items():
|
|
||||||
if isinstance(v, collections.abc.Mapping):
|
|
||||||
if k == "encrypt_info":
|
|
||||||
if "data" in v:
|
|
||||||
v["data"] = ""
|
|
||||||
if "key" in v:
|
|
||||||
v["key"] = ""
|
|
||||||
else:
|
|
||||||
res[k] = scrub(res.get(k))
|
|
||||||
elif (
|
|
||||||
isinstance(v, list)
|
|
||||||
and len(v) > 0
|
|
||||||
and isinstance(v[0], collections.abc.Mapping)
|
|
||||||
):
|
|
||||||
res[k] = [scrub(vi) for vi in v]
|
|
||||||
else:
|
|
||||||
if k in keys_to_scrub:
|
|
||||||
if k in ["mac", "mic_mac"]:
|
|
||||||
# Some macs have : or - as a separator and others do not
|
|
||||||
if len(v) == 12:
|
|
||||||
v = f"{v[:6]}000000"
|
|
||||||
else:
|
|
||||||
delim = ":" if ":" in v else "-"
|
|
||||||
rest = delim.join(
|
|
||||||
format(s, "02x") for s in bytes.fromhex("000000")
|
|
||||||
)
|
|
||||||
v = f"{v[:8]}{delim}{rest}"
|
|
||||||
elif k in ["latitude", "latitude_i", "longitude", "longitude_i"]:
|
|
||||||
v = 0
|
|
||||||
elif k in ["ip", "local_ip"]:
|
|
||||||
v = "127.0.0.123"
|
|
||||||
elif k in ["ssid"]:
|
|
||||||
# Need a valid base64 value here
|
|
||||||
v = base64.b64encode(b"#MASKED_SSID#").decode()
|
|
||||||
elif k in ["nickname"]:
|
|
||||||
v = base64.b64encode(b"#MASKED_NAME#").decode()
|
|
||||||
elif k in [
|
|
||||||
"alias",
|
|
||||||
"device_alias",
|
|
||||||
"device_name",
|
|
||||||
"username",
|
|
||||||
"location",
|
|
||||||
]:
|
|
||||||
v = "#MASKED_NAME#"
|
|
||||||
elif isinstance(res[k], int):
|
|
||||||
v = 0
|
|
||||||
elif k in ["map_data"]: #
|
|
||||||
v = "#SCRUBBED_MAPDATA#"
|
|
||||||
elif k in ["device_id", "dev_id"] and "SCRUBBED" in v:
|
|
||||||
pass # already scrubbed
|
|
||||||
elif k == ["device_id", "dev_id"] and len(v) > 40:
|
|
||||||
# retain the last two chars when scrubbing child ids
|
|
||||||
end = v[-2:]
|
|
||||||
v = re.sub(r"\w", "0", v)
|
|
||||||
v = v[:40] + end
|
|
||||||
else:
|
|
||||||
v = re.sub(r"\w", "0", v)
|
|
||||||
|
|
||||||
res[k] = v
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def default_to_regular(d):
|
def default_to_regular(d):
|
||||||
"""Convert nested defaultdicts to regular ones.
|
"""Convert nested defaultdicts to regular ones.
|
||||||
|
|
||||||
@ -207,9 +147,19 @@ async def handle_device(
|
|||||||
]
|
]
|
||||||
|
|
||||||
for fixture_result in fixture_results:
|
for fixture_result in fixture_results:
|
||||||
save_filename = Path(basedir) / fixture_result.folder / fixture_result.filename
|
save_folder = Path(basedir) / fixture_result.folder
|
||||||
|
if save_folder.exists():
|
||||||
|
save_filename = save_folder / f"{fixture_result.filename}.json"
|
||||||
|
else:
|
||||||
|
# If being run without git clone
|
||||||
|
save_folder = Path(basedir) / NO_GIT_FIXTURE_FOLDER
|
||||||
|
save_folder.mkdir(exist_ok=True)
|
||||||
|
save_filename = (
|
||||||
|
save_folder
|
||||||
|
/ f"{fixture_result.filename}-{fixture_result.protocol_suffix}.json"
|
||||||
|
)
|
||||||
|
|
||||||
pprint(scrub(fixture_result.data))
|
pprint(fixture_result.data)
|
||||||
if autosave:
|
if autosave:
|
||||||
save = "y"
|
save = "y"
|
||||||
else:
|
else:
|
||||||
@ -300,6 +250,12 @@ async def handle_device(
|
|||||||
type=bool,
|
type=bool,
|
||||||
help="Set flag if the device encryption uses https.",
|
help="Set flag if the device encryption uses https.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--timeout",
|
||||||
|
required=False,
|
||||||
|
default=15,
|
||||||
|
help="Timeout for queries.",
|
||||||
|
)
|
||||||
@click.option("--port", help="Port override", type=int)
|
@click.option("--port", help="Port override", type=int)
|
||||||
async def cli(
|
async def cli(
|
||||||
host,
|
host,
|
||||||
@ -317,6 +273,7 @@ async def cli(
|
|||||||
device_family,
|
device_family,
|
||||||
login_version,
|
login_version,
|
||||||
port,
|
port,
|
||||||
|
timeout,
|
||||||
):
|
):
|
||||||
"""Generate devinfo files for devices.
|
"""Generate devinfo files for devices.
|
||||||
|
|
||||||
@ -325,6 +282,11 @@ async def cli(
|
|||||||
if debug:
|
if debug:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
raw_discovery = {}
|
||||||
|
|
||||||
|
def capture_raw(discovered: DiscoveredRaw):
|
||||||
|
raw_discovery[discovered["meta"]["ip"]] = discovered["discovery_response"]
|
||||||
|
|
||||||
credentials = Credentials(username=username, password=password)
|
credentials = Credentials(username=username, password=password)
|
||||||
if host is not None:
|
if host is not None:
|
||||||
if discovery_info:
|
if discovery_info:
|
||||||
@ -342,6 +304,7 @@ async def cli(
|
|||||||
connection_type=connection_type,
|
connection_type=connection_type,
|
||||||
port_override=port,
|
port_override=port,
|
||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
device = await Device.connect(config=dc)
|
device = await Device.connect(config=dc)
|
||||||
await handle_device(
|
await handle_device(
|
||||||
@ -363,6 +326,7 @@ async def cli(
|
|||||||
port_override=port,
|
port_override=port,
|
||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
connection_type=ctype,
|
connection_type=ctype,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
if protocol := get_protocol(config):
|
if protocol := get_protocol(config):
|
||||||
await handle_device(basedir, autosave, protocol, batch_size=batch_size)
|
await handle_device(basedir, autosave, protocol, batch_size=batch_size)
|
||||||
@ -377,12 +341,17 @@ async def cli(
|
|||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
port=port,
|
port=port,
|
||||||
discovery_timeout=discovery_timeout,
|
discovery_timeout=discovery_timeout,
|
||||||
|
timeout=timeout,
|
||||||
|
on_discovered_raw=capture_raw,
|
||||||
)
|
)
|
||||||
|
discovery_info = raw_discovery[device.host]
|
||||||
|
if decrypted_data := device._discovery_info.get("decrypted_data"):
|
||||||
|
discovery_info["result"]["decrypted_data"] = decrypted_data
|
||||||
await handle_device(
|
await handle_device(
|
||||||
basedir,
|
basedir,
|
||||||
autosave,
|
autosave,
|
||||||
device.protocol,
|
device.protocol,
|
||||||
discovery_info=device._discovery_info,
|
discovery_info=discovery_info,
|
||||||
batch_size=batch_size,
|
batch_size=batch_size,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -391,21 +360,29 @@ async def cli(
|
|||||||
f" {target}. Use --target to override."
|
f" {target}. Use --target to override."
|
||||||
)
|
)
|
||||||
devices = await Discover.discover(
|
devices = await Discover.discover(
|
||||||
target=target, credentials=credentials, discovery_timeout=discovery_timeout
|
target=target,
|
||||||
|
credentials=credentials,
|
||||||
|
discovery_timeout=discovery_timeout,
|
||||||
|
timeout=timeout,
|
||||||
|
on_discovered_raw=capture_raw,
|
||||||
)
|
)
|
||||||
click.echo(f"Detected {len(devices)} devices")
|
click.echo(f"Detected {len(devices)} devices")
|
||||||
for dev in devices.values():
|
for dev in devices.values():
|
||||||
|
discovery_info = raw_discovery[dev.host]
|
||||||
|
if decrypted_data := dev._discovery_info.get("decrypted_data"):
|
||||||
|
discovery_info["result"]["decrypted_data"] = decrypted_data
|
||||||
|
|
||||||
await handle_device(
|
await handle_device(
|
||||||
basedir,
|
basedir,
|
||||||
autosave,
|
autosave,
|
||||||
dev.protocol,
|
dev.protocol,
|
||||||
discovery_info=dev._discovery_info,
|
discovery_info=discovery_info,
|
||||||
batch_size=batch_size,
|
batch_size=batch_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_legacy_fixture(
|
async def get_legacy_fixture(
|
||||||
protocol: IotProtocol, *, discovery_info: dict[str, Any] | None
|
protocol: IotProtocol, *, discovery_info: dict[str, dict[str, Any]] | None
|
||||||
) -> FixtureResult:
|
) -> FixtureResult:
|
||||||
"""Get fixture for legacy IOT style protocol."""
|
"""Get fixture for legacy IOT style protocol."""
|
||||||
items = [
|
items = [
|
||||||
@ -475,11 +452,21 @@ async def get_legacy_fixture(
|
|||||||
_echo_error(f"Unable to query all successes at once: {ex}")
|
_echo_error(f"Unable to query all successes at once: {ex}")
|
||||||
finally:
|
finally:
|
||||||
await protocol.close()
|
await protocol.close()
|
||||||
|
|
||||||
|
final = redact_data(final, _wrap_redactors(IOT_REDACTORS))
|
||||||
|
|
||||||
|
# Scrub the child device ids
|
||||||
|
if children := final.get("system", {}).get("get_sysinfo", {}).get("children"):
|
||||||
|
for index, child in enumerate(children):
|
||||||
|
if "id" not in child:
|
||||||
|
_LOGGER.error("Could not find a device for the child device: %s", child)
|
||||||
|
else:
|
||||||
|
child["id"] = f"SCRUBBED_CHILD_DEVICE_ID_{index + 1}"
|
||||||
|
|
||||||
if discovery_info and not discovery_info.get("system"):
|
if discovery_info and not discovery_info.get("system"):
|
||||||
# Need to recreate a DiscoverResult here because we don't want the aliases
|
final["discovery_result"] = redact_data(
|
||||||
# in the fixture, we want the actual field names as returned by the device.
|
discovery_info, _wrap_redactors(NEW_DISCOVERY_REDACTORS)
|
||||||
dr = DiscoveryResult.from_dict(discovery_info)
|
)
|
||||||
final["discovery_result"] = dr.to_dict()
|
|
||||||
|
|
||||||
click.echo(f"Got {len(successes)} successes")
|
click.echo(f"Got {len(successes)} successes")
|
||||||
click.echo(click.style("## device info file ##", bold=True))
|
click.echo(click.style("## device info file ##", bold=True))
|
||||||
@ -489,9 +476,14 @@ async def get_legacy_fixture(
|
|||||||
hw_version = sysinfo["hw_ver"]
|
hw_version = sysinfo["hw_ver"]
|
||||||
sw_version = sysinfo["sw_ver"]
|
sw_version = sysinfo["sw_ver"]
|
||||||
sw_version = sw_version.split(" ", maxsplit=1)[0]
|
sw_version = sw_version.split(" ", maxsplit=1)[0]
|
||||||
save_filename = f"{model}_{hw_version}_{sw_version}.json"
|
save_filename = f"{model}_{hw_version}_{sw_version}"
|
||||||
copy_folder = IOT_FOLDER
|
copy_folder = IOT_FOLDER
|
||||||
return FixtureResult(filename=save_filename, folder=copy_folder, data=final)
|
return FixtureResult(
|
||||||
|
filename=save_filename,
|
||||||
|
folder=copy_folder,
|
||||||
|
data=final,
|
||||||
|
protocol_suffix=IOT_SUFFIX,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _echo_error(msg: str):
|
def _echo_error(msg: str):
|
||||||
@ -860,14 +852,20 @@ def get_smart_child_fixture(response):
|
|||||||
model = model_info.long_name
|
model = model_info.long_name
|
||||||
if model_info.region is not None:
|
if model_info.region is not None:
|
||||||
model = f"{model}({model_info.region})"
|
model = f"{model}({model_info.region})"
|
||||||
save_filename = f"{model}_{hw_version}_{fw_version}.json"
|
save_filename = f"{model}_{hw_version}_{fw_version}"
|
||||||
return FixtureResult(
|
return FixtureResult(
|
||||||
filename=save_filename, folder=SMART_CHILD_FOLDER, data=response
|
filename=save_filename,
|
||||||
|
folder=SMART_CHILD_FOLDER,
|
||||||
|
data=response,
|
||||||
|
protocol_suffix=SMART_CHILD_SUFFIX,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_smart_fixtures(
|
async def get_smart_fixtures(
|
||||||
protocol: SmartProtocol, *, discovery_info: dict[str, Any] | None, batch_size: int
|
protocol: SmartProtocol,
|
||||||
|
*,
|
||||||
|
discovery_info: dict[str, dict[str, Any]] | None,
|
||||||
|
batch_size: int,
|
||||||
) -> list[FixtureResult]:
|
) -> list[FixtureResult]:
|
||||||
"""Get fixture for new TAPO style protocol."""
|
"""Get fixture for new TAPO style protocol."""
|
||||||
if isinstance(protocol, SmartCamProtocol):
|
if isinstance(protocol, SmartCamProtocol):
|
||||||
@ -963,6 +961,7 @@ async def get_smart_fixtures(
|
|||||||
and (child_model := response["get_device_info"].get("model"))
|
and (child_model := response["get_device_info"].get("model"))
|
||||||
and child_model != parent_model
|
and child_model != parent_model
|
||||||
):
|
):
|
||||||
|
response = redact_data(response, _wrap_redactors(SMART_REDACTORS))
|
||||||
fixture_results.append(get_smart_child_fixture(response))
|
fixture_results.append(get_smart_child_fixture(response))
|
||||||
else:
|
else:
|
||||||
cd = final.setdefault("child_devices", {})
|
cd = final.setdefault("child_devices", {})
|
||||||
@ -978,43 +977,56 @@ async def get_smart_fixtures(
|
|||||||
child["device_id"] = scrubbed_device_ids[device_id]
|
child["device_id"] = scrubbed_device_ids[device_id]
|
||||||
|
|
||||||
# Scrub the device ids in the parent for the smart camera protocol
|
# Scrub the device ids in the parent for the smart camera protocol
|
||||||
if gc := final.get("getChildDeviceList"):
|
if gc := final.get("getChildDeviceComponentList"):
|
||||||
for child in gc["child_device_list"]:
|
for child in gc["child_component_list"]:
|
||||||
|
device_id = child["device_id"]
|
||||||
|
child["device_id"] = scrubbed_device_ids[device_id]
|
||||||
|
for child in final["getChildDeviceList"]["child_device_list"]:
|
||||||
if device_id := child.get("device_id"):
|
if device_id := child.get("device_id"):
|
||||||
child["device_id"] = scrubbed_device_ids[device_id]
|
child["device_id"] = scrubbed_device_ids[device_id]
|
||||||
continue
|
continue
|
||||||
if device_id := child.get("dev_id"):
|
elif dev_id := child.get("dev_id"):
|
||||||
child["dev_id"] = scrubbed_device_ids[device_id]
|
child["dev_id"] = scrubbed_device_ids[dev_id]
|
||||||
continue
|
continue
|
||||||
_LOGGER.error("Could not find a device for the child device: %s", child)
|
_LOGGER.error("Could not find a device for the child device: %s", child)
|
||||||
|
|
||||||
# Need to recreate a DiscoverResult here because we don't want the aliases
|
final = redact_data(final, _wrap_redactors(SMART_REDACTORS))
|
||||||
# in the fixture, we want the actual field names as returned by the device.
|
discovery_result = None
|
||||||
if discovery_info:
|
if discovery_info:
|
||||||
dr = DiscoveryResult.from_dict(discovery_info) # type: ignore
|
final["discovery_result"] = redact_data(
|
||||||
final["discovery_result"] = dr.to_dict()
|
discovery_info, _wrap_redactors(NEW_DISCOVERY_REDACTORS)
|
||||||
|
)
|
||||||
|
discovery_result = discovery_info["result"]
|
||||||
|
|
||||||
click.echo(f"Got {len(successes)} successes")
|
click.echo(f"Got {len(successes)} successes")
|
||||||
click.echo(click.style("## device info file ##", bold=True))
|
click.echo(click.style("## device info file ##", bold=True))
|
||||||
|
|
||||||
if "get_device_info" in final:
|
if "get_device_info" in final:
|
||||||
# smart protocol
|
# smart protocol
|
||||||
model_info = SmartDevice._get_device_info(final, discovery_info)
|
model_info = SmartDevice._get_device_info(final, discovery_result)
|
||||||
copy_folder = SMART_FOLDER
|
copy_folder = SMART_FOLDER
|
||||||
|
protocol_suffix = SMART_PROTOCOL_SUFFIX
|
||||||
else:
|
else:
|
||||||
# smart camera protocol
|
# smart camera protocol
|
||||||
model_info = SmartCamDevice._get_device_info(final, discovery_info)
|
model_info = SmartCamDevice._get_device_info(final, discovery_result)
|
||||||
copy_folder = SMARTCAM_FOLDER
|
copy_folder = SMARTCAM_FOLDER
|
||||||
|
protocol_suffix = SMARTCAM_SUFFIX
|
||||||
hw_version = model_info.hardware_version
|
hw_version = model_info.hardware_version
|
||||||
sw_version = model_info.firmware_version
|
sw_version = model_info.firmware_version
|
||||||
model = model_info.long_name
|
model = model_info.long_name
|
||||||
if model_info.region is not None:
|
if model_info.region is not None:
|
||||||
model = f"{model}({model_info.region})"
|
model = f"{model}({model_info.region})"
|
||||||
|
|
||||||
save_filename = f"{model}_{hw_version}_{sw_version}.json"
|
save_filename = f"{model}_{hw_version}_{sw_version}"
|
||||||
|
|
||||||
fixture_results.insert(
|
fixture_results.insert(
|
||||||
0, FixtureResult(filename=save_filename, folder=copy_folder, data=final)
|
0,
|
||||||
|
FixtureResult(
|
||||||
|
filename=save_filename,
|
||||||
|
folder=copy_folder,
|
||||||
|
data=final,
|
||||||
|
protocol_suffix=protocol_suffix,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return fixture_results
|
return fixture_results
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ def _get_supported_devices(
|
|||||||
fixture_data = json.load(f)
|
fixture_data = json.load(f)
|
||||||
|
|
||||||
model_info = device_cls._get_device_info(
|
model_info = device_cls._get_device_info(
|
||||||
fixture_data, fixture_data.get("discovery_result")
|
fixture_data, fixture_data.get("discovery_result", {}).get("result")
|
||||||
)
|
)
|
||||||
|
|
||||||
supported_type = DEVICE_TYPE_TO_PRODUCT_GROUP[model_info.device_type]
|
supported_type = DEVICE_TYPE_TO_PRODUCT_GROUP[model_info.device_type]
|
||||||
|
@ -60,4 +60,7 @@ SMARTCAM_REQUESTS: list[dict] = [
|
|||||||
{"get": {"motor": {"name": ["capability"]}}},
|
{"get": {"motor": {"name": ["capability"]}}},
|
||||||
{"get": {"audio_capability": {"name": ["device_speaker", "device_microphone"]}}},
|
{"get": {"audio_capability": {"name": ["device_speaker", "device_microphone"]}}},
|
||||||
{"get": {"audio_config": {"name": ["speaker", "microphone"]}}},
|
{"get": {"audio_config": {"name": ["speaker", "microphone"]}}},
|
||||||
|
{"getMatterSetupInfo": {"matter": {}}},
|
||||||
|
{"getConnectStatus": {"onboarding": {"get_connect_status": {}}}},
|
||||||
|
{"scanApList": {"onboarding": {"scan": {}}}},
|
||||||
]
|
]
|
||||||
|
128
devtools/update_fixtures.py
Normal file
128
devtools/update_fixtures.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
"""Module to mass update fixture files."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import asyncclick as click
|
||||||
|
|
||||||
|
from devtools.dump_devinfo import _wrap_redactors
|
||||||
|
from kasa.discover import NEW_DISCOVERY_REDACTORS, redact_data
|
||||||
|
from kasa.protocols.iotprotocol import REDACTORS as IOT_REDACTORS
|
||||||
|
from kasa.protocols.smartprotocol import REDACTORS as SMART_REDACTORS
|
||||||
|
|
||||||
|
FIXTURE_FOLDER = "tests/fixtures/"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def update_fixtures(update_func: Callable[[dict], bool], *, dry_run: bool) -> None:
|
||||||
|
"""Run the update function against the fixtures."""
|
||||||
|
for file in Path(FIXTURE_FOLDER).glob("**/*.json"):
|
||||||
|
with file.open("r") as f:
|
||||||
|
fixture_data = json.load(f)
|
||||||
|
|
||||||
|
if file.parent.name == "serialization":
|
||||||
|
continue
|
||||||
|
changed = update_func(fixture_data)
|
||||||
|
if changed:
|
||||||
|
click.echo(f"Will update {file.name}\n")
|
||||||
|
if changed and not dry_run:
|
||||||
|
with file.open("w") as f:
|
||||||
|
json.dump(fixture_data, f, sort_keys=True, indent=4)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _discovery_result_update(info) -> bool:
|
||||||
|
"""Update discovery_result to be the raw result and error_code."""
|
||||||
|
if (disco_result := info.get("discovery_result")) and "result" not in disco_result:
|
||||||
|
info["discovery_result"] = {
|
||||||
|
"result": disco_result,
|
||||||
|
"error_code": 0,
|
||||||
|
}
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _child_device_id_update(info) -> bool:
|
||||||
|
"""Update child device ids to be the scrubbed ids from dump_devinfo."""
|
||||||
|
changed = False
|
||||||
|
if get_child_device_list := info.get("get_child_device_list"):
|
||||||
|
child_device_list = get_child_device_list["child_device_list"]
|
||||||
|
child_component_list = info["get_child_device_component_list"][
|
||||||
|
"child_component_list"
|
||||||
|
]
|
||||||
|
for index, child_device in enumerate(child_device_list):
|
||||||
|
child_component = child_component_list[index]
|
||||||
|
if "SCRUBBED" not in child_device["device_id"]:
|
||||||
|
dev_id = f"SCRUBBED_CHILD_DEVICE_ID_{index + 1}"
|
||||||
|
click.echo(
|
||||||
|
f"child_device_id{index}: {child_device['device_id']} -> {dev_id}"
|
||||||
|
)
|
||||||
|
child_device["device_id"] = dev_id
|
||||||
|
child_component["device_id"] = dev_id
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if children := info.get("system", {}).get("get_sysinfo", {}).get("children"):
|
||||||
|
for index, child_device in enumerate(children):
|
||||||
|
if "SCRUBBED" not in child_device["id"]:
|
||||||
|
dev_id = f"SCRUBBED_CHILD_DEVICE_ID_{index + 1}"
|
||||||
|
click.echo(f"child_device_id{index}: {child_device['id']} -> {dev_id}")
|
||||||
|
child_device["id"] = dev_id
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def _diff_data(fullkey, data1, data2, diffs):
|
||||||
|
if isinstance(data1, dict):
|
||||||
|
for k, v in data1.items():
|
||||||
|
_diff_data(fullkey + "/" + k, v, data2[k], diffs)
|
||||||
|
elif isinstance(data1, list):
|
||||||
|
for index, item in enumerate(data1):
|
||||||
|
_diff_data(fullkey + "/" + str(index), item, data2[index], diffs)
|
||||||
|
elif data1 != data2:
|
||||||
|
diffs[fullkey] = (data1, data2)
|
||||||
|
|
||||||
|
|
||||||
|
def _redactor_result_update(info) -> bool:
|
||||||
|
"""Update fixtures with the output using the common redactors."""
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
redactors = IOT_REDACTORS if "system" in info else SMART_REDACTORS
|
||||||
|
|
||||||
|
for key, val in info.items():
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
continue
|
||||||
|
if key == "discovery_result":
|
||||||
|
info[key] = redact_data(val, _wrap_redactors(NEW_DISCOVERY_REDACTORS))
|
||||||
|
else:
|
||||||
|
info[key] = redact_data(val, _wrap_redactors(redactors))
|
||||||
|
diffs: dict[str, tuple[str, str]] = {}
|
||||||
|
_diff_data(key, val, info[key], diffs)
|
||||||
|
if diffs:
|
||||||
|
for k, v in diffs.items():
|
||||||
|
click.echo(f"{k}: {v[0]} -> {v[1]}")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
@click.option(
|
||||||
|
"--dry-run/--no-dry-run",
|
||||||
|
default=False,
|
||||||
|
is_flag=True,
|
||||||
|
type=bool,
|
||||||
|
help="Perform a dry run without saving.",
|
||||||
|
)
|
||||||
|
@click.command()
|
||||||
|
async def cli(dry_run: bool) -> None:
|
||||||
|
"""Cli method fo rupdating fixtures."""
|
||||||
|
update_fixtures(_discovery_result_update, dry_run=dry_run)
|
||||||
|
update_fixtures(_child_device_id_update, dry_run=dry_run)
|
||||||
|
update_fixtures(_redactor_result_update, dry_run=dry_run)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
13
docs/source/featureattributes.md
Normal file
13
docs/source/featureattributes.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Some modules have attributes that may not be supported by the device.
|
||||||
|
These attributes will be annotated with a `FeatureAttribute` return type.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@property
|
||||||
|
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
|
||||||
|
"""Return the current HSV state of the bulb."""
|
||||||
|
```
|
||||||
|
|
||||||
|
You can test whether a `FeatureAttribute` is supported by the device with {meth}`kasa.Module.has_feature`
|
||||||
|
or {meth}`kasa.Module.get_feature` which will return `None` if not supported.
|
||||||
|
Calling these methods on attributes not annotated with a `FeatureAttribute` return type will return an error.
|
@ -13,11 +13,13 @@
|
|||||||
|
|
||||||
## Device
|
## Device
|
||||||
|
|
||||||
|
% N.B. Credentials clashes with autodoc
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. autoclass:: Device
|
.. autoclass:: Device
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
:exclude-members: Credentials
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -28,7 +30,6 @@
|
|||||||
.. autoclass:: Credentials
|
.. autoclass:: Credentials
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:noindex:
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -61,15 +62,11 @@
|
|||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. autoclass:: Module
|
.. autoclass:: Module
|
||||||
:noindex:
|
|
||||||
:members:
|
:members:
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. autoclass:: Feature
|
.. autoclass:: Feature
|
||||||
:noindex:
|
|
||||||
:members:
|
:members:
|
||||||
:inherited-members:
|
:inherited-members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
@ -77,7 +74,6 @@
|
|||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. automodule:: kasa.interfaces
|
.. automodule:: kasa.interfaces
|
||||||
:noindex:
|
|
||||||
:members:
|
:members:
|
||||||
:inherited-members:
|
:inherited-members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
@ -85,64 +81,29 @@
|
|||||||
|
|
||||||
## Protocols and transports
|
## Protocols and transports
|
||||||
|
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. autoclass:: kasa.protocols.BaseProtocol
|
.. automodule:: kasa.protocols
|
||||||
:members:
|
:members:
|
||||||
:inherited-members:
|
:imported-members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
:exclude-members: SmartErrorCode
|
||||||
|
:no-index:
|
||||||
```
|
```
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. autoclass:: kasa.protocols.IotProtocol
|
.. automodule:: kasa.transports
|
||||||
:members:
|
:members:
|
||||||
:inherited-members:
|
:imported-members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
:no-index:
|
||||||
```
|
```
|
||||||
|
|
||||||
```{eval-rst}
|
|
||||||
.. autoclass:: kasa.protocols.SmartProtocol
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
```
|
|
||||||
|
|
||||||
```{eval-rst}
|
|
||||||
.. autoclass:: kasa.transports.BaseTransport
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
```
|
|
||||||
|
|
||||||
```{eval-rst}
|
|
||||||
.. autoclass:: kasa.transports.XorTransport
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
```
|
|
||||||
|
|
||||||
```{eval-rst}
|
|
||||||
.. autoclass:: kasa.transports.KlapTransport
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
```
|
|
||||||
|
|
||||||
```{eval-rst}
|
|
||||||
.. autoclass:: kasa.transports.KlapTransportV2
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
```
|
|
||||||
|
|
||||||
```{eval-rst}
|
|
||||||
.. autoclass:: kasa.transports.AesTransport
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
```
|
|
||||||
|
|
||||||
## Errors and exceptions
|
## Errors and exceptions
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. autoclass:: kasa.exceptions.KasaException
|
.. autoclass:: kasa.exceptions.KasaException
|
||||||
:members:
|
:members:
|
||||||
@ -171,3 +132,4 @@
|
|||||||
.. autoclass:: kasa.exceptions.TimeoutError
|
.. autoclass:: kasa.exceptions.TimeoutError
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
```
|
||||||
|
@ -80,14 +80,17 @@ This can be done using the {attr}`~kasa.Device.internal_state` property.
|
|||||||
## Modules and Features
|
## Modules and Features
|
||||||
|
|
||||||
The functionality provided by all {class}`~kasa.Device` instances is (mostly) done inside separate modules.
|
The functionality provided by all {class}`~kasa.Device` instances is (mostly) done inside separate modules.
|
||||||
While the individual device-type specific classes provide an easy access for the most import features,
|
While the device class provides easy access for most device related attributes,
|
||||||
you can also access individual modules through {attr}`kasa.Device.modules`.
|
for components like `light` and `camera` you can access the module through {attr}`kasa.Device.modules`.
|
||||||
You can get the list of supported modules for a given device instance using {attr}`~kasa.Device.supported_modules`.
|
The module names are handily available as constants on {class}`~kasa.Module` and will return type aware values from the collection.
|
||||||
|
|
||||||
```{note}
|
Features represent individual pieces of functionality within a module like brightness, hsv and temperature within a light module.
|
||||||
If you only need some module-specific information,
|
They allow for instrospection and can be accessed through {attr}`kasa.Device.features`.
|
||||||
you can call the wanted method on the module to avoid using {meth}`~kasa.Device.update`.
|
Attributes can be accessed via a `Feature` or a module attribute depending on the use case.
|
||||||
```
|
Modules tend to provide richer functionality but using the features does not require an understanding of the module api.
|
||||||
|
|
||||||
|
:::{include} featureattributes.md
|
||||||
|
:::
|
||||||
|
|
||||||
(topics-protocols-and-transports)=
|
(topics-protocols-and-transports)=
|
||||||
## Protocols and Transports
|
## Protocols and Transports
|
||||||
@ -137,96 +140,3 @@ The base exception for all library errors is {class}`KasaException <kasa.excepti
|
|||||||
- If the library encounters and unsupported deviceit raises an {class}`UnsupportedDeviceError <kasa.exceptions.UnsupportedDeviceError>`.
|
- If the library encounters and unsupported deviceit raises an {class}`UnsupportedDeviceError <kasa.exceptions.UnsupportedDeviceError>`.
|
||||||
- If the device fails to respond within a timeout the library raises a {class}`TimeoutError <kasa.exceptions.TimeoutError>`.
|
- If the device fails to respond within a timeout the library raises a {class}`TimeoutError <kasa.exceptions.TimeoutError>`.
|
||||||
- All other failures will raise the base {class}`KasaException <kasa.exceptions.KasaException>` class.
|
- All other failures will raise the base {class}`KasaException <kasa.exceptions.KasaException>` class.
|
||||||
|
|
||||||
<!-- Commenting out this section keeps git seeing the change as a rename.
|
|
||||||
|
|
||||||
API documentation for modules and features
|
|
||||||
******************************************
|
|
||||||
|
|
||||||
.. autoclass:: kasa.Module
|
|
||||||
:noindex:
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. automodule:: kasa.interfaces
|
|
||||||
:noindex:
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.Feature
|
|
||||||
:noindex:
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
API documentation for protocols and transports
|
|
||||||
**********************************************
|
|
||||||
|
|
||||||
.. autoclass:: kasa.protocols.BaseProtocol
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.protocols.IotProtocol
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.protocols.SmartProtocol
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.transports.BaseTransport
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.transports.XorTransport
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.transports.KlapTransport
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.transports.KlapTransportV2
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.transports.AesTransport
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
API documentation for errors and exceptions
|
|
||||||
*******************************************
|
|
||||||
|
|
||||||
.. autoclass:: kasa.exceptions.KasaException
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.exceptions.DeviceError
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.exceptions.AuthenticationError
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.exceptions.UnsupportedDeviceError
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: kasa.exceptions.TimeoutError
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
@ -40,7 +40,7 @@ Different groups of functionality are supported by modules which you can access
|
|||||||
key from :class:`~kasa.Module`.
|
key from :class:`~kasa.Module`.
|
||||||
|
|
||||||
Modules will only be available on the device if they are supported but some individual features of a module may not be available for your device.
|
Modules will only be available on the device if they are supported but some individual features of a module may not be available for your device.
|
||||||
You can check the availability using ``is_``-prefixed properties like `is_color`.
|
You can check the availability using ``has_feature()`` method.
|
||||||
|
|
||||||
>>> from kasa import Module
|
>>> from kasa import Module
|
||||||
>>> Module.Light in dev.modules
|
>>> Module.Light in dev.modules
|
||||||
|
@ -38,7 +38,7 @@ from kasa.feature import Feature
|
|||||||
from kasa.interfaces.light import HSV, ColorTempRange, Light, LightState
|
from kasa.interfaces.light import HSV, ColorTempRange, Light, LightState
|
||||||
from kasa.interfaces.thermostat import Thermostat, ThermostatState
|
from kasa.interfaces.thermostat import Thermostat, ThermostatState
|
||||||
from kasa.module import Module
|
from kasa.module import Module
|
||||||
from kasa.protocols import BaseProtocol, IotProtocol, SmartProtocol
|
from kasa.protocols import BaseProtocol, IotProtocol, SmartCamProtocol, SmartProtocol
|
||||||
from kasa.protocols.iotprotocol import _deprecated_TPLinkSmartHomeProtocol # noqa: F401
|
from kasa.protocols.iotprotocol import _deprecated_TPLinkSmartHomeProtocol # noqa: F401
|
||||||
from kasa.smartcam.modules.camera import StreamResolution
|
from kasa.smartcam.modules.camera import StreamResolution
|
||||||
from kasa.transports import BaseTransport
|
from kasa.transports import BaseTransport
|
||||||
@ -52,6 +52,7 @@ __all__ = [
|
|||||||
"BaseTransport",
|
"BaseTransport",
|
||||||
"IotProtocol",
|
"IotProtocol",
|
||||||
"SmartProtocol",
|
"SmartProtocol",
|
||||||
|
"SmartCamProtocol",
|
||||||
"LightState",
|
"LightState",
|
||||||
"TurnOnBehaviors",
|
"TurnOnBehaviors",
|
||||||
"TurnOnBehavior",
|
"TurnOnBehavior",
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from functools import singledispatch, update_wrapper, wraps
|
from functools import singledispatch, update_wrapper, wraps
|
||||||
|
from gettext import gettext
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
import asyncclick as click
|
import asyncclick as click
|
||||||
@ -238,4 +240,19 @@ def CatchAllExceptions(cls):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_handle_exception(self._debug, exc)
|
_handle_exception(self._debug, exc)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""Run the coroutine in the event loop and print any exceptions.
|
||||||
|
|
||||||
|
python click catches KeyboardInterrupt in main, raises Abort()
|
||||||
|
and does sys.exit. asyncclick doesn't properly handle a coroutine
|
||||||
|
receiving CancelledError on a KeyboardInterrupt, so we catch the
|
||||||
|
KeyboardInterrupt here once asyncio.run has re-raised it. This
|
||||||
|
avoids large stacktraces when a user presses Ctrl-C.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
asyncio.run(self.main(*args, **kwargs))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
click.echo(gettext("\nAborted!"), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
return _CommandCls
|
return _CommandCls
|
||||||
|
@ -41,8 +41,14 @@ async def state(ctx, dev: Device):
|
|||||||
echo(f"Device state: {dev.is_on}")
|
echo(f"Device state: {dev.is_on}")
|
||||||
|
|
||||||
echo(f"Time: {dev.time} (tz: {dev.timezone})")
|
echo(f"Time: {dev.time} (tz: {dev.timezone})")
|
||||||
echo(f"Hardware: {dev.hw_info['hw_ver']}")
|
echo(
|
||||||
echo(f"Software: {dev.hw_info['sw_ver']}")
|
f"Hardware: {dev.device_info.hardware_version}"
|
||||||
|
f"{' (' + dev.region + ')' if dev.region else ''}"
|
||||||
|
)
|
||||||
|
echo(
|
||||||
|
f"Firmware: {dev.device_info.firmware_version}"
|
||||||
|
f" {dev.device_info.firmware_build}"
|
||||||
|
)
|
||||||
echo(f"MAC (rssi): {dev.mac} ({dev.rssi})")
|
echo(f"MAC (rssi): {dev.mac} ({dev.rssi})")
|
||||||
if verbose:
|
if verbose:
|
||||||
echo(f"Location: {dev.location}")
|
echo(f"Location: {dev.location}")
|
||||||
|
@ -123,14 +123,19 @@ async def list(ctx):
|
|||||||
async def print_discovered(dev: Device):
|
async def print_discovered(dev: Device):
|
||||||
cparams = dev.config.connection_type
|
cparams = dev.config.connection_type
|
||||||
infostr = (
|
infostr = (
|
||||||
f"{dev.host:<15} {cparams.device_family.value:<20} "
|
f"{dev.host:<15} {dev.model:<9} {cparams.device_family.value:<20} "
|
||||||
f"{cparams.encryption_type.value:<7}"
|
f"{cparams.encryption_type.value:<7} {cparams.https:<5} "
|
||||||
|
f"{cparams.login_version or '-':<3}"
|
||||||
)
|
)
|
||||||
async with sem:
|
async with sem:
|
||||||
try:
|
try:
|
||||||
await dev.update()
|
await dev.update()
|
||||||
except AuthenticationError:
|
except AuthenticationError:
|
||||||
echo(f"{infostr} - Authentication failed")
|
echo(f"{infostr} - Authentication failed")
|
||||||
|
except TimeoutError:
|
||||||
|
echo(f"{infostr} - Timed out")
|
||||||
|
except Exception as ex:
|
||||||
|
echo(f"{infostr} - Error: {ex}")
|
||||||
else:
|
else:
|
||||||
echo(f"{infostr} {dev.alias}")
|
echo(f"{infostr} {dev.alias}")
|
||||||
|
|
||||||
@ -138,7 +143,10 @@ async def list(ctx):
|
|||||||
if host := unsupported_exception.host:
|
if host := unsupported_exception.host:
|
||||||
echo(f"{host:<15} UNSUPPORTED DEVICE")
|
echo(f"{host:<15} UNSUPPORTED DEVICE")
|
||||||
|
|
||||||
echo(f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}")
|
echo(
|
||||||
|
f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
|
||||||
|
f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
|
||||||
|
)
|
||||||
return await _discover(
|
return await _discover(
|
||||||
ctx,
|
ctx,
|
||||||
print_discovered=print_discovered,
|
print_discovered=print_discovered,
|
||||||
|
@ -25,7 +25,9 @@ def light(dev) -> None:
|
|||||||
@pass_dev_or_child
|
@pass_dev_or_child
|
||||||
async def brightness(dev: Device, brightness: int, transition: int):
|
async def brightness(dev: Device, brightness: int, transition: int):
|
||||||
"""Get or set brightness."""
|
"""Get or set brightness."""
|
||||||
if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
|
if not (light := dev.modules.get(Module.Light)) or not light.has_feature(
|
||||||
|
"brightness"
|
||||||
|
):
|
||||||
error("This device does not support brightness.")
|
error("This device does not support brightness.")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -45,13 +47,15 @@ async def brightness(dev: Device, brightness: int, transition: int):
|
|||||||
@pass_dev_or_child
|
@pass_dev_or_child
|
||||||
async def temperature(dev: Device, temperature: int, transition: int):
|
async def temperature(dev: Device, temperature: int, transition: int):
|
||||||
"""Get or set color temperature."""
|
"""Get or set color temperature."""
|
||||||
if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
|
if not (light := dev.modules.get(Module.Light)) or not (
|
||||||
|
color_temp_feat := light.get_feature("color_temp")
|
||||||
|
):
|
||||||
error("Device does not support color temperature")
|
error("Device does not support color temperature")
|
||||||
return
|
return
|
||||||
|
|
||||||
if temperature is None:
|
if temperature is None:
|
||||||
echo(f"Color temperature: {light.color_temp}")
|
echo(f"Color temperature: {light.color_temp}")
|
||||||
valid_temperature_range = light.valid_temperature_range
|
valid_temperature_range = color_temp_feat.range
|
||||||
if valid_temperature_range != (0, 0):
|
if valid_temperature_range != (0, 0):
|
||||||
echo("(min: {}, max: {})".format(*valid_temperature_range))
|
echo("(min: {}, max: {})".format(*valid_temperature_range))
|
||||||
else:
|
else:
|
||||||
@ -59,7 +63,7 @@ async def temperature(dev: Device, temperature: int, transition: int):
|
|||||||
"Temperature range unknown, please open a github issue"
|
"Temperature range unknown, please open a github issue"
|
||||||
f" or a pull request for model '{dev.model}'"
|
f" or a pull request for model '{dev.model}'"
|
||||||
)
|
)
|
||||||
return light.valid_temperature_range
|
return color_temp_feat.range
|
||||||
else:
|
else:
|
||||||
echo(f"Setting color temperature to {temperature}")
|
echo(f"Setting color temperature to {temperature}")
|
||||||
return await light.set_color_temp(temperature, transition=transition)
|
return await light.set_color_temp(temperature, transition=transition)
|
||||||
@ -99,7 +103,7 @@ async def effect(dev: Device, ctx, effect):
|
|||||||
@pass_dev_or_child
|
@pass_dev_or_child
|
||||||
async def hsv(dev: Device, ctx, h, s, v, transition):
|
async def hsv(dev: Device, ctx, h, s, v, transition):
|
||||||
"""Get or set color in HSV."""
|
"""Get or set color in HSV."""
|
||||||
if not (light := dev.modules.get(Module.Light)) or not light.is_color:
|
if not (light := dev.modules.get(Module.Light)) or not light.has_feature("hsv"):
|
||||||
error("Device does not support colors")
|
error("Device does not support colors")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ All devices provide several informational properties:
|
|||||||
>>> dev.alias
|
>>> dev.alias
|
||||||
Bedroom Lamp Plug
|
Bedroom Lamp Plug
|
||||||
>>> dev.model
|
>>> dev.model
|
||||||
HS110(EU)
|
HS110
|
||||||
>>> dev.rssi
|
>>> dev.rssi
|
||||||
-71
|
-71
|
||||||
>>> dev.mac
|
>>> dev.mac
|
||||||
@ -151,7 +151,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _DeviceInfo:
|
class DeviceInfo:
|
||||||
"""Device Model Information."""
|
"""Device Model Information."""
|
||||||
|
|
||||||
short_name: str
|
short_name: str
|
||||||
@ -208,7 +208,7 @@ class Device(ABC):
|
|||||||
self.protocol: BaseProtocol = protocol or IotProtocol(
|
self.protocol: BaseProtocol = protocol or IotProtocol(
|
||||||
transport=XorTransport(config=config or DeviceConfig(host=host)),
|
transport=XorTransport(config=config or DeviceConfig(host=host)),
|
||||||
)
|
)
|
||||||
self._last_update: Any = None
|
self._last_update: dict[str, Any] = {}
|
||||||
_LOGGER.debug("Initializing %s of type %s", host, type(self))
|
_LOGGER.debug("Initializing %s of type %s", host, type(self))
|
||||||
self._device_type = DeviceType.Unknown
|
self._device_type = DeviceType.Unknown
|
||||||
# TODO: typing Any is just as using dict | None would require separate
|
# TODO: typing Any is just as using dict | None would require separate
|
||||||
@ -334,9 +334,21 @@ class Device(ABC):
|
|||||||
"""Returns the device model."""
|
"""Returns the device model."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
def region(self) -> str | None:
|
||||||
|
"""Returns the device region."""
|
||||||
|
return self.device_info.region
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device info."""
|
||||||
|
return self._get_device_info(self._last_update, self._discovery_info)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _model_region(self) -> str:
|
def _get_device_info(
|
||||||
"""Return device full model name and region."""
|
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
||||||
|
) -> DeviceInfo:
|
||||||
|
"""Get device info."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -8,7 +8,7 @@ from typing import Any
|
|||||||
|
|
||||||
from .device import Device
|
from .device import Device
|
||||||
from .device_type import DeviceType
|
from .device_type import DeviceType
|
||||||
from .deviceconfig import DeviceConfig
|
from .deviceconfig import DeviceConfig, DeviceFamily
|
||||||
from .exceptions import KasaException, UnsupportedDeviceError
|
from .exceptions import KasaException, UnsupportedDeviceError
|
||||||
from .iot import (
|
from .iot import (
|
||||||
IotBulb,
|
IotBulb,
|
||||||
@ -179,20 +179,29 @@ def get_device_class_from_family(
|
|||||||
def get_protocol(
|
def get_protocol(
|
||||||
config: DeviceConfig,
|
config: DeviceConfig,
|
||||||
) -> BaseProtocol | None:
|
) -> BaseProtocol | None:
|
||||||
"""Return the protocol from the connection name."""
|
"""Return the protocol from the connection name.
|
||||||
protocol_name = config.connection_type.device_family.value.split(".")[0]
|
|
||||||
|
For cameras and vacuums the device family is a simple mapping to
|
||||||
|
the protocol/transport. For other device types the transport varies
|
||||||
|
based on the discovery information.
|
||||||
|
"""
|
||||||
ctype = config.connection_type
|
ctype = config.connection_type
|
||||||
|
protocol_name = ctype.device_family.value.split(".")[0]
|
||||||
|
|
||||||
|
if ctype.device_family is DeviceFamily.SmartIpCamera:
|
||||||
|
return SmartCamProtocol(transport=SslAesTransport(config=config))
|
||||||
|
|
||||||
|
if ctype.device_family is DeviceFamily.IotIpCamera:
|
||||||
|
return IotProtocol(transport=LinkieTransportV2(config=config))
|
||||||
|
|
||||||
|
if ctype.device_family is DeviceFamily.SmartTapoRobovac:
|
||||||
|
return SmartProtocol(transport=SslTransport(config=config))
|
||||||
|
|
||||||
protocol_transport_key = (
|
protocol_transport_key = (
|
||||||
protocol_name
|
protocol_name
|
||||||
+ "."
|
+ "."
|
||||||
+ ctype.encryption_type.value
|
+ ctype.encryption_type.value
|
||||||
+ (".HTTPS" if ctype.https else "")
|
+ (".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)
|
_LOGGER.debug("Finding transport for %s", protocol_transport_key)
|
||||||
@ -201,12 +210,11 @@ def get_protocol(
|
|||||||
] = {
|
] = {
|
||||||
"IOT.XOR": (IotProtocol, XorTransport),
|
"IOT.XOR": (IotProtocol, XorTransport),
|
||||||
"IOT.KLAP": (IotProtocol, KlapTransport),
|
"IOT.KLAP": (IotProtocol, KlapTransport),
|
||||||
"IOT.XOR.HTTPS.2": (IotProtocol, LinkieTransportV2),
|
|
||||||
"SMART.AES": (SmartProtocol, AesTransport),
|
"SMART.AES": (SmartProtocol, AesTransport),
|
||||||
"SMART.AES.2": (SmartProtocol, AesTransport),
|
"SMART.KLAP": (SmartProtocol, KlapTransportV2),
|
||||||
"SMART.KLAP.2": (SmartProtocol, KlapTransportV2),
|
# H200 is device family SMART.TAPOHUB and uses SmartCamProtocol so use
|
||||||
"SMART.AES.HTTPS.2": (SmartCamProtocol, SslAesTransport),
|
# https to distuingish from SmartProtocol devices
|
||||||
"SMART.AES.HTTPS": (SmartProtocol, SslTransport),
|
"SMART.AES.HTTPS": (SmartCamProtocol, SslAesTransport),
|
||||||
}
|
}
|
||||||
if not (prot_tran_cls := supported_device_protocols.get(protocol_transport_key)):
|
if not (prot_tran_cls := supported_device_protocols.get(protocol_transport_key)):
|
||||||
return None
|
return None
|
||||||
|
@ -22,7 +22,7 @@ Discovery returns a dict of {ip: discovered devices}:
|
|||||||
>>>
|
>>>
|
||||||
>>> found_devices = await Discover.discover()
|
>>> found_devices = await Discover.discover()
|
||||||
>>> [dev.model for dev in found_devices.values()]
|
>>> [dev.model for dev in found_devices.values()]
|
||||||
['KP303(UK)', 'HS110(EU)', 'L530E', 'KL430(US)', 'HS220(US)']
|
['KP303', 'HS110', 'L530E', 'KL430', 'HS220']
|
||||||
|
|
||||||
You can pass username and password for devices requiring authentication
|
You can pass username and password for devices requiring authentication
|
||||||
|
|
||||||
@ -65,17 +65,17 @@ It is also possible to pass a coroutine to be executed for each found device:
|
|||||||
>>> print(f"Discovered {dev.alias} (model: {dev.model})")
|
>>> print(f"Discovered {dev.alias} (model: {dev.model})")
|
||||||
>>>
|
>>>
|
||||||
>>> devices = await Discover.discover(on_discovered=print_dev_info, credentials=creds)
|
>>> devices = await Discover.discover(on_discovered=print_dev_info, credentials=creds)
|
||||||
Discovered Bedroom Power Strip (model: KP303(UK))
|
Discovered Bedroom Power Strip (model: KP303)
|
||||||
Discovered Bedroom Lamp Plug (model: HS110(EU))
|
Discovered Bedroom Lamp Plug (model: HS110)
|
||||||
Discovered Living Room Bulb (model: L530)
|
Discovered Living Room Bulb (model: L530)
|
||||||
Discovered Bedroom Lightstrip (model: KL430(US))
|
Discovered Bedroom Lightstrip (model: KL430)
|
||||||
Discovered Living Room Dimmer Switch (model: HS220(US))
|
Discovered Living Room Dimmer Switch (model: HS220)
|
||||||
|
|
||||||
Discovering a single device returns a kasa.Device object.
|
Discovering a single device returns a kasa.Device object.
|
||||||
|
|
||||||
>>> device = await Discover.discover_single("127.0.0.1", credentials=creds)
|
>>> device = await Discover.discover_single("127.0.0.1", credentials=creds)
|
||||||
>>> device.model
|
>>> device.model
|
||||||
'KP303(UK)'
|
'KP303'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -168,6 +168,12 @@ OnUnsupportedCallable = Callable[[UnsupportedDeviceError], Coroutine]
|
|||||||
OnConnectAttemptCallable = Callable[[ConnectAttempt, bool], None]
|
OnConnectAttemptCallable = Callable[[ConnectAttempt, bool], None]
|
||||||
DeviceDict = dict[str, Device]
|
DeviceDict = dict[str, Device]
|
||||||
|
|
||||||
|
DECRYPTED_REDACTORS: dict[str, Callable[[Any], Any] | None] = {
|
||||||
|
"connect_ssid": lambda x: "#MASKED_SSID#" if x else "",
|
||||||
|
"device_id": lambda x: "REDACTED_" + x[9::],
|
||||||
|
"owner": lambda x: "REDACTED_" + x[9::],
|
||||||
|
}
|
||||||
|
|
||||||
NEW_DISCOVERY_REDACTORS: dict[str, Callable[[Any], Any] | None] = {
|
NEW_DISCOVERY_REDACTORS: dict[str, Callable[[Any], Any] | None] = {
|
||||||
"device_id": lambda x: "REDACTED_" + x[9::],
|
"device_id": lambda x: "REDACTED_" + x[9::],
|
||||||
"device_name": lambda x: "#MASKED_NAME#" if x else "",
|
"device_name": lambda x: "#MASKED_NAME#" if x else "",
|
||||||
@ -177,6 +183,8 @@ NEW_DISCOVERY_REDACTORS: dict[str, Callable[[Any], Any] | None] = {
|
|||||||
"group_id": lambda x: "REDACTED_" + x[9::],
|
"group_id": lambda x: "REDACTED_" + x[9::],
|
||||||
"group_name": lambda x: "I01BU0tFRF9TU0lEIw==",
|
"group_name": lambda x: "I01BU0tFRF9TU0lEIw==",
|
||||||
"encrypt_info": lambda x: {**x, "key": "", "data": ""},
|
"encrypt_info": lambda x: {**x, "key": "", "data": ""},
|
||||||
|
"ip": lambda x: x, # don't redact but keep listed here for dump_devinfo
|
||||||
|
"decrypted_data": lambda x: redact_data(x, DECRYPTED_REDACTORS),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -490,7 +498,7 @@ class Discover:
|
|||||||
try:
|
try:
|
||||||
_LOGGER.debug("Waiting %s seconds for responses...", discovery_timeout)
|
_LOGGER.debug("Waiting %s seconds for responses...", discovery_timeout)
|
||||||
await protocol.wait_for_discovery_to_complete()
|
await protocol.wait_for_discovery_to_complete()
|
||||||
except KasaException as ex:
|
except (KasaException, asyncio.CancelledError) as ex:
|
||||||
for device in protocol.discovered_devices.values():
|
for device in protocol.discovered_devices.values():
|
||||||
await device.protocol.close()
|
await device.protocol.close()
|
||||||
raise ex
|
raise ex
|
||||||
@ -742,6 +750,7 @@ class Discover:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _decrypt_discovery_data(discovery_result: DiscoveryResult) -> None:
|
def _decrypt_discovery_data(discovery_result: DiscoveryResult) -> None:
|
||||||
|
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert discovery_result.encrypt_info
|
assert discovery_result.encrypt_info
|
||||||
assert _AesDiscoveryQuery.keypair
|
assert _AesDiscoveryQuery.keypair
|
||||||
@ -757,7 +766,19 @@ class Discover:
|
|||||||
session = AesEncyptionSession(key, iv)
|
session = AesEncyptionSession(key, iv)
|
||||||
decrypted_data = session.decrypt(encrypted_data)
|
decrypted_data = session.decrypt(encrypted_data)
|
||||||
|
|
||||||
discovery_result.decrypted_data = json_loads(decrypted_data)
|
result = json_loads(decrypted_data)
|
||||||
|
if debug_enabled:
|
||||||
|
data = (
|
||||||
|
redact_data(result, DECRYPTED_REDACTORS)
|
||||||
|
if Discover._redact_data
|
||||||
|
else result
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Decrypted encrypt_info for %s: %s",
|
||||||
|
discovery_result.ip,
|
||||||
|
pf(data),
|
||||||
|
)
|
||||||
|
discovery_result.decrypted_data = result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_discovery_json(data: bytes, ip: str) -> dict:
|
def _get_discovery_json(data: bytes, ip: str) -> dict:
|
||||||
@ -826,12 +847,12 @@ class Discover:
|
|||||||
):
|
):
|
||||||
encrypt_type = encrypt_info.sym_schm
|
encrypt_type = encrypt_info.sym_schm
|
||||||
|
|
||||||
if (
|
if not (login_version := encrypt_schm.lv) and (
|
||||||
not (login_version := encrypt_schm.lv)
|
et := discovery_result.encrypt_type
|
||||||
and (et := discovery_result.encrypt_type)
|
|
||||||
and et == ["3"]
|
|
||||||
):
|
):
|
||||||
login_version = 2
|
# Known encrypt types are ["1","2"] and ["3"]
|
||||||
|
# Reuse the login_version attribute to pass the max to transport
|
||||||
|
login_version = max([int(i) for i in et])
|
||||||
|
|
||||||
if not encrypt_type:
|
if not encrypt_type:
|
||||||
raise UnsupportedDeviceError(
|
raise UnsupportedDeviceError(
|
||||||
|
@ -113,10 +113,23 @@ class HttpClient:
|
|||||||
ssl=ssl,
|
ssl=ssl,
|
||||||
)
|
)
|
||||||
async with resp:
|
async with resp:
|
||||||
if resp.status == 200:
|
response_data = await resp.read()
|
||||||
response_data = await resp.read()
|
|
||||||
if return_json:
|
if resp.status == 200:
|
||||||
|
if return_json:
|
||||||
|
response_data = json_loads(response_data.decode())
|
||||||
|
else:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Device %s received status code %s with response %s",
|
||||||
|
self._config.host,
|
||||||
|
resp.status,
|
||||||
|
str(response_data),
|
||||||
|
)
|
||||||
|
if response_data and return_json:
|
||||||
|
try:
|
||||||
response_data = json_loads(response_data.decode())
|
response_data = json_loads(response_data.decode())
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.debug("Device %s response could not be parsed as json")
|
||||||
|
|
||||||
except (aiohttp.ServerDisconnectedError, aiohttp.ClientOSError) as ex:
|
except (aiohttp.ServerDisconnectedError, aiohttp.ClientOSError) as ex:
|
||||||
if not self._wait_between_requests:
|
if not self._wait_between_requests:
|
||||||
|
@ -23,7 +23,7 @@ Get the light module to interact:
|
|||||||
|
|
||||||
>>> light = dev.modules[Module.Light]
|
>>> light = dev.modules[Module.Light]
|
||||||
|
|
||||||
You can use the ``is_``-prefixed properties to check for supported features:
|
You can use the ``has_feature()`` method to check for supported features:
|
||||||
|
|
||||||
>>> light.has_feature("brightness")
|
>>> light.has_feature("brightness")
|
||||||
True
|
True
|
||||||
|
@ -22,7 +22,7 @@ from datetime import datetime, timedelta, tzinfo
|
|||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from ..device import Device, WifiNetwork, _DeviceInfo
|
from ..device import Device, DeviceInfo, WifiNetwork
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..exceptions import KasaException
|
from ..exceptions import KasaException
|
||||||
@ -43,7 +43,7 @@ def requires_update(f: Callable) -> Any:
|
|||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
async def wrapped(*args: Any, **kwargs: Any) -> Any:
|
async def wrapped(*args: Any, **kwargs: Any) -> Any:
|
||||||
self = args[0]
|
self = args[0]
|
||||||
if self._last_update is None and (
|
if not self._last_update and (
|
||||||
self._sys_info is None or f.__name__ not in self._sys_info
|
self._sys_info is None or f.__name__ not in self._sys_info
|
||||||
):
|
):
|
||||||
raise KasaException("You need to await update() to access the data")
|
raise KasaException("You need to await update() to access the data")
|
||||||
@ -54,7 +54,7 @@ def requires_update(f: Callable) -> Any:
|
|||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
||||||
self = args[0]
|
self = args[0]
|
||||||
if self._last_update is None and (
|
if not self._last_update and (
|
||||||
self._sys_info is None or f.__name__ not in self._sys_info
|
self._sys_info is None or f.__name__ not in self._sys_info
|
||||||
):
|
):
|
||||||
raise KasaException("You need to await update() to access the data")
|
raise KasaException("You need to await update() to access the data")
|
||||||
@ -112,7 +112,7 @@ class IotDevice(Device):
|
|||||||
>>> dev.alias
|
>>> dev.alias
|
||||||
Bedroom Lamp Plug
|
Bedroom Lamp Plug
|
||||||
>>> dev.model
|
>>> dev.model
|
||||||
HS110(EU)
|
HS110
|
||||||
>>> dev.rssi
|
>>> dev.rssi
|
||||||
-71
|
-71
|
||||||
>>> dev.mac
|
>>> dev.mac
|
||||||
@ -310,7 +310,7 @@ class IotDevice(Device):
|
|||||||
# If this is the initial update, check only for the sysinfo
|
# If this is the initial update, check only for the sysinfo
|
||||||
# This is necessary as some devices crash on unexpected modules
|
# This is necessary as some devices crash on unexpected modules
|
||||||
# See #105, #120, #161
|
# See #105, #120, #161
|
||||||
if self._last_update is None:
|
if not self._last_update:
|
||||||
_LOGGER.debug("Performing the initial update to obtain sysinfo")
|
_LOGGER.debug("Performing the initial update to obtain sysinfo")
|
||||||
response = await self.protocol.query(req)
|
response = await self.protocol.query(req)
|
||||||
self._last_update = response
|
self._last_update = response
|
||||||
@ -452,7 +452,9 @@ class IotDevice(Device):
|
|||||||
# This allows setting of some info properties directly
|
# This allows setting of some info properties directly
|
||||||
# from partial discovery info that will then be found
|
# from partial discovery info that will then be found
|
||||||
# by the requires_update decorator
|
# by the requires_update decorator
|
||||||
self._set_sys_info(info)
|
discovery_model = info["device_model"]
|
||||||
|
no_region_model, _, _ = discovery_model.partition("(")
|
||||||
|
self._set_sys_info({**info, "model": no_region_model})
|
||||||
|
|
||||||
def _set_sys_info(self, sys_info: dict[str, Any]) -> None:
|
def _set_sys_info(self, sys_info: dict[str, Any]) -> None:
|
||||||
"""Set sys_info."""
|
"""Set sys_info."""
|
||||||
@ -471,18 +473,13 @@ class IotDevice(Device):
|
|||||||
"""
|
"""
|
||||||
return self._sys_info # type: ignore
|
return self._sys_info # type: ignore
|
||||||
|
|
||||||
@property # type: ignore
|
|
||||||
@requires_update
|
|
||||||
def model(self) -> str:
|
|
||||||
"""Return device model."""
|
|
||||||
sys_info = self._sys_info
|
|
||||||
return str(sys_info["model"])
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requires_update
|
@requires_update
|
||||||
def _model_region(self) -> str:
|
def model(self) -> str:
|
||||||
"""Return device full model name and region."""
|
"""Returns the device model."""
|
||||||
return self.model
|
if self._last_update:
|
||||||
|
return self.device_info.short_name
|
||||||
|
return self._sys_info["model"]
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
def alias(self) -> str | None:
|
def alias(self) -> str | None:
|
||||||
@ -748,7 +745,7 @@ class IotDevice(Device):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_device_info(
|
def _get_device_info(
|
||||||
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
||||||
) -> _DeviceInfo:
|
) -> DeviceInfo:
|
||||||
"""Get model information for a device."""
|
"""Get model information for a device."""
|
||||||
sys_info = _extract_sys_info(info)
|
sys_info = _extract_sys_info(info)
|
||||||
|
|
||||||
@ -766,7 +763,7 @@ class IotDevice(Device):
|
|||||||
firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
|
firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
|
||||||
auth = bool(discovery_info and ("mgt_encrypt_schm" in discovery_info))
|
auth = bool(discovery_info and ("mgt_encrypt_schm" in discovery_info))
|
||||||
|
|
||||||
return _DeviceInfo(
|
return DeviceInfo(
|
||||||
short_name=long_name,
|
short_name=long_name,
|
||||||
long_name=long_name,
|
long_name=long_name,
|
||||||
brand="kasa",
|
brand="kasa",
|
||||||
|
@ -207,17 +207,18 @@ class Light(IotModule, LightInterface):
|
|||||||
return self._light_state
|
return self._light_state
|
||||||
|
|
||||||
async def _post_update_hook(self) -> None:
|
async def _post_update_hook(self) -> None:
|
||||||
if self._device.is_on is False:
|
device = self._device
|
||||||
|
if device.is_on is False:
|
||||||
state = LightState(light_on=False)
|
state = LightState(light_on=False)
|
||||||
else:
|
else:
|
||||||
state = LightState(light_on=True)
|
state = LightState(light_on=True)
|
||||||
if self._device._is_dimmable:
|
if device._is_dimmable:
|
||||||
state.brightness = self.brightness
|
state.brightness = self.brightness
|
||||||
if self._device._is_color:
|
if device._is_color:
|
||||||
hsv = self.hsv
|
hsv = self.hsv
|
||||||
state.hue = hsv.hue
|
state.hue = hsv.hue
|
||||||
state.saturation = hsv.saturation
|
state.saturation = hsv.saturation
|
||||||
if self._device._is_variable_color_temp:
|
if device._is_variable_color_temp:
|
||||||
state.color_temp = self.color_temp
|
state.color_temp = self.color_temp
|
||||||
self._light_state = state
|
self._light_state = state
|
||||||
|
|
||||||
|
@ -85,17 +85,19 @@ class LightPreset(IotModule, LightPresetInterface):
|
|||||||
def preset(self) -> str:
|
def preset(self) -> str:
|
||||||
"""Return current preset name."""
|
"""Return current preset name."""
|
||||||
light = self._device.modules[Module.Light]
|
light = self._device.modules[Module.Light]
|
||||||
|
is_color = light.has_feature("hsv")
|
||||||
|
is_variable_color_temp = light.has_feature("color_temp")
|
||||||
|
|
||||||
brightness = light.brightness
|
brightness = light.brightness
|
||||||
color_temp = light.color_temp if light.is_variable_color_temp else None
|
color_temp = light.color_temp if is_variable_color_temp else None
|
||||||
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
|
|
||||||
|
h, s = (light.hsv.hue, light.hsv.saturation) if is_color else (None, None)
|
||||||
for preset_name, preset in self._presets.items():
|
for preset_name, preset in self._presets.items():
|
||||||
if (
|
if (
|
||||||
preset.brightness == brightness
|
preset.brightness == brightness
|
||||||
and (
|
and (preset.color_temp == color_temp or not is_variable_color_temp)
|
||||||
preset.color_temp == color_temp or not light.is_variable_color_temp
|
and (preset.hue == h or not is_color)
|
||||||
)
|
and (preset.saturation == s or not is_color)
|
||||||
and (preset.hue == h or not light.is_color)
|
|
||||||
and (preset.saturation == s or not light.is_color)
|
|
||||||
):
|
):
|
||||||
return preset_name
|
return preset_name
|
||||||
return self.PRESET_NOT_SET
|
return self.PRESET_NOT_SET
|
||||||
@ -107,7 +109,7 @@ class LightPreset(IotModule, LightPresetInterface):
|
|||||||
"""Set a light preset for the device."""
|
"""Set a light preset for the device."""
|
||||||
light = self._device.modules[Module.Light]
|
light = self._device.modules[Module.Light]
|
||||||
if preset_name == self.PRESET_NOT_SET:
|
if preset_name == self.PRESET_NOT_SET:
|
||||||
if light.is_color:
|
if light.has_feature("hsv"):
|
||||||
preset = LightState(hue=0, saturation=0, brightness=100)
|
preset = LightState(hue=0, saturation=0, brightness=100)
|
||||||
else:
|
else:
|
||||||
preset = LightState(brightness=100)
|
preset = LightState(brightness=100)
|
||||||
|
@ -21,6 +21,9 @@ check for the existence of the module:
|
|||||||
>>> print(light.brightness)
|
>>> print(light.brightness)
|
||||||
100
|
100
|
||||||
|
|
||||||
|
.. include:: ../featureattributes.md
|
||||||
|
:parser: myst_parser.sphinx_
|
||||||
|
|
||||||
To see whether a device supports specific functionality, you can check whether the
|
To see whether a device supports specific functionality, you can check whether the
|
||||||
module has that feature:
|
module has that feature:
|
||||||
|
|
||||||
@ -151,8 +154,12 @@ class Module(ABC):
|
|||||||
)
|
)
|
||||||
TriggerLogs: Final[ModuleName[smart.TriggerLogs]] = ModuleName("TriggerLogs")
|
TriggerLogs: Final[ModuleName[smart.TriggerLogs]] = ModuleName("TriggerLogs")
|
||||||
|
|
||||||
|
HomeKit: Final[ModuleName[smart.HomeKit]] = ModuleName("HomeKit")
|
||||||
|
Matter: Final[ModuleName[smart.Matter]] = ModuleName("Matter")
|
||||||
|
|
||||||
# SMARTCAM only modules
|
# SMARTCAM only modules
|
||||||
Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera")
|
Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera")
|
||||||
|
LensMask: Final[ModuleName[smartcam.LensMask]] = ModuleName("LensMask")
|
||||||
|
|
||||||
def __init__(self, device: Device, module: str) -> None:
|
def __init__(self, device: Device, module: str) -> None:
|
||||||
self._device = device
|
self._device = device
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from .iotprotocol import IotProtocol
|
from .iotprotocol import IotProtocol
|
||||||
from .protocol import BaseProtocol
|
from .protocol import BaseProtocol
|
||||||
|
from .smartcamprotocol import SmartCamProtocol
|
||||||
from .smartprotocol import SmartErrorCode, SmartProtocol
|
from .smartprotocol import SmartErrorCode, SmartProtocol
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -9,4 +10,5 @@ __all__ = [
|
|||||||
"IotProtocol",
|
"IotProtocol",
|
||||||
"SmartErrorCode",
|
"SmartErrorCode",
|
||||||
"SmartProtocol",
|
"SmartProtocol",
|
||||||
|
"SmartCamProtocol",
|
||||||
]
|
]
|
||||||
|
@ -25,19 +25,35 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_children(children: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
def mask_child(child: dict[str, Any], index: int) -> dict[str, Any]:
|
||||||
|
result = {
|
||||||
|
**child,
|
||||||
|
"id": f"SCRUBBED_CHILD_DEVICE_ID_{index+1}",
|
||||||
|
}
|
||||||
|
# Will leave empty aliases as blank
|
||||||
|
if child.get("alias"):
|
||||||
|
result["alias"] = f"#MASKED_NAME# {index + 1}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
return [mask_child(child, index) for index, child in enumerate(children)]
|
||||||
|
|
||||||
|
|
||||||
REDACTORS: dict[str, Callable[[Any], Any] | None] = {
|
REDACTORS: dict[str, Callable[[Any], Any] | None] = {
|
||||||
"latitude": lambda x: 0,
|
"latitude": lambda x: 0,
|
||||||
"longitude": lambda x: 0,
|
"longitude": lambda x: 0,
|
||||||
"latitude_i": lambda x: 0,
|
"latitude_i": lambda x: 0,
|
||||||
"longitude_i": lambda x: 0,
|
"longitude_i": lambda x: 0,
|
||||||
"deviceId": lambda x: "REDACTED_" + x[9::],
|
"deviceId": lambda x: "REDACTED_" + x[9::],
|
||||||
"id": lambda x: "REDACTED_" + x[9::],
|
"children": _mask_children,
|
||||||
"alias": lambda x: "#MASKED_NAME#" if x else "",
|
"alias": lambda x: "#MASKED_NAME#" if x else "",
|
||||||
"mac": mask_mac,
|
"mac": mask_mac,
|
||||||
"mic_mac": mask_mac,
|
"mic_mac": mask_mac,
|
||||||
"ssid": lambda x: "#MASKED_SSID#" if x else "",
|
"ssid": lambda x: "#MASKED_SSID#" if x else "",
|
||||||
"oemId": lambda x: "REDACTED_" + x[9::],
|
"oemId": lambda x: "REDACTED_" + x[9::],
|
||||||
"username": lambda _: "user@example.com", # cnCloud
|
"username": lambda _: "user@example.com", # cnCloud
|
||||||
|
"hwId": lambda x: "REDACTED_" + x[9::],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,6 +66,8 @@ def redact_data(data: _T, redactors: dict[str, Callable[[Any], Any] | None]) ->
|
|||||||
|
|
||||||
def mask_mac(mac: str) -> str:
|
def mask_mac(mac: str) -> str:
|
||||||
"""Return mac address with last two octects blanked."""
|
"""Return mac address with last two octects blanked."""
|
||||||
|
if len(mac) == 12:
|
||||||
|
return f"{mac[:6]}000000"
|
||||||
delim = ":" if ":" in mac else "-"
|
delim = ":" if ":" in mac else "-"
|
||||||
rest = delim.join(format(s, "02x") for s in bytes.fromhex("000000"))
|
rest = delim.join(format(s, "02x") for s in bytes.fromhex("000000"))
|
||||||
return f"{mac[:8]}{delim}{rest}"
|
return f"{mac[:8]}{delim}{rest}"
|
||||||
|
@ -19,7 +19,7 @@ from ..transports.sslaestransport import (
|
|||||||
SMART_RETRYABLE_ERRORS,
|
SMART_RETRYABLE_ERRORS,
|
||||||
SmartErrorCode,
|
SmartErrorCode,
|
||||||
)
|
)
|
||||||
from . import SmartProtocol
|
from .smartprotocol import SmartProtocol
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@ -45,15 +46,36 @@ REDACTORS: dict[str, Callable[[Any], Any] | None] = {
|
|||||||
"original_device_id": lambda x: "REDACTED_" + x[9::], # Strip children
|
"original_device_id": lambda x: "REDACTED_" + x[9::], # Strip children
|
||||||
"nickname": lambda x: "I01BU0tFRF9OQU1FIw==" if x else "",
|
"nickname": lambda x: "I01BU0tFRF9OQU1FIw==" if x else "",
|
||||||
"mac": mask_mac,
|
"mac": mask_mac,
|
||||||
"ssid": lambda x: "I01BU0tFRF9TU0lEIw=" if x else "",
|
"ssid": lambda x: "I01BU0tFRF9TU0lEIw==" if x else "",
|
||||||
"bssid": lambda _: "000000000000",
|
"bssid": lambda _: "000000000000",
|
||||||
|
"channel": lambda _: 0,
|
||||||
"oem_id": lambda x: "REDACTED_" + x[9::],
|
"oem_id": lambda x: "REDACTED_" + x[9::],
|
||||||
"setup_code": None, # matter
|
"hw_id": lambda x: "REDACTED_" + x[9::],
|
||||||
"setup_payload": None, # matter
|
"fw_id": lambda x: "REDACTED_" + x[9::],
|
||||||
"mfi_setup_code": None, # mfi_ for homekit
|
"setup_code": lambda x: re.sub(r"\w", "0", x), # matter
|
||||||
"mfi_setup_id": None,
|
"setup_payload": lambda x: re.sub(r"\w", "0", x), # matter
|
||||||
"mfi_token_token": None,
|
"mfi_setup_code": lambda x: re.sub(r"\w", "0", x), # mfi_ for homekit
|
||||||
"mfi_token_uuid": None,
|
"mfi_setup_id": lambda x: re.sub(r"\w", "0", x),
|
||||||
|
"mfi_token_token": lambda x: re.sub(r"\w", "0", x),
|
||||||
|
"mfi_token_uuid": lambda x: re.sub(r"\w", "0", x),
|
||||||
|
"ip": lambda x: x, # don't redact but keep listed here for dump_devinfo
|
||||||
|
# smartcam
|
||||||
|
"dev_id": lambda x: "REDACTED_" + x[9::],
|
||||||
|
"device_name": lambda x: "#MASKED_NAME#" if x else "",
|
||||||
|
"device_alias": lambda x: "#MASKED_NAME#" if x else "",
|
||||||
|
"local_ip": lambda x: x, # don't redact but keep listed here for dump_devinfo
|
||||||
|
# robovac
|
||||||
|
"board_sn": lambda _: "000000000000",
|
||||||
|
"custom_sn": lambda _: "000000000000",
|
||||||
|
"location": lambda x: "#MASKED_NAME#" if x else "",
|
||||||
|
"map_data": lambda x: "#SCRUBBED_MAPDATA#" if x else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Queries that are known not to work properly when sent as a
|
||||||
|
# multiRequest. They will not return the `method` key.
|
||||||
|
FORCE_SINGLE_REQUEST = {
|
||||||
|
"getConnectStatus",
|
||||||
|
"scanApList",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +98,7 @@ class SmartProtocol(BaseProtocol):
|
|||||||
self._transport._config.batch_size or self.DEFAULT_MULTI_REQUEST_BATCH_SIZE
|
self._transport._config.batch_size or self.DEFAULT_MULTI_REQUEST_BATCH_SIZE
|
||||||
)
|
)
|
||||||
self._redact_data = True
|
self._redact_data = True
|
||||||
|
self._method_missing_logged = False
|
||||||
|
|
||||||
def get_smart_request(self, method: str, params: dict | None = None) -> str:
|
def get_smart_request(self, method: str, params: dict | None = None) -> str:
|
||||||
"""Get a request message as a string."""
|
"""Get a request message as a string."""
|
||||||
@ -162,17 +185,18 @@ class SmartProtocol(BaseProtocol):
|
|||||||
multi_result: dict[str, Any] = {}
|
multi_result: dict[str, Any] = {}
|
||||||
smart_method = "multipleRequest"
|
smart_method = "multipleRequest"
|
||||||
|
|
||||||
multi_requests = [
|
end = len(requests)
|
||||||
{"method": method, "params": params} if params else {"method": method}
|
|
||||||
for method, params in requests.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
end = len(multi_requests)
|
|
||||||
# The SmartCamProtocol sends requests with a length 1 as a
|
# The SmartCamProtocol sends requests with a length 1 as a
|
||||||
# multipleRequest. The SmartProtocol doesn't so will never
|
# multipleRequest. The SmartProtocol doesn't so will never
|
||||||
# raise_on_error
|
# raise_on_error
|
||||||
raise_on_error = end == 1
|
raise_on_error = end == 1
|
||||||
|
|
||||||
|
multi_requests = [
|
||||||
|
{"method": method, "params": params} if params else {"method": method}
|
||||||
|
for method, params in requests.items()
|
||||||
|
if method not in FORCE_SINGLE_REQUEST
|
||||||
|
]
|
||||||
|
|
||||||
# Break the requests down as there can be a size limit
|
# Break the requests down as there can be a size limit
|
||||||
step = self._multi_request_batch_size
|
step = self._multi_request_batch_size
|
||||||
if step == 1:
|
if step == 1:
|
||||||
@ -233,7 +257,20 @@ class SmartProtocol(BaseProtocol):
|
|||||||
|
|
||||||
responses = response_step["result"]["responses"]
|
responses = response_step["result"]["responses"]
|
||||||
for response in responses:
|
for response in responses:
|
||||||
method = response["method"]
|
# some smartcam devices calls do not populate the method key
|
||||||
|
# these should be defined in DO_NOT_SEND_AS_MULTI_REQUEST.
|
||||||
|
if not (method := response.get("method")):
|
||||||
|
if not self._method_missing_logged:
|
||||||
|
# Avoid spamming the logs
|
||||||
|
self._method_missing_logged = True
|
||||||
|
_LOGGER.error(
|
||||||
|
"No method key in response for %s, skipping: %s",
|
||||||
|
self._host,
|
||||||
|
response_step,
|
||||||
|
)
|
||||||
|
# These will end up being queried individually
|
||||||
|
continue
|
||||||
|
|
||||||
self._handle_response_error_code(
|
self._handle_response_error_code(
|
||||||
response, method, raise_on_error=raise_on_error
|
response, method, raise_on_error=raise_on_error
|
||||||
)
|
)
|
||||||
@ -242,13 +279,17 @@ class SmartProtocol(BaseProtocol):
|
|||||||
result, method, retry_count=retry_count
|
result, method, retry_count=retry_count
|
||||||
)
|
)
|
||||||
multi_result[method] = result
|
multi_result[method] = result
|
||||||
# Multi requests don't continue after errors so requery any missing
|
|
||||||
|
# Multi requests don't continue after errors so requery any missing.
|
||||||
|
# Will also query individually any DO_NOT_SEND_AS_MULTI_REQUEST.
|
||||||
for method, params in requests.items():
|
for method, params in requests.items():
|
||||||
if method not in multi_result:
|
if method not in multi_result:
|
||||||
resp = await self._transport.send(
|
resp = await self._transport.send(
|
||||||
self.get_smart_request(method, params)
|
self.get_smart_request(method, params)
|
||||||
)
|
)
|
||||||
self._handle_response_error_code(resp, method, raise_on_error=False)
|
self._handle_response_error_code(
|
||||||
|
resp, method, raise_on_error=raise_on_error
|
||||||
|
)
|
||||||
multi_result[method] = resp.get("result")
|
multi_result[method] = resp.get("result")
|
||||||
return multi_result
|
return multi_result
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ from .energy import Energy
|
|||||||
from .fan import Fan
|
from .fan import Fan
|
||||||
from .firmware import Firmware
|
from .firmware import Firmware
|
||||||
from .frostprotection import FrostProtection
|
from .frostprotection import FrostProtection
|
||||||
|
from .homekit import HomeKit
|
||||||
from .humiditysensor import HumiditySensor
|
from .humiditysensor import HumiditySensor
|
||||||
from .led import Led
|
from .led import Led
|
||||||
from .light import Light
|
from .light import Light
|
||||||
@ -23,6 +24,7 @@ from .lighteffect import LightEffect
|
|||||||
from .lightpreset import LightPreset
|
from .lightpreset import LightPreset
|
||||||
from .lightstripeffect import LightStripEffect
|
from .lightstripeffect import LightStripEffect
|
||||||
from .lighttransition import LightTransition
|
from .lighttransition import LightTransition
|
||||||
|
from .matter import Matter
|
||||||
from .motionsensor import MotionSensor
|
from .motionsensor import MotionSensor
|
||||||
from .overheatprotection import OverheatProtection
|
from .overheatprotection import OverheatProtection
|
||||||
from .reportmode import ReportMode
|
from .reportmode import ReportMode
|
||||||
@ -66,4 +68,6 @@ __all__ = [
|
|||||||
"Thermostat",
|
"Thermostat",
|
||||||
"SmartLightEffect",
|
"SmartLightEffect",
|
||||||
"OverheatProtection",
|
"OverheatProtection",
|
||||||
|
"HomeKit",
|
||||||
|
"Matter",
|
||||||
]
|
]
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import NoReturn
|
from typing import Any, NoReturn
|
||||||
|
|
||||||
from ...emeterstatus import EmeterStatus
|
from ...emeterstatus import EmeterStatus
|
||||||
from ...exceptions import KasaException
|
from ...exceptions import DeviceError, KasaException
|
||||||
from ...interfaces.energy import Energy as EnergyInterface
|
from ...interfaces.energy import Energy as EnergyInterface
|
||||||
from ..smartmodule import SmartModule, raise_if_update_error
|
from ..smartmodule import SmartModule, raise_if_update_error
|
||||||
|
|
||||||
@ -15,12 +15,39 @@ class Energy(SmartModule, EnergyInterface):
|
|||||||
|
|
||||||
REQUIRED_COMPONENT = "energy_monitoring"
|
REQUIRED_COMPONENT = "energy_monitoring"
|
||||||
|
|
||||||
|
_energy: dict[str, Any]
|
||||||
|
_current_consumption: float | None
|
||||||
|
|
||||||
async def _post_update_hook(self) -> None:
|
async def _post_update_hook(self) -> None:
|
||||||
if "voltage_mv" in self.data.get("get_emeter_data", {}):
|
try:
|
||||||
|
data = self.data
|
||||||
|
except DeviceError as de:
|
||||||
|
self._energy = {}
|
||||||
|
self._current_consumption = None
|
||||||
|
raise de
|
||||||
|
|
||||||
|
# If version is 1 then data is get_energy_usage
|
||||||
|
self._energy = data.get("get_energy_usage", data)
|
||||||
|
|
||||||
|
if "voltage_mv" in data.get("get_emeter_data", {}):
|
||||||
self._supported = (
|
self._supported = (
|
||||||
self._supported | EnergyInterface.ModuleFeature.VOLTAGE_CURRENT
|
self._supported | EnergyInterface.ModuleFeature.VOLTAGE_CURRENT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (power := self._energy.get("current_power")) is not None or (
|
||||||
|
power := data.get("get_emeter_data", {}).get("power_mw")
|
||||||
|
) is not None:
|
||||||
|
self._current_consumption = power / 1_000
|
||||||
|
# Fallback if get_energy_usage does not provide current_power,
|
||||||
|
# which can happen on some newer devices (e.g. P304M).
|
||||||
|
# This may not be valid scenario as it pre-dates trying get_emeter_data
|
||||||
|
elif (
|
||||||
|
power := self.data.get("get_current_power", {}).get("current_power")
|
||||||
|
) is not None:
|
||||||
|
self._current_consumption = power
|
||||||
|
else:
|
||||||
|
self._current_consumption = None
|
||||||
|
|
||||||
def query(self) -> dict:
|
def query(self) -> dict:
|
||||||
"""Query to execute during the update cycle."""
|
"""Query to execute during the update cycle."""
|
||||||
req = {
|
req = {
|
||||||
@ -33,28 +60,21 @@ class Energy(SmartModule, EnergyInterface):
|
|||||||
return req
|
return req
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@raise_if_update_error
|
def optional_response_keys(self) -> list[str]:
|
||||||
def current_consumption(self) -> float | None:
|
"""Return optional response keys for the module."""
|
||||||
"""Current power in watts."""
|
if self.supported_version > 1:
|
||||||
if (power := self.energy.get("current_power")) is not None or (
|
return ["get_energy_usage"]
|
||||||
power := self.data.get("get_emeter_data", {}).get("power_mw")
|
return []
|
||||||
) is not None:
|
|
||||||
return power / 1_000
|
@property
|
||||||
# Fallback if get_energy_usage does not provide current_power,
|
def current_consumption(self) -> float | None:
|
||||||
# which can happen on some newer devices (e.g. P304M).
|
"""Current power in watts."""
|
||||||
elif (
|
return self._current_consumption
|
||||||
power := self.data.get("get_current_power", {}).get("current_power")
|
|
||||||
) is not None:
|
|
||||||
return power
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@raise_if_update_error
|
|
||||||
def energy(self) -> dict:
|
def energy(self) -> dict:
|
||||||
"""Return get_energy_usage results."""
|
"""Return get_energy_usage results."""
|
||||||
if en := self.data.get("get_energy_usage"):
|
return self._energy
|
||||||
return en
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
def _get_status_from_energy(self, energy: dict) -> EmeterStatus:
|
def _get_status_from_energy(self, energy: dict) -> EmeterStatus:
|
||||||
return EmeterStatus(
|
return EmeterStatus(
|
||||||
@ -83,16 +103,18 @@ class Energy(SmartModule, EnergyInterface):
|
|||||||
return self._get_status_from_energy(res["get_energy_usage"])
|
return self._get_status_from_energy(res["get_energy_usage"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@raise_if_update_error
|
|
||||||
def consumption_this_month(self) -> float | None:
|
def consumption_this_month(self) -> float | None:
|
||||||
"""Get the emeter value for this month in kWh."""
|
"""Get the emeter value for this month in kWh."""
|
||||||
return self.energy.get("month_energy", 0) / 1_000
|
if (month := self.energy.get("month_energy")) is not None:
|
||||||
|
return month / 1_000
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@raise_if_update_error
|
|
||||||
def consumption_today(self) -> float | None:
|
def consumption_today(self) -> float | None:
|
||||||
"""Get the emeter value for today in kWh."""
|
"""Get the emeter value for today in kWh."""
|
||||||
return self.energy.get("today_energy", 0) / 1_000
|
if (today := self.energy.get("today_energy")) is not None:
|
||||||
|
return today / 1_000
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@raise_if_update_error
|
@raise_if_update_error
|
||||||
|
32
kasa/smart/modules/homekit.py
Normal file
32
kasa/smart/modules/homekit.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""Implementation of homekit module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
|
class HomeKit(SmartModule):
|
||||||
|
"""Implementation of homekit module."""
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME: str = "get_homekit_info"
|
||||||
|
REQUIRED_COMPONENT = "homekit"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="homekit_setup_code",
|
||||||
|
name="Homekit setup code",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["mfi_setup_code"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> dict[str, str]:
|
||||||
|
"""Homekit mfi setup info."""
|
||||||
|
return self.data
|
@ -136,16 +136,17 @@ class Light(SmartModule, LightInterface):
|
|||||||
return self._light_state
|
return self._light_state
|
||||||
|
|
||||||
async def _post_update_hook(self) -> None:
|
async def _post_update_hook(self) -> None:
|
||||||
if self._device.is_on is False:
|
device = self._device
|
||||||
|
if device.is_on is False:
|
||||||
state = LightState(light_on=False)
|
state = LightState(light_on=False)
|
||||||
else:
|
else:
|
||||||
state = LightState(light_on=True)
|
state = LightState(light_on=True)
|
||||||
if Module.Brightness in self._device.modules:
|
if Module.Brightness in device.modules:
|
||||||
state.brightness = self.brightness
|
state.brightness = self.brightness
|
||||||
if Module.Color in self._device.modules:
|
if Module.Color in device.modules:
|
||||||
hsv = self.hsv
|
hsv = self.hsv
|
||||||
state.hue = hsv.hue
|
state.hue = hsv.hue
|
||||||
state.saturation = hsv.saturation
|
state.saturation = hsv.saturation
|
||||||
if Module.ColorTemperature in self._device.modules:
|
if Module.ColorTemperature in device.modules:
|
||||||
state.color_temp = self.color_temp
|
state.color_temp = self.color_temp
|
||||||
self._light_state = state
|
self._light_state = state
|
||||||
|
43
kasa/smart/modules/matter.py
Normal file
43
kasa/smart/modules/matter.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""Implementation of matter module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
|
class Matter(SmartModule):
|
||||||
|
"""Implementation of matter module."""
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME: str = "get_matter_setup_info"
|
||||||
|
REQUIRED_COMPONENT = "matter"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="matter_setup_code",
|
||||||
|
name="Matter setup code",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["setup_code"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="matter_setup_payload",
|
||||||
|
name="Matter setup payload",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["setup_payload"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> dict[str, str]:
|
||||||
|
"""Matter setup info."""
|
||||||
|
return self.data
|
@ -6,10 +6,11 @@ import logging
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from ..device import DeviceInfo
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
from ..protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
||||||
from .smartdevice import SmartDevice
|
from .smartdevice import ComponentsRaw, SmartDevice
|
||||||
from .smartmodule import SmartModule
|
from .smartmodule import SmartModule
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -37,7 +38,7 @@ class SmartChildDevice(SmartDevice):
|
|||||||
self,
|
self,
|
||||||
parent: SmartDevice,
|
parent: SmartDevice,
|
||||||
info: dict,
|
info: dict,
|
||||||
component_info: dict,
|
component_info_raw: ComponentsRaw,
|
||||||
*,
|
*,
|
||||||
config: DeviceConfig | None = None,
|
config: DeviceConfig | None = None,
|
||||||
protocol: SmartProtocol | None = None,
|
protocol: SmartProtocol | None = None,
|
||||||
@ -47,7 +48,24 @@ class SmartChildDevice(SmartDevice):
|
|||||||
super().__init__(parent.host, config=parent.config, protocol=_protocol)
|
super().__init__(parent.host, config=parent.config, protocol=_protocol)
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._update_internal_state(info)
|
self._update_internal_state(info)
|
||||||
self._components = component_info
|
self._components_raw = component_info_raw
|
||||||
|
self._components = self._parse_components(self._components_raw)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device info.
|
||||||
|
|
||||||
|
Child device does not have it info and components in _last_update so
|
||||||
|
this overrides the base implementation to call _get_device_info with
|
||||||
|
info and components combined as they would be in _last_update.
|
||||||
|
"""
|
||||||
|
return self._get_device_info(
|
||||||
|
{
|
||||||
|
"get_device_info": self._info,
|
||||||
|
"component_nego": self._components_raw,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
async def update(self, update_children: bool = True) -> None:
|
async def update(self, update_children: bool = True) -> None:
|
||||||
"""Update child module info.
|
"""Update child module info.
|
||||||
@ -84,7 +102,7 @@ class SmartChildDevice(SmartDevice):
|
|||||||
cls,
|
cls,
|
||||||
parent: SmartDevice,
|
parent: SmartDevice,
|
||||||
child_info: dict,
|
child_info: dict,
|
||||||
child_components: dict,
|
child_components_raw: ComponentsRaw,
|
||||||
protocol: SmartProtocol | None = None,
|
protocol: SmartProtocol | None = None,
|
||||||
*,
|
*,
|
||||||
last_update: dict | None = None,
|
last_update: dict | None = None,
|
||||||
@ -97,7 +115,7 @@ class SmartChildDevice(SmartDevice):
|
|||||||
derived from the parent.
|
derived from the parent.
|
||||||
"""
|
"""
|
||||||
child: SmartChildDevice = cls(
|
child: SmartChildDevice = cls(
|
||||||
parent, child_info, child_components, protocol=protocol
|
parent, child_info, child_components_raw, protocol=protocol
|
||||||
)
|
)
|
||||||
if last_update:
|
if last_update:
|
||||||
child._last_update = last_update
|
child._last_update = last_update
|
||||||
|
@ -7,9 +7,9 @@ import logging
|
|||||||
import time
|
import time
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from datetime import UTC, datetime, timedelta, tzinfo
|
from datetime import UTC, datetime, timedelta, tzinfo
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, TypeAlias, cast
|
||||||
|
|
||||||
from ..device import Device, WifiNetwork, _DeviceInfo
|
from ..device import Device, DeviceInfo, WifiNetwork
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
|
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
|
||||||
@ -40,6 +40,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
# same issue, homekit perhaps?
|
# same issue, homekit perhaps?
|
||||||
NON_HUB_PARENT_ONLY_MODULES = [DeviceModule, Time, Firmware, Cloud]
|
NON_HUB_PARENT_ONLY_MODULES = [DeviceModule, Time, Firmware, Cloud]
|
||||||
|
|
||||||
|
ComponentsRaw: TypeAlias = dict[str, list[dict[str, int | str]]]
|
||||||
|
|
||||||
|
|
||||||
# Device must go last as the other interfaces also inherit Device
|
# Device must go last as the other interfaces also inherit Device
|
||||||
# and python needs a consistent method resolution order.
|
# and python needs a consistent method resolution order.
|
||||||
@ -61,13 +63,12 @@ class SmartDevice(Device):
|
|||||||
)
|
)
|
||||||
super().__init__(host=host, config=config, protocol=_protocol)
|
super().__init__(host=host, config=config, protocol=_protocol)
|
||||||
self.protocol: SmartProtocol
|
self.protocol: SmartProtocol
|
||||||
self._components_raw: dict[str, Any] | None = None
|
self._components_raw: ComponentsRaw | None = None
|
||||||
self._components: dict[str, int] = {}
|
self._components: dict[str, int] = {}
|
||||||
self._state_information: dict[str, Any] = {}
|
self._state_information: dict[str, Any] = {}
|
||||||
self._modules: dict[str | ModuleName[Module], SmartModule] = {}
|
self._modules: dict[str | ModuleName[Module], SmartModule] = {}
|
||||||
self._parent: SmartDevice | None = None
|
self._parent: SmartDevice | None = None
|
||||||
self._children: Mapping[str, SmartDevice] = {}
|
self._children: Mapping[str, SmartDevice] = {}
|
||||||
self._last_update = {}
|
|
||||||
self._last_update_time: float | None = None
|
self._last_update_time: float | None = None
|
||||||
self._on_since: datetime | None = None
|
self._on_since: datetime | None = None
|
||||||
self._info: dict[str, Any] = {}
|
self._info: dict[str, Any] = {}
|
||||||
@ -82,10 +83,8 @@ class SmartDevice(Device):
|
|||||||
self.internal_state.update(resp)
|
self.internal_state.update(resp)
|
||||||
|
|
||||||
children = self.internal_state["get_child_device_list"]["child_device_list"]
|
children = self.internal_state["get_child_device_list"]["child_device_list"]
|
||||||
children_components = {
|
children_components_raw = {
|
||||||
child["device_id"]: {
|
child["device_id"]: child
|
||||||
comp["id"]: int(comp["ver_code"]) for comp in child["component_list"]
|
|
||||||
}
|
|
||||||
for child in self.internal_state["get_child_device_component_list"][
|
for child in self.internal_state["get_child_device_component_list"][
|
||||||
"child_component_list"
|
"child_component_list"
|
||||||
]
|
]
|
||||||
@ -96,7 +95,7 @@ class SmartDevice(Device):
|
|||||||
child_info["device_id"]: await SmartChildDevice.create(
|
child_info["device_id"]: await SmartChildDevice.create(
|
||||||
parent=self,
|
parent=self,
|
||||||
child_info=child_info,
|
child_info=child_info,
|
||||||
child_components=children_components[child_info["device_id"]],
|
child_components_raw=children_components_raw[child_info["device_id"]],
|
||||||
)
|
)
|
||||||
for child_info in children
|
for child_info in children
|
||||||
}
|
}
|
||||||
@ -131,6 +130,13 @@ class SmartDevice(Device):
|
|||||||
f"{request} not found in {responses} for device {self.host}"
|
f"{request} not found in {responses} for device {self.host}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_components(components_raw: ComponentsRaw) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
str(comp["id"]): int(comp["ver_code"])
|
||||||
|
for comp in components_raw["component_list"]
|
||||||
|
}
|
||||||
|
|
||||||
async def _negotiate(self) -> None:
|
async def _negotiate(self) -> None:
|
||||||
"""Perform initialization.
|
"""Perform initialization.
|
||||||
|
|
||||||
@ -151,12 +157,9 @@ class SmartDevice(Device):
|
|||||||
self._info = self._try_get_response(resp, "get_device_info")
|
self._info = self._try_get_response(resp, "get_device_info")
|
||||||
|
|
||||||
# Create our internal presentation of available components
|
# Create our internal presentation of available components
|
||||||
self._components_raw = cast(dict, resp["component_nego"])
|
self._components_raw = cast(ComponentsRaw, resp["component_nego"])
|
||||||
|
|
||||||
self._components = {
|
self._components = self._parse_components(self._components_raw)
|
||||||
comp["id"]: int(comp["ver_code"])
|
|
||||||
for comp in self._components_raw["component_list"]
|
|
||||||
}
|
|
||||||
|
|
||||||
if "child_device" in self._components and not self.children:
|
if "child_device" in self._components and not self.children:
|
||||||
await self._initialize_children()
|
await self._initialize_children()
|
||||||
@ -493,18 +496,13 @@ class SmartDevice(Device):
|
|||||||
@property
|
@property
|
||||||
def model(self) -> str:
|
def model(self) -> str:
|
||||||
"""Returns the device model."""
|
"""Returns the device model."""
|
||||||
return str(self._info.get("model"))
|
# If update hasn't been called self._device_info can't be used
|
||||||
|
if self._last_update:
|
||||||
|
return self.device_info.short_name
|
||||||
|
|
||||||
@property
|
disco_model = str(self._info.get("device_model"))
|
||||||
def _model_region(self) -> str:
|
long_name, _, _ = disco_model.partition("(")
|
||||||
"""Return device full model name and region."""
|
return long_name
|
||||||
if (disco := self._discovery_info) and (
|
|
||||||
disco_model := disco.get("device_model")
|
|
||||||
):
|
|
||||||
return disco_model
|
|
||||||
# Some devices have the region in the specs element.
|
|
||||||
region = f"({specs})" if (specs := self._info.get("specs")) else ""
|
|
||||||
return f"{self.model}{region}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alias(self) -> str | None:
|
def alias(self) -> str | None:
|
||||||
@ -804,7 +802,7 @@ class SmartDevice(Device):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_device_info(
|
def _get_device_info(
|
||||||
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
||||||
) -> _DeviceInfo:
|
) -> DeviceInfo:
|
||||||
"""Get model information for a device."""
|
"""Get model information for a device."""
|
||||||
di = info["get_device_info"]
|
di = info["get_device_info"]
|
||||||
components = [comp["id"] for comp in info["component_nego"]["component_list"]]
|
components = [comp["id"] for comp in info["component_nego"]["component_list"]]
|
||||||
@ -833,7 +831,7 @@ class SmartDevice(Device):
|
|||||||
# Brand inferred from SMART.KASAPLUG/SMART.TAPOPLUG etc.
|
# Brand inferred from SMART.KASAPLUG/SMART.TAPOPLUG etc.
|
||||||
brand = devicetype[:4].lower()
|
brand = devicetype[:4].lower()
|
||||||
|
|
||||||
return _DeviceInfo(
|
return DeviceInfo(
|
||||||
short_name=short_name,
|
short_name=short_name,
|
||||||
long_name=long_name,
|
long_name=long_name,
|
||||||
brand=brand,
|
brand=brand,
|
||||||
|
@ -57,7 +57,7 @@ class SmartModule(Module):
|
|||||||
#: Module is initialized, if any of the given keys exists in the sysinfo
|
#: Module is initialized, if any of the given keys exists in the sysinfo
|
||||||
SYSINFO_LOOKUP_KEYS: list[str] = []
|
SYSINFO_LOOKUP_KEYS: list[str] = []
|
||||||
#: Query to execute during the main update cycle
|
#: Query to execute during the main update cycle
|
||||||
QUERY_GETTER_NAME: str
|
QUERY_GETTER_NAME: str = ""
|
||||||
|
|
||||||
REGISTERED_MODULES: dict[str, type[SmartModule]] = {}
|
REGISTERED_MODULES: dict[str, type[SmartModule]] = {}
|
||||||
|
|
||||||
@ -72,6 +72,7 @@ class SmartModule(Module):
|
|||||||
self._last_update_time: float | None = None
|
self._last_update_time: float | None = None
|
||||||
self._last_update_error: KasaException | None = None
|
self._last_update_error: KasaException | None = None
|
||||||
self._error_count = 0
|
self._error_count = 0
|
||||||
|
self._logged_remove_keys: list[str] = []
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs) -> None:
|
def __init_subclass__(cls, **kwargs) -> None:
|
||||||
# We only want to register submodules in a modules package so that
|
# We only want to register submodules in a modules package so that
|
||||||
@ -138,7 +139,9 @@ class SmartModule(Module):
|
|||||||
|
|
||||||
Default implementation uses the raw query getter w/o parameters.
|
Default implementation uses the raw query getter w/o parameters.
|
||||||
"""
|
"""
|
||||||
return {self.QUERY_GETTER_NAME: None}
|
if self.QUERY_GETTER_NAME:
|
||||||
|
return {self.QUERY_GETTER_NAME: None}
|
||||||
|
return {}
|
||||||
|
|
||||||
async def call(self, method: str, params: dict | None = None) -> dict:
|
async def call(self, method: str, params: dict | None = None) -> dict:
|
||||||
"""Call a method.
|
"""Call a method.
|
||||||
@ -147,6 +150,15 @@ class SmartModule(Module):
|
|||||||
"""
|
"""
|
||||||
return await self._device._query_helper(method, params)
|
return await self._device._query_helper(method, params)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def optional_response_keys(self) -> list[str]:
|
||||||
|
"""Return optional response keys for the module.
|
||||||
|
|
||||||
|
Defaults to no keys. Overriding this and providing keys will remove
|
||||||
|
instead of raise on error.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self) -> dict[str, Any]:
|
def data(self) -> dict[str, Any]:
|
||||||
"""Return response data for the module.
|
"""Return response data for the module.
|
||||||
@ -179,12 +191,31 @@ class SmartModule(Module):
|
|||||||
|
|
||||||
filtered_data = {k: v for k, v in dev._last_update.items() if k in q_keys}
|
filtered_data = {k: v for k, v in dev._last_update.items() if k in q_keys}
|
||||||
|
|
||||||
|
remove_keys: list[str] = []
|
||||||
for data_item in filtered_data:
|
for data_item in filtered_data:
|
||||||
if isinstance(filtered_data[data_item], SmartErrorCode):
|
if isinstance(filtered_data[data_item], SmartErrorCode):
|
||||||
raise DeviceError(
|
if data_item in self.optional_response_keys:
|
||||||
f"{data_item} for {self.name}", error_code=filtered_data[data_item]
|
remove_keys.append(data_item)
|
||||||
|
else:
|
||||||
|
raise DeviceError(
|
||||||
|
f"{data_item} for {self.name}",
|
||||||
|
error_code=filtered_data[data_item],
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in remove_keys:
|
||||||
|
if key not in self._logged_remove_keys:
|
||||||
|
self._logged_remove_keys.append(key)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Removed key %s from response for device %s as it returned "
|
||||||
|
"error: %s. This message will only be logged once per key.",
|
||||||
|
key,
|
||||||
|
self._device.host,
|
||||||
|
filtered_data[key],
|
||||||
)
|
)
|
||||||
if len(filtered_data) == 1:
|
|
||||||
|
filtered_data.pop(key)
|
||||||
|
|
||||||
|
if len(filtered_data) == 1 and not remove_keys:
|
||||||
return next(iter(filtered_data.values()))
|
return next(iter(filtered_data.values()))
|
||||||
|
|
||||||
return filtered_data
|
return filtered_data
|
||||||
|
@ -1,19 +1,33 @@
|
|||||||
"""Modules for SMARTCAM devices."""
|
"""Modules for SMARTCAM devices."""
|
||||||
|
|
||||||
from .alarm import Alarm
|
from .alarm import Alarm
|
||||||
|
from .babycrydetection import BabyCryDetection
|
||||||
from .camera import Camera
|
from .camera import Camera
|
||||||
from .childdevice import ChildDevice
|
from .childdevice import ChildDevice
|
||||||
from .device import DeviceModule
|
from .device import DeviceModule
|
||||||
|
from .homekit import HomeKit
|
||||||
from .led import Led
|
from .led import Led
|
||||||
|
from .lensmask import LensMask
|
||||||
|
from .matter import Matter
|
||||||
|
from .motiondetection import MotionDetection
|
||||||
from .pantilt import PanTilt
|
from .pantilt import PanTilt
|
||||||
|
from .persondetection import PersonDetection
|
||||||
|
from .tamperdetection import TamperDetection
|
||||||
from .time import Time
|
from .time import Time
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Alarm",
|
"Alarm",
|
||||||
|
"BabyCryDetection",
|
||||||
"Camera",
|
"Camera",
|
||||||
"ChildDevice",
|
"ChildDevice",
|
||||||
"DeviceModule",
|
"DeviceModule",
|
||||||
"Led",
|
"Led",
|
||||||
"PanTilt",
|
"PanTilt",
|
||||||
|
"PersonDetection",
|
||||||
"Time",
|
"Time",
|
||||||
|
"HomeKit",
|
||||||
|
"Matter",
|
||||||
|
"MotionDetection",
|
||||||
|
"LensMask",
|
||||||
|
"TamperDetection",
|
||||||
]
|
]
|
||||||
|
47
kasa/smartcam/modules/babycrydetection.py
Normal file
47
kasa/smartcam/modules/babycrydetection.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Implementation of baby cry detection module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BabyCryDetection(SmartCamModule):
|
||||||
|
"""Implementation of baby cry detection module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "babyCryDetection"
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME = "getBCDConfig"
|
||||||
|
QUERY_MODULE_NAME = "sound_detection"
|
||||||
|
QUERY_SECTION_NAMES = "bcd"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="baby_cry_detection",
|
||||||
|
name="Baby cry detection",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="enabled",
|
||||||
|
attribute_setter="set_enabled",
|
||||||
|
type=Feature.Type.Switch,
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
"""Return the baby cry detection enabled state."""
|
||||||
|
return self.data["bcd"]["enabled"] == "on"
|
||||||
|
|
||||||
|
async def set_enabled(self, enable: bool) -> dict:
|
||||||
|
"""Set the baby cry detection enabled state."""
|
||||||
|
params = {"enabled": "on" if enable else "off"}
|
||||||
|
return await self._device._query_setter_helper(
|
||||||
|
"setBCDConfig", self.QUERY_MODULE_NAME, "bcd", params
|
||||||
|
)
|
@ -1,16 +1,18 @@
|
|||||||
"""Implementation of device module."""
|
"""Implementation of camera module."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
from typing import Annotated
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from ...credentials import Credentials
|
from ...credentials import Credentials
|
||||||
from ...device_type import DeviceType
|
from ...device_type import DeviceType
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
from ...json import loads as json_loads
|
from ...json import loads as json_loads
|
||||||
|
from ...module import FeatureAttribute, Module
|
||||||
from ..smartcammodule import SmartCamModule
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -29,28 +31,38 @@ class StreamResolution(StrEnum):
|
|||||||
class Camera(SmartCamModule):
|
class Camera(SmartCamModule):
|
||||||
"""Implementation of device module."""
|
"""Implementation of device module."""
|
||||||
|
|
||||||
QUERY_GETTER_NAME = "getLensMaskConfig"
|
|
||||||
QUERY_MODULE_NAME = "lens_mask"
|
|
||||||
QUERY_SECTION_NAMES = "lens_mask_info"
|
|
||||||
|
|
||||||
def _initialize_features(self) -> None:
|
def _initialize_features(self) -> None:
|
||||||
"""Initialize features after the initial update."""
|
"""Initialize features after the initial update."""
|
||||||
self._add_feature(
|
if Module.LensMask in self._device.modules:
|
||||||
Feature(
|
self._add_feature(
|
||||||
self._device,
|
Feature(
|
||||||
id="state",
|
self._device,
|
||||||
name="State",
|
id="state",
|
||||||
attribute_getter="is_on",
|
name="State",
|
||||||
attribute_setter="set_state",
|
container=self,
|
||||||
type=Feature.Type.Switch,
|
attribute_getter="is_on",
|
||||||
category=Feature.Category.Primary,
|
attribute_setter="set_state",
|
||||||
|
type=Feature.Type.Switch,
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return the device id."""
|
"""Return the device on state."""
|
||||||
return self.data["lens_mask_info"]["enabled"] == "off"
|
if lens_mask := self._device.modules.get(Module.LensMask):
|
||||||
|
return not lens_mask.enabled
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def set_state(self, on: bool) -> Annotated[dict, FeatureAttribute()]:
|
||||||
|
"""Set the device on state.
|
||||||
|
|
||||||
|
If the device does not support setting state will do nothing.
|
||||||
|
"""
|
||||||
|
if lens_mask := self._device.modules.get(Module.LensMask):
|
||||||
|
# Turning off enables the privacy mask which is why value is reversed.
|
||||||
|
return await lens_mask.set_enabled(not on)
|
||||||
|
return {}
|
||||||
|
|
||||||
def _get_credentials(self) -> Credentials | None:
|
def _get_credentials(self) -> Credentials | None:
|
||||||
"""Get credentials from ."""
|
"""Get credentials from ."""
|
||||||
@ -109,14 +121,6 @@ class Camera(SmartCamModule):
|
|||||||
"""Return the onvif url."""
|
"""Return the onvif url."""
|
||||||
return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service"
|
return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service"
|
||||||
|
|
||||||
async def set_state(self, on: bool) -> dict:
|
|
||||||
"""Set the device state."""
|
|
||||||
# Turning off enables the privacy mask which is why value is reversed.
|
|
||||||
params = {"enabled": "off" if on else "on"}
|
|
||||||
return await self._device._query_setter_helper(
|
|
||||||
"setLensMaskConfig", self.QUERY_MODULE_NAME, "lens_mask_info", params
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _check_supported(self) -> bool:
|
async def _check_supported(self) -> bool:
|
||||||
"""Additional check to see if the module is supported by the device."""
|
"""Additional check to see if the module is supported by the device."""
|
||||||
return self._device.device_type is DeviceType.Camera
|
return self._device.device_type is DeviceType.Camera
|
||||||
|
@ -14,6 +14,13 @@ class DeviceModule(SmartCamModule):
|
|||||||
QUERY_MODULE_NAME = "device_info"
|
QUERY_MODULE_NAME = "device_info"
|
||||||
QUERY_SECTION_NAMES = ["basic_info", "info"]
|
QUERY_SECTION_NAMES = ["basic_info", "info"]
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
q = super().query()
|
||||||
|
q["getConnectionType"] = {"network": {"get_connection_type": []}}
|
||||||
|
|
||||||
|
return q
|
||||||
|
|
||||||
def _initialize_features(self) -> None:
|
def _initialize_features(self) -> None:
|
||||||
"""Initialize features after the initial update."""
|
"""Initialize features after the initial update."""
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
@ -26,6 +33,32 @@ class DeviceModule(SmartCamModule):
|
|||||||
type=Feature.Type.Sensor,
|
type=Feature.Type.Sensor,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if self.rssi is not None:
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
container=self,
|
||||||
|
id="rssi",
|
||||||
|
name="RSSI",
|
||||||
|
attribute_getter="rssi",
|
||||||
|
icon="mdi:signal",
|
||||||
|
unit_getter=lambda: "dBm",
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
container=self,
|
||||||
|
id="signal_level",
|
||||||
|
name="Signal Level",
|
||||||
|
attribute_getter="signal_level",
|
||||||
|
icon="mdi:signal",
|
||||||
|
category=Feature.Category.Info,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def _post_update_hook(self) -> None:
|
async def _post_update_hook(self) -> None:
|
||||||
"""Overriden to prevent module disabling.
|
"""Overriden to prevent module disabling.
|
||||||
@ -37,4 +70,14 @@ class DeviceModule(SmartCamModule):
|
|||||||
@property
|
@property
|
||||||
def device_id(self) -> str:
|
def device_id(self) -> str:
|
||||||
"""Return the device id."""
|
"""Return the device id."""
|
||||||
return self.data["basic_info"]["dev_id"]
|
return self.data[self.QUERY_GETTER_NAME]["basic_info"]["dev_id"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rssi(self) -> int | None:
|
||||||
|
"""Return the device id."""
|
||||||
|
return self.data["getConnectionType"].get("rssiValue")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signal_level(self) -> int | None:
|
||||||
|
"""Return the device id."""
|
||||||
|
return self.data["getConnectionType"].get("rssi")
|
||||||
|
16
kasa/smartcam/modules/homekit.py
Normal file
16
kasa/smartcam/modules/homekit.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""Implementation of homekit module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
|
||||||
|
class HomeKit(SmartCamModule):
|
||||||
|
"""Implementation of homekit module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "homekit"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> dict[str, str]:
|
||||||
|
"""Not supported, return empty dict."""
|
||||||
|
return {}
|
31
kasa/smartcam/modules/lensmask.py
Normal file
31
kasa/smartcam/modules/lensmask.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""Implementation of lens mask privacy module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LensMask(SmartCamModule):
|
||||||
|
"""Implementation of lens mask module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "lensMask"
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME = "getLensMaskConfig"
|
||||||
|
QUERY_MODULE_NAME = "lens_mask"
|
||||||
|
QUERY_SECTION_NAMES = "lens_mask_info"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
"""Return the lens mask state."""
|
||||||
|
return self.data["lens_mask_info"]["enabled"] == "on"
|
||||||
|
|
||||||
|
async def set_enabled(self, enable: bool) -> dict:
|
||||||
|
"""Set the lens mask state."""
|
||||||
|
params = {"enabled": "on" if enable else "off"}
|
||||||
|
return await self._device._query_setter_helper(
|
||||||
|
"setLensMaskConfig", self.QUERY_MODULE_NAME, "lens_mask_info", params
|
||||||
|
)
|
44
kasa/smartcam/modules/matter.py
Normal file
44
kasa/smartcam/modules/matter.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""Implementation of matter module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
|
||||||
|
class Matter(SmartCamModule):
|
||||||
|
"""Implementation of matter module."""
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME = "getMatterSetupInfo"
|
||||||
|
QUERY_MODULE_NAME = "matter"
|
||||||
|
REQUIRED_COMPONENT = "matter"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="matter_setup_code",
|
||||||
|
name="Matter setup code",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["setup_code"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="matter_setup_payload",
|
||||||
|
name="Matter setup payload",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["setup_payload"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> dict[str, str]:
|
||||||
|
"""Matter setup info."""
|
||||||
|
return self.data
|
47
kasa/smartcam/modules/motiondetection.py
Normal file
47
kasa/smartcam/modules/motiondetection.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Implementation of motion detection module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MotionDetection(SmartCamModule):
|
||||||
|
"""Implementation of motion detection module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "detection"
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME = "getDetectionConfig"
|
||||||
|
QUERY_MODULE_NAME = "motion_detection"
|
||||||
|
QUERY_SECTION_NAMES = "motion_det"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="motion_detection",
|
||||||
|
name="Motion detection",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="enabled",
|
||||||
|
attribute_setter="set_enabled",
|
||||||
|
type=Feature.Type.Switch,
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
"""Return the motion detection enabled state."""
|
||||||
|
return self.data["motion_det"]["enabled"] == "on"
|
||||||
|
|
||||||
|
async def set_enabled(self, enable: bool) -> dict:
|
||||||
|
"""Set the motion detection enabled state."""
|
||||||
|
params = {"enabled": "on" if enable else "off"}
|
||||||
|
return await self._device._query_setter_helper(
|
||||||
|
"setDetectionConfig", self.QUERY_MODULE_NAME, "motion_det", params
|
||||||
|
)
|
47
kasa/smartcam/modules/persondetection.py
Normal file
47
kasa/smartcam/modules/persondetection.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Implementation of person detection module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PersonDetection(SmartCamModule):
|
||||||
|
"""Implementation of person detection module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "personDetection"
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME = "getPersonDetectionConfig"
|
||||||
|
QUERY_MODULE_NAME = "people_detection"
|
||||||
|
QUERY_SECTION_NAMES = "detection"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="person_detection",
|
||||||
|
name="Person detection",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="enabled",
|
||||||
|
attribute_setter="set_enabled",
|
||||||
|
type=Feature.Type.Switch,
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
"""Return the person detection enabled state."""
|
||||||
|
return self.data["detection"]["enabled"] == "on"
|
||||||
|
|
||||||
|
async def set_enabled(self, enable: bool) -> dict:
|
||||||
|
"""Set the person detection enabled state."""
|
||||||
|
params = {"enabled": "on" if enable else "off"}
|
||||||
|
return await self._device._query_setter_helper(
|
||||||
|
"setPersonDetectionConfig", self.QUERY_MODULE_NAME, "detection", params
|
||||||
|
)
|
47
kasa/smartcam/modules/tamperdetection.py
Normal file
47
kasa/smartcam/modules/tamperdetection.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Implementation of tamper detection module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TamperDetection(SmartCamModule):
|
||||||
|
"""Implementation of tamper detection module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "tamperDetection"
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME = "getTamperDetectionConfig"
|
||||||
|
QUERY_MODULE_NAME = "tamper_detection"
|
||||||
|
QUERY_SECTION_NAMES = "tamper_det"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="tamper_detection",
|
||||||
|
name="Tamper detection",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="enabled",
|
||||||
|
attribute_setter="set_enabled",
|
||||||
|
type=Feature.Type.Switch,
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
"""Return the tamper detection enabled state."""
|
||||||
|
return self.data["tamper_det"]["enabled"] == "on"
|
||||||
|
|
||||||
|
async def set_enabled(self, enable: bool) -> dict:
|
||||||
|
"""Set the tamper detection enabled state."""
|
||||||
|
params = {"enabled": "on" if enable else "off"}
|
||||||
|
return await self._device._query_setter_helper(
|
||||||
|
"setTamperDetectionConfig", self.QUERY_MODULE_NAME, "tamper_det", params
|
||||||
|
)
|
@ -3,13 +3,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from ..device import _DeviceInfo
|
from ..device import DeviceInfo
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..module import Module
|
from ..module import Module
|
||||||
from ..protocols.smartcamprotocol import _ChildCameraProtocolWrapper
|
from ..protocols.smartcamprotocol import _ChildCameraProtocolWrapper
|
||||||
from ..smart import SmartChildDevice, SmartDevice
|
from ..smart import SmartChildDevice, SmartDevice
|
||||||
|
from ..smart.smartdevice import ComponentsRaw
|
||||||
from .modules import ChildDevice, DeviceModule
|
from .modules import ChildDevice, DeviceModule
|
||||||
from .smartcammodule import SmartCamModule
|
from .smartcammodule import SmartCamModule
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ class SmartCamDevice(SmartDevice):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_device_info(
|
def _get_device_info(
|
||||||
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
||||||
) -> _DeviceInfo:
|
) -> DeviceInfo:
|
||||||
"""Get model information for a device."""
|
"""Get model information for a device."""
|
||||||
basic_info = info["getDeviceInfo"]["device_info"]["basic_info"]
|
basic_info = info["getDeviceInfo"]["device_info"]["basic_info"]
|
||||||
short_name = basic_info["device_model"]
|
short_name = basic_info["device_model"]
|
||||||
@ -44,7 +45,7 @@ class SmartCamDevice(SmartDevice):
|
|||||||
device_type = SmartCamDevice._get_device_type_from_sysinfo(basic_info)
|
device_type = SmartCamDevice._get_device_type_from_sysinfo(basic_info)
|
||||||
fw_version_full = basic_info["sw_version"]
|
fw_version_full = basic_info["sw_version"]
|
||||||
firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
|
firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
|
||||||
return _DeviceInfo(
|
return DeviceInfo(
|
||||||
short_name=basic_info["device_model"],
|
short_name=basic_info["device_model"],
|
||||||
long_name=long_name,
|
long_name=long_name,
|
||||||
brand="tapo",
|
brand="tapo",
|
||||||
@ -78,7 +79,7 @@ class SmartCamDevice(SmartDevice):
|
|||||||
self._children[child_id]._update_internal_state(info)
|
self._children[child_id]._update_internal_state(info)
|
||||||
|
|
||||||
async def _initialize_smart_child(
|
async def _initialize_smart_child(
|
||||||
self, info: dict, child_components: dict
|
self, info: dict, child_components_raw: ComponentsRaw
|
||||||
) -> SmartDevice:
|
) -> SmartDevice:
|
||||||
"""Initialize a smart child device attached to a smartcam device."""
|
"""Initialize a smart child device attached to a smartcam device."""
|
||||||
child_id = info["device_id"]
|
child_id = info["device_id"]
|
||||||
@ -93,7 +94,7 @@ class SmartCamDevice(SmartDevice):
|
|||||||
return await SmartChildDevice.create(
|
return await SmartChildDevice.create(
|
||||||
parent=self,
|
parent=self,
|
||||||
child_info=info,
|
child_info=info,
|
||||||
child_components=child_components,
|
child_components_raw=child_components_raw,
|
||||||
protocol=child_protocol,
|
protocol=child_protocol,
|
||||||
last_update=initial_response,
|
last_update=initial_response,
|
||||||
)
|
)
|
||||||
@ -108,17 +109,8 @@ class SmartCamDevice(SmartDevice):
|
|||||||
self.internal_state.update(resp)
|
self.internal_state.update(resp)
|
||||||
|
|
||||||
smart_children_components = {
|
smart_children_components = {
|
||||||
child["device_id"]: {
|
child["device_id"]: child
|
||||||
comp["id"]: int(comp["ver_code"]) for comp in component_list
|
|
||||||
}
|
|
||||||
for child in resp["getChildDeviceComponentList"]["child_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 = {}
|
children = {}
|
||||||
for info in resp["getChildDeviceList"]["child_device_list"]:
|
for info in resp["getChildDeviceList"]["child_device_list"]:
|
||||||
@ -142,6 +134,11 @@ class SmartCamDevice(SmartDevice):
|
|||||||
if (
|
if (
|
||||||
mod.REQUIRED_COMPONENT
|
mod.REQUIRED_COMPONENT
|
||||||
and mod.REQUIRED_COMPONENT not in self._components
|
and mod.REQUIRED_COMPONENT not in self._components
|
||||||
|
# Always add Camera module to cameras
|
||||||
|
and (
|
||||||
|
mod._module_name() != Module.Camera
|
||||||
|
or self._device_type is not DeviceType.Camera
|
||||||
|
)
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
module = mod(self, mod._module_name())
|
module = mod(self, mod._module_name())
|
||||||
@ -172,6 +169,13 @@ class SmartCamDevice(SmartDevice):
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_components(components_raw: ComponentsRaw) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
str(comp["name"]): int(comp["version"])
|
||||||
|
for comp in components_raw["app_component_list"]
|
||||||
|
}
|
||||||
|
|
||||||
async def _negotiate(self) -> None:
|
async def _negotiate(self) -> None:
|
||||||
"""Perform initialization.
|
"""Perform initialization.
|
||||||
|
|
||||||
@ -181,17 +185,16 @@ class SmartCamDevice(SmartDevice):
|
|||||||
initial_query = {
|
initial_query = {
|
||||||
"getDeviceInfo": {"device_info": {"name": ["basic_info", "info"]}},
|
"getDeviceInfo": {"device_info": {"name": ["basic_info", "info"]}},
|
||||||
"getAppComponentList": {"app_component": {"name": "app_component_list"}},
|
"getAppComponentList": {"app_component": {"name": "app_component_list"}},
|
||||||
|
"getConnectionType": {"network": {"get_connection_type": {}}},
|
||||||
}
|
}
|
||||||
resp = await self.protocol.query(initial_query)
|
resp = await self.protocol.query(initial_query)
|
||||||
self._last_update.update(resp)
|
self._last_update.update(resp)
|
||||||
self._update_internal_info(resp)
|
self._update_internal_info(resp)
|
||||||
|
|
||||||
self._components = {
|
self._components_raw = cast(
|
||||||
comp["name"]: int(comp["version"])
|
ComponentsRaw, resp["getAppComponentList"]["app_component"]
|
||||||
for comp in resp["getAppComponentList"]["app_component"][
|
)
|
||||||
"app_component_list"
|
self._components = self._parse_components(self._components_raw)
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if "childControl" in self._components and not self.children:
|
if "childControl" in self._components and not self.children:
|
||||||
await self._initialize_children()
|
await self._initialize_children()
|
||||||
@ -251,11 +254,16 @@ class SmartCamDevice(SmartDevice):
|
|||||||
def hw_info(self) -> dict:
|
def hw_info(self) -> dict:
|
||||||
"""Return hardware info for the device."""
|
"""Return hardware info for the device."""
|
||||||
return {
|
return {
|
||||||
"sw_ver": self._info.get("hw_ver"),
|
"sw_ver": self._info.get("fw_ver"),
|
||||||
"hw_ver": self._info.get("fw_ver"),
|
"hw_ver": self._info.get("hw_ver"),
|
||||||
"mac": self._info.get("mac"),
|
"mac": self._info.get("mac"),
|
||||||
"type": self._info.get("type"),
|
"type": self._info.get("type"),
|
||||||
"hwId": self._info.get("hwId"),
|
"hwId": self._info.get("hwId"),
|
||||||
"dev_name": self.alias,
|
"dev_name": self.alias,
|
||||||
"oemId": self._info.get("oem_id"),
|
"oemId": self._info.get("oem_id"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rssi(self) -> int | None:
|
||||||
|
"""Return the device id."""
|
||||||
|
return self.modules[SmartCamModule.SmartCamDeviceModule].rssi
|
||||||
|
@ -20,9 +20,23 @@ class SmartCamModule(SmartModule):
|
|||||||
"""Base class for SMARTCAM modules."""
|
"""Base class for SMARTCAM modules."""
|
||||||
|
|
||||||
SmartCamAlarm: Final[ModuleName[modules.Alarm]] = ModuleName("SmartCamAlarm")
|
SmartCamAlarm: Final[ModuleName[modules.Alarm]] = ModuleName("SmartCamAlarm")
|
||||||
|
SmartCamMotionDetection: Final[ModuleName[modules.MotionDetection]] = ModuleName(
|
||||||
|
"MotionDetection"
|
||||||
|
)
|
||||||
|
SmartCamPersonDetection: Final[ModuleName[modules.PersonDetection]] = ModuleName(
|
||||||
|
"PersonDetection"
|
||||||
|
)
|
||||||
|
SmartCamTamperDetection: Final[ModuleName[modules.TamperDetection]] = ModuleName(
|
||||||
|
"TamperDetection"
|
||||||
|
)
|
||||||
|
SmartCamBabyCryDetection: Final[ModuleName[modules.BabyCryDetection]] = ModuleName(
|
||||||
|
"BabyCryDetection"
|
||||||
|
)
|
||||||
|
|
||||||
|
SmartCamDeviceModule: Final[ModuleName[modules.DeviceModule]] = ModuleName(
|
||||||
|
"devicemodule"
|
||||||
|
)
|
||||||
|
|
||||||
#: Query to execute during the main update cycle
|
|
||||||
QUERY_GETTER_NAME: str
|
|
||||||
#: Module name to be queried
|
#: Module name to be queried
|
||||||
QUERY_MODULE_NAME: str
|
QUERY_MODULE_NAME: str
|
||||||
#: Section name or names to be queried
|
#: Section name or names to be queried
|
||||||
@ -37,6 +51,8 @@ class SmartCamModule(SmartModule):
|
|||||||
|
|
||||||
Default implementation uses the raw query getter w/o parameters.
|
Default implementation uses the raw query getter w/o parameters.
|
||||||
"""
|
"""
|
||||||
|
if not self.QUERY_GETTER_NAME:
|
||||||
|
return {}
|
||||||
section_names = (
|
section_names = (
|
||||||
{"name": self.QUERY_SECTION_NAMES} if self.QUERY_SECTION_NAMES else {}
|
{"name": self.QUERY_SECTION_NAMES} if self.QUERY_SECTION_NAMES else {}
|
||||||
)
|
)
|
||||||
@ -86,7 +102,8 @@ class SmartCamModule(SmartModule):
|
|||||||
f" for '{self._module}'"
|
f" for '{self._module}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return query_resp.get(self.QUERY_MODULE_NAME)
|
# Some calls return the data under the module, others not
|
||||||
|
return query_resp.get(self.QUERY_MODULE_NAME, query_resp)
|
||||||
else:
|
else:
|
||||||
found = {key: val for key, val in dev._last_update.items() if key in q}
|
found = {key: val for key, val in dev._last_update.items() if key in q}
|
||||||
for key in q:
|
for key in q:
|
||||||
|
@ -4,6 +4,7 @@ from .aestransport import AesEncyptionSession, AesTransport
|
|||||||
from .basetransport import BaseTransport
|
from .basetransport import BaseTransport
|
||||||
from .klaptransport import KlapTransport, KlapTransportV2
|
from .klaptransport import KlapTransport, KlapTransportV2
|
||||||
from .linkietransport import LinkieTransportV2
|
from .linkietransport import LinkieTransportV2
|
||||||
|
from .sslaestransport import SslAesTransport
|
||||||
from .ssltransport import SslTransport
|
from .ssltransport import SslTransport
|
||||||
from .xortransport import XorEncryption, XorTransport
|
from .xortransport import XorEncryption, XorTransport
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ __all__ = [
|
|||||||
"AesTransport",
|
"AesTransport",
|
||||||
"AesEncyptionSession",
|
"AesEncyptionSession",
|
||||||
"SslTransport",
|
"SslTransport",
|
||||||
|
"SslAesTransport",
|
||||||
"BaseTransport",
|
"BaseTransport",
|
||||||
"KlapTransport",
|
"KlapTransport",
|
||||||
"KlapTransportV2",
|
"KlapTransportV2",
|
||||||
|
@ -8,6 +8,7 @@ import hashlib
|
|||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
import ssl
|
import ssl
|
||||||
|
from contextlib import suppress
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
@ -160,6 +161,19 @@ class SslAesTransport(BaseTransport):
|
|||||||
error_code = SmartErrorCode.INTERNAL_UNKNOWN_ERROR
|
error_code = SmartErrorCode.INTERNAL_UNKNOWN_ERROR
|
||||||
return error_code
|
return error_code
|
||||||
|
|
||||||
|
def _get_response_inner_error(self, resp_dict: Any) -> SmartErrorCode | None:
|
||||||
|
error_code_raw = resp_dict.get("data", {}).get("code")
|
||||||
|
if error_code_raw is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
error_code = SmartErrorCode.from_int(error_code_raw)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Device %s received unknown error code: %s", self._host, error_code_raw
|
||||||
|
)
|
||||||
|
error_code = SmartErrorCode.INTERNAL_UNKNOWN_ERROR
|
||||||
|
return error_code
|
||||||
|
|
||||||
def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None:
|
def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None:
|
||||||
error_code = self._get_response_error(resp_dict)
|
error_code = self._get_response_error(resp_dict)
|
||||||
if error_code is SmartErrorCode.SUCCESS:
|
if error_code is SmartErrorCode.SUCCESS:
|
||||||
@ -216,6 +230,31 @@ class SslAesTransport(BaseTransport):
|
|||||||
ssl=await self._get_ssl_context(),
|
ssl=await self._get_ssl_context(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert self._encryption_session is not None
|
||||||
|
|
||||||
|
# Devices can respond with 500 if another session is created from
|
||||||
|
# the same host. Decryption may not succeed after that
|
||||||
|
if status_code == 500:
|
||||||
|
msg = (
|
||||||
|
f"Device {self._host} replied with status 500 after handshake, "
|
||||||
|
f"response: "
|
||||||
|
)
|
||||||
|
decrypted = None
|
||||||
|
if isinstance(resp_dict, dict) and (
|
||||||
|
response := resp_dict.get("result", {}).get("response")
|
||||||
|
):
|
||||||
|
with suppress(Exception):
|
||||||
|
decrypted = self._encryption_session.decrypt(response.encode())
|
||||||
|
|
||||||
|
if decrypted:
|
||||||
|
msg += decrypted
|
||||||
|
else:
|
||||||
|
msg += str(resp_dict)
|
||||||
|
|
||||||
|
_LOGGER.debug(msg)
|
||||||
|
raise _RetryableError(msg)
|
||||||
|
|
||||||
if status_code != 200:
|
if status_code != 200:
|
||||||
raise KasaException(
|
raise KasaException(
|
||||||
f"{self._host} responded with an unexpected "
|
f"{self._host} responded with an unexpected "
|
||||||
@ -228,7 +267,6 @@ class SslAesTransport(BaseTransport):
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
resp_dict = cast(dict[str, Any], resp_dict)
|
resp_dict = cast(dict[str, Any], resp_dict)
|
||||||
assert self._encryption_session is not None
|
|
||||||
|
|
||||||
if "result" in resp_dict and "response" in resp_dict["result"]:
|
if "result" in resp_dict and "response" in resp_dict["result"]:
|
||||||
raw_response: str = resp_dict["result"]["response"]
|
raw_response: str = resp_dict["result"]["response"]
|
||||||
@ -383,13 +421,29 @@ class SslAesTransport(BaseTransport):
|
|||||||
error_code = default_error_code
|
error_code = default_error_code
|
||||||
resp_dict = default_resp_dict
|
resp_dict = default_resp_dict
|
||||||
|
|
||||||
|
# If the default login worked it's ok not to provide credentials but if
|
||||||
|
# it didn't raise auth error here.
|
||||||
if not self._username:
|
if not self._username:
|
||||||
raise AuthenticationError(
|
raise AuthenticationError(
|
||||||
f"Credentials must be supplied to connect to {self._host}"
|
f"Credentials must be supplied to connect to {self._host}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Device responds with INVALID_NONCE and a "nonce" to indicate ready
|
||||||
|
# for secure login. Otherwise error.
|
||||||
if error_code is not SmartErrorCode.INVALID_NONCE or (
|
if error_code is not SmartErrorCode.INVALID_NONCE or (
|
||||||
resp_dict and "nonce" not in resp_dict["result"].get("data", {})
|
resp_dict and "nonce" not in resp_dict.get("result", {}).get("data", {})
|
||||||
):
|
):
|
||||||
|
if (
|
||||||
|
resp_dict
|
||||||
|
and self._get_response_inner_error(resp_dict)
|
||||||
|
is SmartErrorCode.DEVICE_BLOCKED
|
||||||
|
):
|
||||||
|
sec_left = resp_dict.get("data", {}).get("sec_left")
|
||||||
|
msg = "Device blocked" + (
|
||||||
|
f" for {sec_left} seconds" if sec_left else ""
|
||||||
|
)
|
||||||
|
raise DeviceError(msg, error_code=SmartErrorCode.DEVICE_BLOCKED)
|
||||||
|
|
||||||
raise AuthenticationError(f"Error trying handshake1: {resp_dict}")
|
raise AuthenticationError(f"Error trying handshake1: {resp_dict}")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "python-kasa"
|
name = "python-kasa"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
description = "Python API for TP-Link Kasa and Tapo devices"
|
description = "Python API for TP-Link Kasa and Tapo devices"
|
||||||
license = {text = "GPL-3.0-or-later"}
|
license = {text = "GPL-3.0-or-later"}
|
||||||
authors = [ { name = "python-kasa developers" }]
|
authors = [ { name = "python-kasa developers" }]
|
||||||
|
@ -79,8 +79,6 @@ PLUGS_IOT = {
|
|||||||
"KP125",
|
"KP125",
|
||||||
"KP401",
|
"KP401",
|
||||||
}
|
}
|
||||||
# P135 supports dimming, but its not currently support
|
|
||||||
# by the library
|
|
||||||
PLUGS_SMART = {
|
PLUGS_SMART = {
|
||||||
"P100",
|
"P100",
|
||||||
"P110",
|
"P110",
|
||||||
@ -112,7 +110,7 @@ SWITCHES_SMART = {
|
|||||||
}
|
}
|
||||||
SWITCHES = {*SWITCHES_IOT, *SWITCHES_SMART}
|
SWITCHES = {*SWITCHES_IOT, *SWITCHES_SMART}
|
||||||
STRIPS_IOT = {"HS107", "HS300", "KP303", "KP200", "KP400", "EP40"}
|
STRIPS_IOT = {"HS107", "HS300", "KP303", "KP200", "KP400", "EP40"}
|
||||||
STRIPS_SMART = {"P300", "P304M", "TP25", "EP40M"}
|
STRIPS_SMART = {"P300", "P304M", "TP25", "EP40M", "P210M", "P306"}
|
||||||
STRIPS = {*STRIPS_IOT, *STRIPS_SMART}
|
STRIPS = {*STRIPS_IOT, *STRIPS_SMART}
|
||||||
|
|
||||||
DIMMERS_IOT = {"ES20M", "HS220", "KS220", "KS220M", "KS230", "KP405"}
|
DIMMERS_IOT = {"ES20M", "HS220", "KS220", "KS220M", "KS230", "KP405"}
|
||||||
@ -435,7 +433,7 @@ async def get_device_for_fixture(
|
|||||||
|
|
||||||
discovery_data = None
|
discovery_data = None
|
||||||
if "discovery_result" in fixture_data.data:
|
if "discovery_result" in fixture_data.data:
|
||||||
discovery_data = fixture_data.data["discovery_result"]
|
discovery_data = fixture_data.data["discovery_result"]["result"]
|
||||||
elif "system" in fixture_data.data:
|
elif "system" in fixture_data.data:
|
||||||
discovery_data = {
|
discovery_data = {
|
||||||
"system": {"get_sysinfo": fixture_data.data["system"]["get_sysinfo"]}
|
"system": {"get_sysinfo": fixture_data.data["system"]["get_sysinfo"]}
|
||||||
@ -473,8 +471,12 @@ def get_nearest_fixture_to_ip(dev):
|
|||||||
assert protocol_fixtures, "Unknown device type"
|
assert protocol_fixtures, "Unknown device type"
|
||||||
|
|
||||||
# This will get the best fixture with a match on model region
|
# This will get the best fixture with a match on model region
|
||||||
if model_region_fixtures := filter_fixtures(
|
if (di := dev.device_info) and (
|
||||||
"", model_filter={dev._model_region}, fixture_list=protocol_fixtures
|
model_region_fixtures := filter_fixtures(
|
||||||
|
"",
|
||||||
|
model_filter={di.long_name + (f"({di.region})" if di.region else "")},
|
||||||
|
fixture_list=protocol_fixtures,
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return next(iter(model_region_fixtures))
|
return next(iter(model_region_fixtures))
|
||||||
|
|
||||||
|
@ -139,7 +139,8 @@ smart_discovery = parametrize_discovery("smart discovery", protocol_filter={"SMA
|
|||||||
)
|
)
|
||||||
async def discovery_mock(request, mocker):
|
async def discovery_mock(request, mocker):
|
||||||
"""Mock discovery and patch protocol queries to use Fake protocols."""
|
"""Mock discovery and patch protocol queries to use Fake protocols."""
|
||||||
fixture_info: FixtureInfo = request.param
|
fi: FixtureInfo = request.param
|
||||||
|
fixture_info = FixtureInfo(fi.name, fi.protocol, copy.deepcopy(fi.data))
|
||||||
return patch_discovery({DISCOVERY_MOCK_IP: fixture_info}, mocker)
|
return patch_discovery({DISCOVERY_MOCK_IP: fixture_info}, mocker)
|
||||||
|
|
||||||
|
|
||||||
@ -159,6 +160,17 @@ def create_discovery_mock(ip: str, fixture_data: dict):
|
|||||||
login_version: int | None = None
|
login_version: int | None = None
|
||||||
port_override: int | None = None
|
port_override: int | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self) -> str:
|
||||||
|
dd = self.discovery_data
|
||||||
|
model_region = (
|
||||||
|
dd["result"]["device_model"]
|
||||||
|
if self.discovery_port == 20002
|
||||||
|
else dd["system"]["get_sysinfo"]["model"]
|
||||||
|
)
|
||||||
|
model, _, _ = model_region.partition("(")
|
||||||
|
return model
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _datagram(self) -> bytes:
|
def _datagram(self) -> bytes:
|
||||||
if self.default_port == 9999:
|
if self.default_port == 9999:
|
||||||
@ -170,14 +182,17 @@ def create_discovery_mock(ip: str, fixture_data: dict):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if "discovery_result" in fixture_data:
|
if "discovery_result" in fixture_data:
|
||||||
discovery_data = {"result": fixture_data["discovery_result"].copy()}
|
discovery_data = fixture_data["discovery_result"].copy()
|
||||||
discovery_result = fixture_data["discovery_result"]
|
discovery_result = fixture_data["discovery_result"]["result"]
|
||||||
device_type = discovery_result["device_type"]
|
device_type = discovery_result["device_type"]
|
||||||
encrypt_type = discovery_result["mgt_encrypt_schm"].get(
|
encrypt_type = discovery_result["mgt_encrypt_schm"].get(
|
||||||
"encrypt_type", discovery_result.get("encrypt_info", {}).get("sym_schm")
|
"encrypt_type", discovery_result.get("encrypt_info", {}).get("sym_schm")
|
||||||
)
|
)
|
||||||
|
|
||||||
login_version = discovery_result["mgt_encrypt_schm"].get("lv")
|
if not (login_version := discovery_result["mgt_encrypt_schm"].get("lv")) and (
|
||||||
|
et := discovery_result.get("encrypt_type")
|
||||||
|
):
|
||||||
|
login_version = max([int(i) for i in et])
|
||||||
https = discovery_result["mgt_encrypt_schm"]["is_support_https"]
|
https = discovery_result["mgt_encrypt_schm"]["is_support_https"]
|
||||||
dm = _DiscoveryMock(
|
dm = _DiscoveryMock(
|
||||||
ip,
|
ip,
|
||||||
@ -305,7 +320,7 @@ def discovery_data(request, mocker):
|
|||||||
mocker.patch("kasa.IotProtocol.query", return_value=fixture_data)
|
mocker.patch("kasa.IotProtocol.query", return_value=fixture_data)
|
||||||
mocker.patch("kasa.SmartProtocol.query", return_value=fixture_data)
|
mocker.patch("kasa.SmartProtocol.query", return_value=fixture_data)
|
||||||
if "discovery_result" in fixture_data:
|
if "discovery_result" in fixture_data:
|
||||||
return {"result": fixture_data["discovery_result"]}
|
return fixture_data["discovery_result"].copy()
|
||||||
else:
|
else:
|
||||||
return {"system": {"get_sysinfo": fixture_data["system"]["get_sysinfo"]}}
|
return {"system": {"get_sysinfo": fixture_data["system"]["get_sysinfo"]}}
|
||||||
|
|
||||||
|
@ -114,6 +114,15 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
"type": 0,
|
"type": 0,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
"get_homekit_info": (
|
||||||
|
"homekit",
|
||||||
|
{
|
||||||
|
"mfi_setup_code": "000-00-000",
|
||||||
|
"mfi_setup_id": "0000",
|
||||||
|
"mfi_token_token": "000000000000000000000000000000000",
|
||||||
|
"mfi_token_uuid": "00000000-0000-0000-0000-000000000000",
|
||||||
|
},
|
||||||
|
),
|
||||||
"get_auto_update_info": (
|
"get_auto_update_info": (
|
||||||
"firmware",
|
"firmware",
|
||||||
{"enable": True, "random_range": 120, "time": 180},
|
{"enable": True, "random_range": 120, "time": 180},
|
||||||
@ -151,6 +160,13 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
"energy_monitoring",
|
"energy_monitoring",
|
||||||
{"igain": 10861, "vgain": 118657},
|
{"igain": 10861, "vgain": 118657},
|
||||||
),
|
),
|
||||||
|
"get_matter_setup_info": (
|
||||||
|
"matter",
|
||||||
|
{
|
||||||
|
"setup_code": "00000000000",
|
||||||
|
"setup_payload": "00:0000000-0000.00.000",
|
||||||
|
},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def send(self, request: str):
|
async def send(self, request: str):
|
||||||
|
@ -34,6 +34,7 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
list_return_size=10,
|
list_return_size=10,
|
||||||
is_child=False,
|
is_child=False,
|
||||||
verbatim=False,
|
verbatim=False,
|
||||||
|
components_not_included=False,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
config=DeviceConfig(
|
config=DeviceConfig(
|
||||||
@ -44,6 +45,7 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.fixture_name = fixture_name
|
self.fixture_name = fixture_name
|
||||||
# When True verbatim will bypass any extra processing of missing
|
# When True verbatim will bypass any extra processing of missing
|
||||||
# methods and is used to test the fixture creation itself.
|
# methods and is used to test the fixture creation itself.
|
||||||
@ -58,6 +60,17 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
# self.child_protocols = self._get_child_protocols()
|
# self.child_protocols = self._get_child_protocols()
|
||||||
self.list_return_size = list_return_size
|
self.list_return_size = list_return_size
|
||||||
|
|
||||||
|
# Setting this flag allows tests to create dummy transports without
|
||||||
|
# full fixture info for testing specific cases like list handling etc
|
||||||
|
self.components_not_included = (components_not_included,)
|
||||||
|
if not components_not_included:
|
||||||
|
self.components = {
|
||||||
|
comp["name"]: comp["version"]
|
||||||
|
for comp in self.info["getAppComponentList"]["app_component"][
|
||||||
|
"app_component_list"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_port(self):
|
def default_port(self):
|
||||||
"""Default port for the transport."""
|
"""Default port for the transport."""
|
||||||
@ -112,6 +125,15 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
info = info[key]
|
info = info[key]
|
||||||
info[set_keys[-1]] = value
|
info[set_keys[-1]] = value
|
||||||
|
|
||||||
|
FIXTURE_MISSING_MAP = {
|
||||||
|
"getMatterSetupInfo": (
|
||||||
|
"matter",
|
||||||
|
{
|
||||||
|
"setup_code": "00000000000",
|
||||||
|
"setup_payload": "00:0000000-0000.00.000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
# Setters for when there's not a simple mapping of setters to getters
|
# Setters for when there's not a simple mapping of setters to getters
|
||||||
SETTERS = {
|
SETTERS = {
|
||||||
("system", "sys", "dev_alias"): [
|
("system", "sys", "dev_alias"): [
|
||||||
@ -199,26 +221,38 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
return {**result, "error_code": 0}
|
return {**result, "error_code": 0}
|
||||||
else:
|
else:
|
||||||
return {"error_code": -1}
|
return {"error_code": -1}
|
||||||
elif method[:3] == "get":
|
|
||||||
params = request_dict.get("params")
|
|
||||||
if method in info:
|
|
||||||
result = copy.deepcopy(info[method])
|
|
||||||
if "start_index" in result and "sum" in result:
|
|
||||||
list_key = next(
|
|
||||||
iter([key for key in result if isinstance(result[key], list)])
|
|
||||||
)
|
|
||||||
start_index = (
|
|
||||||
start_index
|
|
||||||
if (params and (start_index := params.get("start_index")))
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
result[list_key] = result[list_key][
|
if method in info:
|
||||||
start_index : start_index + self.list_return_size
|
params = request_dict.get("params")
|
||||||
]
|
result = copy.deepcopy(info[method])
|
||||||
return {"result": result, "error_code": 0}
|
if "start_index" in result and "sum" in result:
|
||||||
else:
|
list_key = next(
|
||||||
return {"error_code": -1}
|
iter([key for key in result if isinstance(result[key], list)])
|
||||||
|
)
|
||||||
|
start_index = (
|
||||||
|
start_index
|
||||||
|
if (params and (start_index := params.get("start_index")))
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
result[list_key] = result[list_key][
|
||||||
|
start_index : start_index + self.list_return_size
|
||||||
|
]
|
||||||
|
return {"result": result, "error_code": 0}
|
||||||
|
|
||||||
|
if self.verbatim:
|
||||||
|
return {"error_code": -1}
|
||||||
|
|
||||||
|
if (
|
||||||
|
# FIXTURE_MISSING is for service calls not in place when
|
||||||
|
# SMART fixtures started to be generated
|
||||||
|
missing_result := self.FIXTURE_MISSING_MAP.get(method)
|
||||||
|
) and missing_result[0] in self.components:
|
||||||
|
# Copy to info so it will work with update methods
|
||||||
|
info[method] = copy.deepcopy(missing_result[1])
|
||||||
|
result = copy.deepcopy(info[method])
|
||||||
|
return {"result": result, "error_code": 0}
|
||||||
|
|
||||||
return {"error_code": -1}
|
return {"error_code": -1}
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
|
@ -145,12 +145,21 @@ def filter_fixtures(
|
|||||||
def _component_match(
|
def _component_match(
|
||||||
fixture_data: FixtureInfo, component_filter: str | ComponentFilter
|
fixture_data: FixtureInfo, component_filter: str | ComponentFilter
|
||||||
):
|
):
|
||||||
if (component_nego := fixture_data.data.get("component_nego")) is None:
|
components = {}
|
||||||
|
if component_nego := fixture_data.data.get("component_nego"):
|
||||||
|
components = {
|
||||||
|
component["id"]: component["ver_code"]
|
||||||
|
for component in component_nego["component_list"]
|
||||||
|
}
|
||||||
|
if get_app_component_list := fixture_data.data.get("getAppComponentList"):
|
||||||
|
components = {
|
||||||
|
component["name"]: component["version"]
|
||||||
|
for component in get_app_component_list["app_component"][
|
||||||
|
"app_component_list"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if not components:
|
||||||
return False
|
return False
|
||||||
components = {
|
|
||||||
component["id"]: component["ver_code"]
|
|
||||||
for component in component_nego["component_list"]
|
|
||||||
}
|
|
||||||
if isinstance(component_filter, str):
|
if isinstance(component_filter, str):
|
||||||
return component_filter in components
|
return component_filter in components
|
||||||
else:
|
else:
|
||||||
|
2
tests/fixtures/iot/EP10(US)_1.0_1.0.2.json
vendored
2
tests/fixtures/iot/EP10(US)_1.0_1.0.2.json
vendored
@ -2,7 +2,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "167 lamp",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi Plug Mini",
|
"dev_name": "Smart Wi-Fi Plug Mini",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
10
tests/fixtures/iot/EP40(US)_1.0_1.0.2.json
vendored
10
tests/fixtures/iot/EP40(US)_1.0_1.0.2.json
vendored
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"alias": "TP-LINK_Smart Plug_004F",
|
"alias": "#MASKED_NAME#",
|
||||||
"child_num": 2,
|
"child_num": 2,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"alias": "Zombie",
|
"alias": "#MASKED_NAME# 1",
|
||||||
"id": "8006231E1499BAC4D4BC7EFCD4B075181E6393F200",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_1",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -14,8 +14,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Magic",
|
"alias": "#MASKED_NAME# 2",
|
||||||
"id": "8006231E1499BAC4D4BC7EFCD4B075181E6393F201",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_2",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
|
2
tests/fixtures/iot/ES20M(US)_1.0_1.0.11.json
vendored
2
tests/fixtures/iot/ES20M(US)_1.0_1.0.11.json
vendored
@ -11,7 +11,7 @@
|
|||||||
"stopConnect": 0,
|
"stopConnect": 0,
|
||||||
"tcspInfo": "",
|
"tcspInfo": "",
|
||||||
"tcspStatus": 1,
|
"tcspStatus": 1,
|
||||||
"username": "#MASKED_NAME#"
|
"username": "user@example.com"
|
||||||
},
|
},
|
||||||
"get_intl_fw_list": {
|
"get_intl_fw_list": {
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/ES20M(US)_1.0_1.0.8.json
vendored
2
tests/fixtures/iot/ES20M(US)_1.0_1.0.8.json
vendored
@ -78,7 +78,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Test ES20M",
|
"alias": "#MASKED_NAME#",
|
||||||
"brightness": 35,
|
"brightness": 35,
|
||||||
"dev_name": "Wi-Fi Smart Dimmer with sensor",
|
"dev_name": "Wi-Fi Smart Dimmer with sensor",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
|
29
tests/fixtures/iot/HS100(UK)_4.1_1.1.0.json
vendored
29
tests/fixtures/iot/HS100(UK)_4.1_1.1.0.json
vendored
@ -1,18 +1,21 @@
|
|||||||
{
|
{
|
||||||
"discovery_result": {
|
"discovery_result": {
|
||||||
"device_id": "00000000000000000000000000000000",
|
"error_code": 0,
|
||||||
"device_model": "HS100(UK)",
|
"result": {
|
||||||
"device_type": "IOT.SMARTPLUGSWITCH",
|
"device_id": "00000000000000000000000000000000",
|
||||||
"factory_default": true,
|
"device_model": "HS100(UK)",
|
||||||
"hw_ver": "4.1",
|
"device_type": "IOT.SMARTPLUGSWITCH",
|
||||||
"ip": "127.0.0.123",
|
"factory_default": true,
|
||||||
"mac": "CC-32-E5-00-00-00",
|
"hw_ver": "4.1",
|
||||||
"mgt_encrypt_schm": {
|
"ip": "127.0.0.123",
|
||||||
"encrypt_type": "KLAP",
|
"mac": "CC-32-E5-00-00-00",
|
||||||
"http_port": 80,
|
"mgt_encrypt_schm": {
|
||||||
"is_support_https": false
|
"encrypt_type": "KLAP",
|
||||||
},
|
"http_port": 80,
|
||||||
"owner": "00000000000000000000000000000000"
|
"is_support_https": false
|
||||||
|
},
|
||||||
|
"owner": "00000000000000000000000000000000"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
|
2
tests/fixtures/iot/HS100(US)_1.0_1.2.5.json
vendored
2
tests/fixtures/iot/HS100(US)_1.0_1.2.5.json
vendored
@ -18,7 +18,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "schedule",
|
"active_mode": "schedule",
|
||||||
"alias": "Unused 3",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Wi-Fi Smart Plug",
|
"dev_name": "Wi-Fi Smart Plug",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS100(US)_2.0_1.5.6.json
vendored
2
tests/fixtures/iot/HS100(US)_2.0_1.5.6.json
vendored
@ -20,7 +20,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "3D Printer",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi Plug",
|
"dev_name": "Smart Wi-Fi Plug",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS103(US)_1.0_1.5.7.json
vendored
2
tests/fixtures/iot/HS103(US)_1.0_1.5.7.json
vendored
@ -20,7 +20,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "schedule",
|
"active_mode": "schedule",
|
||||||
"alias": "Night lite",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi Plug Lite",
|
"dev_name": "Smart Wi-Fi Plug Lite",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS103(US)_2.1_1.1.2.json
vendored
2
tests/fixtures/iot/HS103(US)_2.1_1.1.2.json
vendored
@ -18,7 +18,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Corner",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi Plug Lite",
|
"dev_name": "Smart Wi-Fi Plug Lite",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS103(US)_2.1_1.1.4.json
vendored
2
tests/fixtures/iot/HS103(US)_2.1_1.1.4.json
vendored
@ -2,7 +2,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Plug",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi Plug Lite",
|
"dev_name": "Smart Wi-Fi Plug Lite",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS105(US)_1.0_1.5.6.json
vendored
2
tests/fixtures/iot/HS105(US)_1.0_1.5.6.json
vendored
@ -20,7 +20,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Unused 1",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi Plug Mini",
|
"dev_name": "Smart Wi-Fi Plug Mini",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
12
tests/fixtures/iot/HS107(US)_1.0_1.0.8.json
vendored
12
tests/fixtures/iot/HS107(US)_1.0_1.0.8.json
vendored
@ -17,12 +17,12 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"alias": "TP-LINK_Smart Plug_D310",
|
"alias": "#MASKED_NAME#",
|
||||||
"child_num": 2,
|
"child_num": 2,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"alias": "Garage Charger 1",
|
"alias": "#MASKED_NAME# 1",
|
||||||
"id": "00",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_1",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -30,8 +30,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Garage Charger 2",
|
"alias": "#MASKED_NAME# 2",
|
||||||
"id": "01",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_2",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"hw_ver": "1.0",
|
"hw_ver": "1.0",
|
||||||
"latitude_i": 0,
|
"latitude_i": 0,
|
||||||
"led_off": 0,
|
"led_off": 0,
|
||||||
"longitude_i": -0,
|
"longitude_i": 0,
|
||||||
"mac": "00:00:00:00:00:00",
|
"mac": "00:00:00:00:00:00",
|
||||||
"mic_type": "IOT.SMARTPLUGSWITCH",
|
"mic_type": "IOT.SMARTPLUGSWITCH",
|
||||||
"model": "HS107(US)",
|
"model": "HS107(US)",
|
||||||
|
2
tests/fixtures/iot/HS110(EU)_1.0_1.2.5.json
vendored
2
tests/fixtures/iot/HS110(EU)_1.0_1.2.5.json
vendored
@ -11,7 +11,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "schedule",
|
"active_mode": "schedule",
|
||||||
"alias": "Bedroom Lamp Plug",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
|
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS110(US)_1.0_1.2.6.json
vendored
2
tests/fixtures/iot/HS110(US)_1.0_1.2.6.json
vendored
@ -11,7 +11,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "schedule",
|
"active_mode": "schedule",
|
||||||
"alias": "Home Google WiFi HS110",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
|
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS200(US)_2.0_1.5.7.json
vendored
2
tests/fixtures/iot/HS200(US)_2.0_1.5.7.json
vendored
@ -20,7 +20,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Master Bedroom Fan",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi Light Switch",
|
"dev_name": "Smart Wi-Fi Light Switch",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS200(US)_5.0_1.0.2.json
vendored
2
tests/fixtures/iot/HS200(US)_5.0_1.0.2.json
vendored
@ -2,7 +2,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "House Fan",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi Light Switch",
|
"dev_name": "Smart Wi-Fi Light Switch",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/HS210(US)_1.0_1.5.8.json
vendored
2
tests/fixtures/iot/HS210(US)_1.0_1.5.8.json
vendored
@ -21,7 +21,7 @@
|
|||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"abnormal_detect": 1,
|
"abnormal_detect": 1,
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Garage Light",
|
"alias": "#MASKED_NAME#",
|
||||||
"dev_name": "Smart Wi-Fi 3-Way Light Switch",
|
"dev_name": "Smart Wi-Fi 3-Way Light Switch",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
6
tests/fixtures/iot/HS220(US)_1.0_1.5.7.json
vendored
6
tests/fixtures/iot/HS220(US)_1.0_1.5.7.json
vendored
@ -28,7 +28,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Living Room Dimmer Switch",
|
"alias": "#MASKED_NAME#",
|
||||||
"brightness": 25,
|
"brightness": 25,
|
||||||
"dev_name": "Smart Wi-Fi Dimmer",
|
"dev_name": "Smart Wi-Fi Dimmer",
|
||||||
"deviceId": "000000000000000000000000000000000000000",
|
"deviceId": "000000000000000000000000000000000000000",
|
||||||
@ -38,9 +38,9 @@
|
|||||||
"hwId": "00000000000000000000000000000000",
|
"hwId": "00000000000000000000000000000000",
|
||||||
"hw_ver": "1.0",
|
"hw_ver": "1.0",
|
||||||
"icon_hash": "",
|
"icon_hash": "",
|
||||||
"latitude_i": 11.6210,
|
"latitude_i": 0,
|
||||||
"led_off": 0,
|
"led_off": 0,
|
||||||
"longitude_i": 42.2074,
|
"longitude_i": 0,
|
||||||
"mac": "00:00:00:00:00:00",
|
"mac": "00:00:00:00:00:00",
|
||||||
"mic_type": "IOT.SMARTPLUGSWITCH",
|
"mic_type": "IOT.SMARTPLUGSWITCH",
|
||||||
"model": "HS220(US)",
|
"model": "HS220(US)",
|
||||||
|
2
tests/fixtures/iot/HS220(US)_2.0_1.0.3.json
vendored
2
tests/fixtures/iot/HS220(US)_2.0_1.0.3.json
vendored
@ -17,7 +17,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Living Room Dimmer Switch",
|
"alias": "#MASKED_NAME#",
|
||||||
"brightness": 100,
|
"brightness": 100,
|
||||||
"dev_name": "Wi-Fi Smart Dimmer",
|
"dev_name": "Wi-Fi Smart Dimmer",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
|
28
tests/fixtures/iot/HS300(US)_1.0_1.0.10.json
vendored
28
tests/fixtures/iot/HS300(US)_1.0_1.0.10.json
vendored
@ -22,12 +22,12 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"alias": "TP-LINK_Power Strip_DAE1",
|
"alias": "#MASKED_NAME#",
|
||||||
"child_num": 6,
|
"child_num": 6,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"alias": "Office Monitor 1",
|
"alias": "#MASKED_NAME# 1",
|
||||||
"id": "00",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_1",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -35,8 +35,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Office Monitor 2",
|
"alias": "#MASKED_NAME# 2",
|
||||||
"id": "01",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_2",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -44,8 +44,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Office Monitor 3",
|
"alias": "#MASKED_NAME# 3",
|
||||||
"id": "02",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_3",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -53,8 +53,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Office Laptop Dock",
|
"alias": "#MASKED_NAME# 4",
|
||||||
"id": "03",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_4",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -62,8 +62,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Office Desk Light",
|
"alias": "#MASKED_NAME# 5",
|
||||||
"id": "04",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_5",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -71,8 +71,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Laptop",
|
"alias": "#MASKED_NAME# 6",
|
||||||
"id": "05",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_6",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -87,7 +87,7 @@
|
|||||||
"hw_ver": "1.0",
|
"hw_ver": "1.0",
|
||||||
"latitude_i": 0,
|
"latitude_i": 0,
|
||||||
"led_off": 0,
|
"led_off": 0,
|
||||||
"longitude_i": -0,
|
"longitude_i": 0,
|
||||||
"mac": "00:00:00:00:00:00",
|
"mac": "00:00:00:00:00:00",
|
||||||
"mic_type": "IOT.SMARTPLUGSWITCH",
|
"mic_type": "IOT.SMARTPLUGSWITCH",
|
||||||
"model": "HS300(US)",
|
"model": "HS300(US)",
|
||||||
|
26
tests/fixtures/iot/HS300(US)_1.0_1.0.21.json
vendored
26
tests/fixtures/iot/HS300(US)_1.0_1.0.21.json
vendored
@ -10,12 +10,12 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"alias": "TP-LINK_Power Strip_2CA9",
|
"alias": "#MASKED_NAME#",
|
||||||
"child_num": 6,
|
"child_num": 6,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"alias": "Home CameraPC",
|
"alias": "#MASKED_NAME# 1",
|
||||||
"id": "800623145DFF1AA096363EFD161C2E661A9D8DED00",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_1",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -23,8 +23,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Home Firewalla",
|
"alias": "#MASKED_NAME# 2",
|
||||||
"id": "800623145DFF1AA096363EFD161C2E661A9D8DED01",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_2",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -32,8 +32,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Home Cox modem",
|
"alias": "#MASKED_NAME# 3",
|
||||||
"id": "800623145DFF1AA096363EFD161C2E661A9D8DED02",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_3",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -41,8 +41,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Home rpi3-2",
|
"alias": "#MASKED_NAME# 4",
|
||||||
"id": "800623145DFF1AA096363EFD161C2E661A9D8DED03",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_4",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -50,8 +50,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Home Camera Switch",
|
"alias": "#MASKED_NAME# 5",
|
||||||
"id": "800623145DFF1AA096363EFD161C2E661A9D8DED05",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_5",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -59,8 +59,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Home Network Switch",
|
"alias": "#MASKED_NAME# 6",
|
||||||
"id": "800623145DFF1AA096363EFD161C2E661A9D8DED04",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_6",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
|
24
tests/fixtures/iot/HS300(US)_2.0_1.0.12.json
vendored
24
tests/fixtures/iot/HS300(US)_2.0_1.0.12.json
vendored
@ -15,8 +15,8 @@
|
|||||||
"child_num": 6,
|
"child_num": 6,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"alias": "#MASKED_NAME#",
|
"alias": "#MASKED_NAME# 1",
|
||||||
"id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D00",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_1",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -24,8 +24,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "#MASKED_NAME#",
|
"alias": "#MASKED_NAME# 2",
|
||||||
"id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D01",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_2",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -33,8 +33,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "#MASKED_NAME#",
|
"alias": "#MASKED_NAME# 3",
|
||||||
"id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D02",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_3",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -42,8 +42,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "#MASKED_NAME#",
|
"alias": "#MASKED_NAME# 4",
|
||||||
"id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D03",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_4",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -51,8 +51,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "#MASKED_NAME#",
|
"alias": "#MASKED_NAME# 5",
|
||||||
"id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D04",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_5",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -60,8 +60,8 @@
|
|||||||
"state": 1
|
"state": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "#MASKED_NAME#",
|
"alias": "#MASKED_NAME# 6",
|
||||||
"id": "8006A0F1D01120C3F93794F7AACACDBE1EAD246D05",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_6",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
|
26
tests/fixtures/iot/HS300(US)_2.0_1.0.3.json
vendored
26
tests/fixtures/iot/HS300(US)_2.0_1.0.3.json
vendored
@ -11,12 +11,12 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"alias": "TP-LINK_Power Strip_5C33",
|
"alias": "#MASKED_NAME#",
|
||||||
"child_num": 6,
|
"child_num": 6,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"alias": "Plug 1",
|
"alias": "#MASKED_NAME# 1",
|
||||||
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031900",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_1",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -24,8 +24,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Plug 2",
|
"alias": "#MASKED_NAME# 2",
|
||||||
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031901",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_2",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -33,8 +33,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Plug 3",
|
"alias": "#MASKED_NAME# 3",
|
||||||
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031902",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_3",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -42,8 +42,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Plug 4",
|
"alias": "#MASKED_NAME# 4",
|
||||||
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031903",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_4",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -51,8 +51,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Plug 5",
|
"alias": "#MASKED_NAME# 5",
|
||||||
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031904",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_5",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
@ -60,8 +60,8 @@
|
|||||||
"state": 0
|
"state": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Plug 6",
|
"alias": "#MASKED_NAME# 6",
|
||||||
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031905",
|
"id": "SCRUBBED_CHILD_DEVICE_ID_6",
|
||||||
"next_action": {
|
"next_action": {
|
||||||
"type": -1
|
"type": -1
|
||||||
},
|
},
|
||||||
|
2
tests/fixtures/iot/KL110(US)_1.0_1.8.11.json
vendored
2
tests/fixtures/iot/KL110(US)_1.0_1.8.11.json
vendored
@ -21,7 +21,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Bulb3",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL120(US)_1.0_1.8.11.json
vendored
2
tests/fixtures/iot/KL120(US)_1.0_1.8.11.json
vendored
@ -19,7 +19,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Home Family Room Table",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
8
tests/fixtures/iot/KL120(US)_1.0_1.8.6.json
vendored
8
tests/fixtures/iot/KL120(US)_1.0_1.8.6.json
vendored
@ -34,11 +34,11 @@
|
|||||||
},
|
},
|
||||||
"description": "Smart Wi-Fi LED Bulb with Tunable White Light",
|
"description": "Smart Wi-Fi LED Bulb with Tunable White Light",
|
||||||
"dev_state": "normal",
|
"dev_state": "normal",
|
||||||
"deviceId": "801200814AD69370AC59DE5501319C051AF409C3",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"disco_ver": "1.0",
|
"disco_ver": "1.0",
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
"heapsize": 290784,
|
"heapsize": 290784,
|
||||||
"hwId": "111E35908497A05512E259BB76801E10",
|
"hwId": "00000000000000000000000000000000",
|
||||||
"hw_ver": "1.0",
|
"hw_ver": "1.0",
|
||||||
"is_color": 0,
|
"is_color": 0,
|
||||||
"is_dimmable": 1,
|
"is_dimmable": 1,
|
||||||
@ -52,10 +52,10 @@
|
|||||||
"on_off": 1,
|
"on_off": 1,
|
||||||
"saturation": 0
|
"saturation": 0
|
||||||
},
|
},
|
||||||
"mic_mac": "D80D17150474",
|
"mic_mac": "D80D17000000",
|
||||||
"mic_type": "IOT.SMARTBULB",
|
"mic_type": "IOT.SMARTBULB",
|
||||||
"model": "KL120(US)",
|
"model": "KL120(US)",
|
||||||
"oemId": "1210657CD7FBDC72895644388EEFAE8B",
|
"oemId": "00000000000000000000000000000000",
|
||||||
"preferred_state": [
|
"preferred_state": [
|
||||||
{
|
{
|
||||||
"brightness": 100,
|
"brightness": 100,
|
||||||
|
2
tests/fixtures/iot/KL125(US)_1.20_1.0.5.json
vendored
2
tests/fixtures/iot/KL125(US)_1.20_1.0.5.json
vendored
@ -20,7 +20,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "kasa-bc01",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL125(US)_2.0_1.0.7.json
vendored
2
tests/fixtures/iot/KL125(US)_2.0_1.0.7.json
vendored
@ -22,7 +22,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Test bulb 6",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL130(EU)_1.0_1.8.8.json
vendored
2
tests/fixtures/iot/KL130(EU)_1.0_1.8.8.json
vendored
@ -11,7 +11,7 @@
|
|||||||
"stopConnect": 0,
|
"stopConnect": 0,
|
||||||
"tcspInfo": "",
|
"tcspInfo": "",
|
||||||
"tcspStatus": 1,
|
"tcspStatus": 1,
|
||||||
"username": "#MASKED_NAME#"
|
"username": "user@example.com"
|
||||||
},
|
},
|
||||||
"get_intl_fw_list": {
|
"get_intl_fw_list": {
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/KL130(US)_1.0_1.8.11.json
vendored
2
tests/fixtures/iot/KL130(US)_1.0_1.8.11.json
vendored
@ -21,7 +21,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Bulb2",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL135(US)_1.0_1.0.15.json
vendored
2
tests/fixtures/iot/KL135(US)_1.0_1.0.15.json
vendored
@ -11,7 +11,7 @@
|
|||||||
"stopConnect": 0,
|
"stopConnect": 0,
|
||||||
"tcspInfo": "",
|
"tcspInfo": "",
|
||||||
"tcspStatus": 1,
|
"tcspStatus": 1,
|
||||||
"username": "#MASKED_NAME#"
|
"username": "user@example.com"
|
||||||
},
|
},
|
||||||
"get_intl_fw_list": {
|
"get_intl_fw_list": {
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/KL135(US)_1.0_1.0.6.json
vendored
2
tests/fixtures/iot/KL135(US)_1.0_1.0.6.json
vendored
@ -20,7 +20,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "KL135 Bulb",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"LEF": 0,
|
"LEF": 0,
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Kl400",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"LEF": 0,
|
"LEF": 0,
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Kl400",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"LEF": 1,
|
"LEF": 1,
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Kl420 test",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL430(UN)_2.0_1.0.8.json
vendored
2
tests/fixtures/iot/KL430(UN)_2.0_1.0.8.json
vendored
@ -10,7 +10,7 @@
|
|||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"LEF": 1,
|
"LEF": 1,
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Bedroom light strip",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL430(US)_1.0_1.0.10.json
vendored
2
tests/fixtures/iot/KL430(US)_1.0_1.0.10.json
vendored
@ -23,7 +23,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "schedule",
|
"active_mode": "schedule",
|
||||||
"alias": "Bedroom Lightstrip",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL430(US)_2.0_1.0.11.json
vendored
2
tests/fixtures/iot/KL430(US)_2.0_1.0.11.json
vendored
@ -11,7 +11,7 @@
|
|||||||
"stopConnect": 0,
|
"stopConnect": 0,
|
||||||
"tcspInfo": "",
|
"tcspInfo": "",
|
||||||
"tcspStatus": 1,
|
"tcspStatus": 1,
|
||||||
"username": "#MASKED_NAME#"
|
"username": "user@example.com"
|
||||||
},
|
},
|
||||||
"get_intl_fw_list": {
|
"get_intl_fw_list": {
|
||||||
"err_code": 0,
|
"err_code": 0,
|
||||||
|
2
tests/fixtures/iot/KL430(US)_2.0_1.0.8.json
vendored
2
tests/fixtures/iot/KL430(US)_2.0_1.0.8.json
vendored
@ -10,7 +10,7 @@
|
|||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"LEF": 1,
|
"LEF": 1,
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "89 strip",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL430(US)_2.0_1.0.9.json
vendored
2
tests/fixtures/iot/KL430(US)_2.0_1.0.9.json
vendored
@ -10,7 +10,7 @@
|
|||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"LEF": 1,
|
"LEF": 1,
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "kl430 updated",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
2
tests/fixtures/iot/KL50(US)_1.0_1.1.13.json
vendored
2
tests/fixtures/iot/KL50(US)_1.0_1.1.13.json
vendored
@ -22,7 +22,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Kl50",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
|
4
tests/fixtures/iot/KL60(UN)_1.0_1.1.4.json
vendored
4
tests/fixtures/iot/KL60(UN)_1.0_1.1.4.json
vendored
@ -32,7 +32,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "TP-LINK_Smart Bulb_9179",
|
"alias": "#MASKED_NAME#",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
@ -60,7 +60,7 @@
|
|||||||
"on_off": 0
|
"on_off": 0
|
||||||
},
|
},
|
||||||
"longitude_i": 0,
|
"longitude_i": 0,
|
||||||
"mic_mac": "74DA88C89179",
|
"mic_mac": "74DA88000000",
|
||||||
"mic_type": "IOT.SMARTBULB",
|
"mic_type": "IOT.SMARTBULB",
|
||||||
"model": "KL60(UN)",
|
"model": "KL60(UN)",
|
||||||
"oemId": "00000000000000000000000000000000",
|
"oemId": "00000000000000000000000000000000",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user