././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1585507846.5139406
python-miio-0.5.0.1/ 0000755 0001750 0001750 00000000000 00000000000 013564 5 ustar 00tpr tpr 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585505438.0
python-miio-0.5.0.1/.pre-commit-config.yaml 0000644 0001750 0001750 00000001240 00000000000 020042 0 ustar 00tpr tpr 0000000 0000000 repos:
- repo: https://github.com/ambv/black
rev: stable
hooks:
- id: black
language_version: python3
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: flake8
additional_dependencies: [flake8-docstrings]
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21
hooks:
- id: isort
- repo: https://github.com/PyCQA/doc8
rev: 0.8.1rc2
hooks:
- id: doc8
#- repo: https://github.com/pre-commit/mirrors-mypy
# rev: v0.740
# hooks:
# - id: mypy
# args: [--no-strict-optional, --ignore-missing-imports]
- repo: https://github.com/mgedmin/check-manifest
rev: "0.40"
hooks:
- id: check-manifest
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1549371166.0
python-miio-0.5.0.1/.readthedocs.yml 0000644 0001750 0001750 00000000127 00000000000 016652 0 ustar 00tpr tpr 0000000 0000000 requirements_file: requirements_docs.txt
python:
version: 3
setup_py_install: true
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/.travis.yml 0000644 0001750 0001750 00000000210 00000000000 015666 0 ustar 00tpr tpr 0000000 0000000 sudo: false
language: python
python:
- "3.6"
- "3.7"
install: pip install tox-travis coveralls
script: tox
after_success: coveralls
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507535.0
python-miio-0.5.0.1/CHANGELOG.md 0000644 0001750 0001750 00000266343 00000000000 015413 0 ustar 00tpr tpr 0000000 0000000 # Change Log
## [0.5.0.1](https://github.com/rytilahti/python-miio/tree/0.5.0.1)
Due to a mistake during the release process, some changes were completely left out from the release.
This release simply bases itself on the current master to fix that.
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.5.0...0.5.0.1)
**Closed issues:**
- Xiaomi Mijia Smart Sterilization Humidifier \(SCK0A45\) error - DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b'' [\#649](https://github.com/rytilahti/python-miio/issues/649)
**Merged pull requests:**
- Prepare for 0.5.0 [\#658](https://github.com/rytilahti/python-miio/pull/658) ([rytilahti](https://github.com/rytilahti))
- Add miottemplate tool to simplify adding support for new miot devices [\#656](https://github.com/rytilahti/python-miio/pull/656) ([rytilahti](https://github.com/rytilahti))
- Add Xiaomi Zero Fog Humidifier \(shuii.humidifier.jsq001\) support \(\#642\) [\#654](https://github.com/rytilahti/python-miio/pull/654) ([iromeo](https://github.com/iromeo))
- Gateway get\_device\_prop\_exp command [\#652](https://github.com/rytilahti/python-miio/pull/652) ([fsalomon](https://github.com/fsalomon))
- Add fan\_speed\_presets\(\) for querying available fan speeds [\#643](https://github.com/rytilahti/python-miio/pull/643) ([rytilahti](https://github.com/rytilahti))
- Initial support for xiaomi gateway devices [\#470](https://github.com/rytilahti/python-miio/pull/470) ([rytilahti](https://github.com/rytilahti))
## [0.5.0](https://github.com/rytilahti/python-miio/tree/0.5.0)
Xiaomi is slowly moving to use new protocol dubbed MiOT on the newer devices. To celebrate the integration of initial support for this protocol, it is time to jump from 0.4 to 0.5 series! Shout-out to @rezmus for the insightful notes, links, clarifications on #543 to help to understand how the protocol works!
Special thanks go to both @petrkotek (for initial support) and @foxel (for polishing it for this release) for making this possible. The ground work they did will make adding support for other new miot devices possible.
For those who are interested in adding support to new MiOT devices can check out devtools directory in the git repository, which now hosts a tool to simplify the process. As always, contributions are welcome!
This release adds support for the following new devices:
* Air purifier 3/3H support (zhimi.airpurifier.mb3, zhimi.airpurifier.ma4)
* Xiaomi Gateway devices (lumi.gateway.v3, basic support)
* SmartMi Zhimi Heaters (zhimi.heater.za2)
* Xiaomi Zero Fog Humidifier (shuii.humidifier.jsq001)
Fixes & Enhancements:
* Vacuum objects can now be queried for supported fanspeeds
* Several improvements to Viomi vacuums
* Roborock S6: recovery map controls
* And some other fixes, see the full changelog!
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.8...0.5.0)
**Closed issues:**
- viomi.vacuum.v7 and zhimi.airpurifier.mb3 support homeassistain yet? [\#645](https://github.com/rytilahti/python-miio/issues/645)
- subcon should be a Construct field [\#641](https://github.com/rytilahti/python-miio/issues/641)
- Roborock S6 - only reachable from different subnet [\#640](https://github.com/rytilahti/python-miio/issues/640)
- Python 3.7 error [\#639](https://github.com/rytilahti/python-miio/issues/639)
- Posibillity for local push instead of poll? [\#638](https://github.com/rytilahti/python-miio/issues/638)
- Xiaomi STYJ02YM discovered but not responding [\#628](https://github.com/rytilahti/python-miio/issues/628)
- miplug module is not working from python scrips [\#621](https://github.com/rytilahti/python-miio/issues/621)
- Unsupported device found: zhimi.humidifier.v1 [\#620](https://github.com/rytilahti/python-miio/issues/620)
- Support for Smartmi Radiant Heater Smart Version \(zhimi.heater.za2\) [\#615](https://github.com/rytilahti/python-miio/issues/615)
- Support for Xiaomi Qingping Bluetooth Alarm Clock? [\#614](https://github.com/rytilahti/python-miio/issues/614)
- How to connect a device to WIFI without MiHome app | Can I connect a device to WIFI using Raspberry Pi? \#help wanted \#Support [\#609](https://github.com/rytilahti/python-miio/issues/609)
- Additional commands for vacuum [\#607](https://github.com/rytilahti/python-miio/issues/607)
- "cgllc.airmonitor.b1" No response from the device [\#603](https://github.com/rytilahti/python-miio/issues/603)
- Xiao AI Smart Alarm Clock Time [\#600](https://github.com/rytilahti/python-miio/issues/600)
- Support new device \(yeelink.light.lamp4\) [\#598](https://github.com/rytilahti/python-miio/issues/598)
- Errors not shown for S6 [\#595](https://github.com/rytilahti/python-miio/issues/595)
- Fully charged state not shown [\#594](https://github.com/rytilahti/python-miio/issues/594)
- Support for Roborock S6/T6 [\#593](https://github.com/rytilahti/python-miio/issues/593)
- Pi3 b python error [\#588](https://github.com/rytilahti/python-miio/issues/588)
- Support for Xiaomi Air Purifier 3 \(zhimi.airpurifier.ma4\) [\#577](https://github.com/rytilahti/python-miio/issues/577)
- Updater: Uses wrong local IP address for HTTP server [\#571](https://github.com/rytilahti/python-miio/issues/571)
- How to deal with getDeviceWifi\(\).subscribe [\#528](https://github.com/rytilahti/python-miio/issues/528)
- Move Roborock when in error [\#524](https://github.com/rytilahti/python-miio/issues/524)
- Roborock v2 zoned\_clean\(\) doesn't work [\#490](https://github.com/rytilahti/python-miio/issues/490)
- \[ADD\] Xiaomi Mijia Caméra IP WiFi 1080P Panoramique [\#484](https://github.com/rytilahti/python-miio/issues/484)
- Add unit tests [\#88](https://github.com/rytilahti/python-miio/issues/88)
- Get the map from Mi Vacuum V1? [\#356](https://github.com/rytilahti/python-miio/issues/356)
**Merged pull requests:**
- Add miottemplate tool to simplify adding support for new miot devices [\#656](https://github.com/rytilahti/python-miio/pull/656) ([rytilahti](https://github.com/rytilahti))
- Add Xiaomi Zero Fog Humidifier \(shuii.humidifier.jsq001\) support \(\#642\) [\#654](https://github.com/rytilahti/python-miio/pull/654) ([iromeo](https://github.com/iromeo))
- Gateway get\_device\_prop\_exp command [\#652](https://github.com/rytilahti/python-miio/pull/652) ([fsalomon](https://github.com/fsalomon))
- Add fan\_speed\_presets\(\) for querying available fan speeds [\#643](https://github.com/rytilahti/python-miio/pull/643) ([rytilahti](https://github.com/rytilahti))
- Air purifier 3/3H support \(remastered\) [\#634](https://github.com/rytilahti/python-miio/pull/634) ([foxel](https://github.com/foxel))
- Add eyecare on/off to philips\_eyecare\_cli [\#631](https://github.com/rytilahti/python-miio/pull/631) ([hhrsscc](https://github.com/hhrsscc))
- Extend viomi vacuum support [\#626](https://github.com/rytilahti/python-miio/pull/626) ([rytilahti](https://github.com/rytilahti))
- Add support for SmartMi Zhimi Heaters [\#625](https://github.com/rytilahti/python-miio/pull/625) ([bazuchan](https://github.com/bazuchan))
- Add error code 24 definition \("No-go zone or invisible wall detected"\) [\#623](https://github.com/rytilahti/python-miio/pull/623) ([insajd](https://github.com/insajd))
- s6: two new commands for map handling [\#608](https://github.com/rytilahti/python-miio/pull/608) ([glompfine](https://github.com/glompfine))
- Refactoring: Split Device class into Device+Protocol [\#592](https://github.com/rytilahti/python-miio/pull/592) ([petrkotek](https://github.com/petrkotek))
- STYJ02YM: Manual movement and mop mode support [\#590](https://github.com/rytilahti/python-miio/pull/590) ([rumpeltux](https://github.com/rumpeltux))
- Initial support for xiaomi gateway devices [\#470](https://github.com/rytilahti/python-miio/pull/470) ([rytilahti](https://github.com/rytilahti))
## [0.4.8](https://github.com/rytilahti/python-miio/tree/0.4.8)
This release adds support for the following new devices:
* Xiaomi Mijia STYJ02YM vacuum \(viomi.vacuum.v7\)
* Xiaomi Mi Smart Humidifier \(deerma.humidifier.mjjsq\)
* Xiaomi Mi Fresh Air Ventilator \(dmaker.airfresh.t2017\)
* Xiaomi Philips Desk Lamp RW Read \(philips.light.rwread\)
* Xiaomi Philips LED Ball Lamp White \(philips.light.hbulb\)
Fixes & Enhancements:
* Improve Xiaomi Tinymu Smart Toilet Cover support
* Remove UTF-8 encoding definition from source files
* Azure pipeline for tests
* Pre-commit hook to enforce black, flake8 and isort
* Pre-commit hook to check-manifest, check for pypi-description, flake8-docstrings
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.7...0.4.8)
**Implemented enhancements:**
- Support for new vaccum Xiaomi Mijia STYJ02YM [\#550](https://github.com/rytilahti/python-miio/issues/550)
- Support for Mi Smart Humidifier \(deerma.humidifier.mjjsq\) [\#533](https://github.com/rytilahti/python-miio/issues/533)
- Support for Mi Fresh Air Ventilator dmaker.airfresh.t2017 [\#502](https://github.com/rytilahti/python-miio/issues/502)
**Closed issues:**
- The voice pack does not change in Xiaomi Vacuum 1S [\#583](https://github.com/rytilahti/python-miio/issues/583)
- Support for chuangmi.plug.hmi206 [\#574](https://github.com/rytilahti/python-miio/issues/574)
- miplug crash in macos catalina 10.15.1 [\#573](https://github.com/rytilahti/python-miio/issues/573)
- Roborock S50 not responding to handshake anymore [\#572](https://github.com/rytilahti/python-miio/issues/572)
- Cannot control my Roborock S50 through my home wifi network [\#570](https://github.com/rytilahti/python-miio/issues/570)
- I can not get load\_power with my set is Xiaomi Smart WiFi with two usb \(chuangmi.plug.v3\) [\#549](https://github.com/rytilahti/python-miio/issues/549)
**Merged pull requests:**
- Add Xiaomi Mi Fresh Air \(dmaker.airfresh.t2017\) support [\#591](https://github.com/rytilahti/python-miio/pull/591) ([syssi](https://github.com/syssi))
- Add philips.light.rwread support [\#589](https://github.com/rytilahti/python-miio/pull/589) ([syssi](https://github.com/syssi))
- Add philips.light.hbulb support [\#587](https://github.com/rytilahti/python-miio/pull/587) ([syssi](https://github.com/syssi))
- Add support for deerma.humidifier.mjjsq [\#586](https://github.com/rytilahti/python-miio/pull/586) ([syssi](https://github.com/syssi))
- Improve toiletlid various parameters [\#579](https://github.com/rytilahti/python-miio/pull/579) ([scp10011](https://github.com/scp10011))
- Add support for Xiaomi Mijia STYJ02YM \(viomi.vacuum.v7\) [\#576](https://github.com/rytilahti/python-miio/pull/576) ([rytilahti](https://github.com/rytilahti))
- Add check-manifest, check for pypi-description, flake8-docstrings [\#575](https://github.com/rytilahti/python-miio/pull/575) ([rytilahti](https://github.com/rytilahti))
- Remove UTF-8 encoding comment [\#569](https://github.com/rytilahti/python-miio/pull/569) ([quamilek](https://github.com/quamilek))
- Improve the contribution process with better checks and docs [\#568](https://github.com/rytilahti/python-miio/pull/568) ([rytilahti](https://github.com/rytilahti))
- add azure pipeline for tests, and enforce black, flake8 and isort for commits [\#566](https://github.com/rytilahti/python-miio/pull/566) ([rytilahti](https://github.com/rytilahti))
## [0.4.7](https://github.com/rytilahti/python-miio/tree/0.4.7)
This release adds support for the following new devices:
* Widetech WDH318EFW1 dehumidifier \(nwt.derh.wdh318efw1\)
* Xiaomi Xiao AI Smart Alarm Clock \(zimi.clock.myk01\)
* Xiaomi Air Quality Monitor 2gen \(cgllc.airmonitor.b1\)
* Xiaomi ZNCZ05CM EU Smart Socket \(chuangmi.plug.hmi206\)
Fixes & Enhancements:
* Air Humidifier: Parsing of the firmware version improved
* Add travis build for python 3.7
* Use black for source code formatting
* Require python \>=3.6
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.6...0.4.7)
**Implemented enhancements:**
- Add support for WIDETECH WDH318EFW1 dehumidifier \(nwt.derh.wdh318efw1\) [\#534](https://github.com/rytilahti/python-miio/issues/534)
- Support for Xiaomi Xiao AI Smart Alarm Clock \(zimi.clock.myk01\) [\#505](https://github.com/rytilahti/python-miio/issues/505)
- Add support for the cgllc.airmonitor.b1 [\#562](https://github.com/rytilahti/python-miio/pull/562) ([fwestenberg](https://github.com/fwestenberg))
**Fixed bugs:**
- Air Humidifier [\#529](https://github.com/rytilahti/python-miio/issues/529)
**Closed issues:**
- mirobo updater -- No request was made [\#557](https://github.com/rytilahti/python-miio/issues/557)
- What‘s the plan to release 0.4.6? [\#553](https://github.com/rytilahti/python-miio/issues/553)
- does this support aqara water leak sensor? [\#551](https://github.com/rytilahti/python-miio/issues/551)
- Unsupported device chuangmi.plug.hmi206 [\#545](https://github.com/rytilahti/python-miio/issues/545)
- python-miio not compatible with Python \<=3.5.1 [\#494](https://github.com/rytilahti/python-miio/issues/494)
- Support for Xiaomi Air Quality Monitor 2gen [\#419](https://github.com/rytilahti/python-miio/issues/419)
**Merged pull requests:**
- Add travis build for python 3.7 [\#561](https://github.com/rytilahti/python-miio/pull/561) ([syssi](https://github.com/syssi))
- bump required python version to 3.6+ [\#560](https://github.com/rytilahti/python-miio/pull/560) ([rytilahti](https://github.com/rytilahti))
- Use black for source code formatting [\#559](https://github.com/rytilahti/python-miio/pull/559) ([rytilahti](https://github.com/rytilahti))
- Add initial support for Xiao AI Smart Alarm Clock \(zimi.clock.myk01\) [\#558](https://github.com/rytilahti/python-miio/pull/558) ([rytilahti](https://github.com/rytilahti))
- Improve firmware version parser of the Air Humidifier \(Closes: \#529\) [\#556](https://github.com/rytilahti/python-miio/pull/556) ([syssi](https://github.com/syssi))
- Bring cgllc.airmonitor.s1 into line [\#555](https://github.com/rytilahti/python-miio/pull/555) ([syssi](https://github.com/syssi))
- Add Xiaomi ZNCZ05CM EU Smart Socket \(chuangmi.plug.hmi206\) support [\#554](https://github.com/rytilahti/python-miio/pull/554) ([syssi](https://github.com/syssi))
## [0.4.6](https://github.com/rytilahti/python-miio/tree/0.4.6)
This release adds support for the following new devices:
* Xiaomi Air Quality Monitor S1 \(cgllc.airmonitor.s1\)
* Xiaomi Mi Dehumidifier V1 \(nwt.derh.wdh318efw1\)
* Xiaomi Mi Roborock M1S and Mi Robot S1
* Xiaomi Mijia 360 1080p camera \(chuangmi.camera.ipc009\)
* Xiaomi Mi Smart Fan \(zhimi.fan.za3, zhimi.fan.za4, dmaker.fan.p5\)
* Xiaomi Smartmi Pure Evaporative Air Humidifier \(zhimi.humidifier.cb1\)
* Xiaomi Tinymu Smart Toilet Cover
* Xiaomi 16 Relays Module
Fixes & Enhancements:
* Air Conditioning Companion: Add particular swing mode values of a chigo air conditioner
* Air Humidifier: Handle poweroff exception on set\_mode
* Chuangmi IR controller: Add indicator led support
* Chuangmi IR controller: Add discovery of the Xiaomi IR remote 2gen \(chuangmi.remote.h102a03\)
* Chuangmi Plug: Fix set\_wifi\_led cli command
* Vacuum: Add state 18 as "segment cleaning"
* Device: Add easily accessible properties to DeviceError exception
* Always import DeviceError exception
* Require click version \>=7
* Remove pretty\_cron and typing dependencies from requirements.txt
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.5...0.4.6)
**Closed issues:**
- Roborock Vacuum Bin full [\#546](https://github.com/rytilahti/python-miio/issues/546)
- Add support for Xiao AI Smart Alarm Clock [\#538](https://github.com/rytilahti/python-miio/issues/538)
- rockrobo.vacuum.v1 Error: No response from the device [\#536](https://github.com/rytilahti/python-miio/issues/536)
- Assistance [\#532](https://github.com/rytilahti/python-miio/issues/532)
- Unsupported device found - roborock.vacuum.s5 [\#527](https://github.com/rytilahti/python-miio/issues/527)
- Discovery mode to chuangmi\_camera. [\#522](https://github.com/rytilahti/python-miio/issues/522)
- 新款小米1X电风扇不支持 [\#520](https://github.com/rytilahti/python-miio/issues/520)
- Add swing mode of a Chigo Air Conditioner [\#518](https://github.com/rytilahti/python-miio/issues/518)
- Discover not working with Mi AirHumidifier CA1 [\#514](https://github.com/rytilahti/python-miio/issues/514)
- Question about vacuum errors\_codes duration [\#511](https://github.com/rytilahti/python-miio/issues/511)
- Support device model dmaker.fan.p5 [\#510](https://github.com/rytilahti/python-miio/issues/510)
- Roborock S50: ERROR:miio.updater:No request was made.. [\#508](https://github.com/rytilahti/python-miio/issues/508)
- Roborock S50: losing connection with mirobo [\#507](https://github.com/rytilahti/python-miio/issues/507)
- Support for Xiaomi IR Remote \(chuangmi.remote.v2\) [\#506](https://github.com/rytilahti/python-miio/issues/506)
- Support for Humidifier new model: zhimi.humidifier.cb1 [\#492](https://github.com/rytilahti/python-miio/issues/492)
- impossible to get the last version \(0.4.5\) or even the 0.4.4 [\#489](https://github.com/rytilahti/python-miio/issues/489)
- Getting the token of Air Purifier Pro v7 [\#461](https://github.com/rytilahti/python-miio/issues/461)
- Moonlight sync with HA [\#452](https://github.com/rytilahti/python-miio/issues/452)
- Replace pretty-cron dependency with cron\_descriptor [\#423](https://github.com/rytilahti/python-miio/issues/423)
**Merged pull requests:**
- remove pretty\_cron and typing dependencies from requirements.txt [\#548](https://github.com/rytilahti/python-miio/pull/548) ([rytilahti](https://github.com/rytilahti))
- Add tinymu smart toiletlid [\#544](https://github.com/rytilahti/python-miio/pull/544) ([scp10011](https://github.com/scp10011))
- Add support for Air Quality Monitor S1 \(cgllc.airmonitor.s1\) [\#539](https://github.com/rytilahti/python-miio/pull/539) ([zhumuht](https://github.com/zhumuht))
- Add pwzn relay [\#537](https://github.com/rytilahti/python-miio/pull/537) ([SchumyHao](https://github.com/SchumyHao))
- add mi dehumidifier v1 \(nwt.derh.wdh318efw1\) [\#535](https://github.com/rytilahti/python-miio/pull/535) ([stkang](https://github.com/stkang))
- add mi robot s1 \(m1s\) to discovery [\#531](https://github.com/rytilahti/python-miio/pull/531) ([rytilahti](https://github.com/rytilahti))
- Add preliminary Roborock M1S / Mi Robot S1 support [\#526](https://github.com/rytilahti/python-miio/pull/526) ([syssi](https://github.com/syssi))
- Add state 18 as "segment cleaning" [\#525](https://github.com/rytilahti/python-miio/pull/525) ([syssi](https://github.com/syssi))
- Add particular swing mode values of a chigo air conditioner [\#519](https://github.com/rytilahti/python-miio/pull/519) ([syssi](https://github.com/syssi))
- Add chuangmi.camera.ipc009 support [\#516](https://github.com/rytilahti/python-miio/pull/516) ([impankratov](https://github.com/impankratov))
- Add zhimi.fan.za3 support [\#515](https://github.com/rytilahti/python-miio/pull/515) ([syssi](https://github.com/syssi))
- Add dmaker.fan.p5 support [\#513](https://github.com/rytilahti/python-miio/pull/513) ([syssi](https://github.com/syssi))
- Add zhimi.fan.za4 support [\#512](https://github.com/rytilahti/python-miio/pull/512) ([syssi](https://github.com/syssi))
- Require click version \>=7 [\#503](https://github.com/rytilahti/python-miio/pull/503) ([fvollmer](https://github.com/fvollmer))
- Add indicator led support of the chuangmi.remote.h102a03 and chuangmi.remote.v2 [\#500](https://github.com/rytilahti/python-miio/pull/500) ([syssi](https://github.com/syssi))
- Chuangmi Plug: Fix set\_wifi\_led cli command [\#499](https://github.com/rytilahti/python-miio/pull/499) ([syssi](https://github.com/syssi))
- Add discovery of the Xiaomi IR remote 2gen \(chuangmi.remote.h102a03\) [\#497](https://github.com/rytilahti/python-miio/pull/497) ([syssi](https://github.com/syssi))
- Air Humidifier: Handle poweroff exception on set\_mode [\#496](https://github.com/rytilahti/python-miio/pull/496) ([syssi](https://github.com/syssi))
- Add zhimi.humidifier.cb1 support [\#493](https://github.com/rytilahti/python-miio/pull/493) ([antylama](https://github.com/antylama))
- Add easily accessible properties to DeviceError exception [\#488](https://github.com/rytilahti/python-miio/pull/488) ([syssi](https://github.com/syssi))
- Always import DeviceError exception [\#487](https://github.com/rytilahti/python-miio/pull/487) ([syssi](https://github.com/syssi))
## [0.4.5](https://github.com/rytilahti/python-miio/tree/0.4.5)
This release adds support for the following new devices:
* Xiaomi Chuangmi Plug M3
* Xiaomi Chuangmi Plug HMI205
* Xiaomi Air Purifier Pro V7
* Xiaomi Air Quality Monitor 2gen
* Xiaomi Aqara Camera
Fixes & Enhancements:
* Handle "resp invalid json" error
* Drop pretty\_cron dependency
* Make android\_backup an optional dependency
* Docs: Add troubleshooting guide for cross-subnet communications
* Docs: Fix link in discovery.rst
* Docs: Sphinx config fix
* Docs: Token extraction for Apple users
* Docs: Add a troubleshooting entry for vacuum timeouts
* Docs: New method to obtain tokens
* miio-extract-tokens: Allow extraction from Yeelight app db
* miio-extract-tokens: Fix for devices without tokens
API changes:
* Air Conditioning Partner: Add swing mode 7 with unknown meaning
* Air Conditioning Partner: Extract the return value of the plug\_state request properly
* Air Conditioning Partner: Expose power\_socket property
* Air Conditioning Partner: Fix some conversion issues
* Air Humidifier: Add set\_led method
* Air Humidifier: Rename speed property to avoid a name clash at HA
* Air Humidifier CA1: Fix led brightness command
* Air Purifier: Add favorite level 17
* Moonlight: Align signature of set\_brightness\_and\_rgb
* Moonlight: Fix parameters of the set\_rgb api call
* Moonlight: Night mode support and additional scenes
* Vacuum: Add control for persistent maps, no-go zones and barriers
* Vacuum: Add resume\_zoned\_clean\(\) and resume\_or\_start\(\) helper
* Vacuum: Additional error descriptions
* Yeelight Bedside: Fix set\_name and set\_color\_temp
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.4...0.4.5)
**Fixed bugs:**
- miio-extract-tokens raises a TypeError when running against extracted SQLite database [\#467](https://github.com/rytilahti/python-miio/issues/467)
- Do not crash on last\_clean\_details when no history available [\#457](https://github.com/rytilahti/python-miio/issues/457)
- install-sound command not working on Xiaowa vacuum \(roborock.vacuum.c1 v1.3.0\) [\#418](https://github.com/rytilahti/python-miio/issues/418)
- DeviceError code -30001 \(Resp Invalid JSON\) - Philips Bulb [\#205](https://github.com/rytilahti/python-miio/issues/205)
**Closed issues:**
- Issues adding roborock s50 vacuum to HA and controlling from mirobo [\#456](https://github.com/rytilahti/python-miio/issues/456)
- Support for chuangmi plug m3 [\#454](https://github.com/rytilahti/python-miio/issues/454)
- Xiaomi Phillips Smart LED Ball Lamp and API token for Home Assistant \(yaml\) [\#445](https://github.com/rytilahti/python-miio/issues/445)
- xiaomi ir control [\#444](https://github.com/rytilahti/python-miio/issues/444)
- Mirobo does not start on raspberry pi [\#442](https://github.com/rytilahti/python-miio/issues/442)
- Add mi band 3 watch to your library [\#441](https://github.com/rytilahti/python-miio/issues/441)
- Unsupported Device: chuangmi.plug.hmi205 [\#440](https://github.com/rytilahti/python-miio/issues/440)
- Air Purifier zhimi.airpurifier.m1 set\_mode isn't working [\#436](https://github.com/rytilahti/python-miio/issues/436)
- Can't make it work in a Domoticz plugin [\#433](https://github.com/rytilahti/python-miio/issues/433)
- chuangmi.plug.hmi205 unsupported device [\#427](https://github.com/rytilahti/python-miio/issues/427)
- Some devices not responding across subnets. [\#422](https://github.com/rytilahti/python-miio/issues/422)
**Merged pull requests:**
- Add missing error description [\#483](https://github.com/rytilahti/python-miio/pull/483) ([oncleben31](https://github.com/oncleben31))
- Enable the night mode \(scene 6\) by calling "go\_night" [\#481](https://github.com/rytilahti/python-miio/pull/481) ([syssi](https://github.com/syssi))
- Philips Moonlight: Support up to 6 fixed scenes [\#478](https://github.com/rytilahti/python-miio/pull/478) ([syssi](https://github.com/syssi))
- Remove duplicate paragraph about "Tokens from Mi Home logs" [\#477](https://github.com/rytilahti/python-miio/pull/477) ([syssi](https://github.com/syssi))
- Make android\_backup an optional dependency [\#476](https://github.com/rytilahti/python-miio/pull/476) ([rytilahti](https://github.com/rytilahti))
- Drop pretty\_cron dependency [\#475](https://github.com/rytilahti/python-miio/pull/475) ([rytilahti](https://github.com/rytilahti))
- Vacuum: add resume\_zoned\_clean\(\) and resume\_or\_start\(\) helper [\#473](https://github.com/rytilahti/python-miio/pull/473) ([rytilahti](https://github.com/rytilahti))
- Check for empty clean\_history instead of crashing on it [\#472](https://github.com/rytilahti/python-miio/pull/472) ([rytilahti](https://github.com/rytilahti))
- Fix miio-extract-tokens for devices without tokens [\#469](https://github.com/rytilahti/python-miio/pull/469) ([domibarton](https://github.com/domibarton))
- Rename speed property to avoid a name clash at HA [\#466](https://github.com/rytilahti/python-miio/pull/466) ([syssi](https://github.com/syssi))
- Corrected link in discovery.rst and Xiaomi Air Purifier Pro fix [\#465](https://github.com/rytilahti/python-miio/pull/465) ([swiergot](https://github.com/swiergot))
- New method to obtain tokens [\#464](https://github.com/rytilahti/python-miio/pull/464) ([swiergot](https://github.com/swiergot))
- Add a troubleshooting entry for vacuum timeouts [\#463](https://github.com/rytilahti/python-miio/pull/463) ([rytilahti](https://github.com/rytilahti))
- Extend miio-extract-tokens to allow extraction from yeelight app db [\#462](https://github.com/rytilahti/python-miio/pull/462) ([rytilahti](https://github.com/rytilahti))
- Docs for token extraction for Apple users [\#460](https://github.com/rytilahti/python-miio/pull/460) ([domibarton](https://github.com/domibarton))
- Add troubleshooting guide for cross-subnet communications [\#459](https://github.com/rytilahti/python-miio/pull/459) ([domibarton](https://github.com/domibarton))
- Sphinx config fix [\#458](https://github.com/rytilahti/python-miio/pull/458) ([domibarton](https://github.com/domibarton))
- Add Xiaomi Chuangmi Plug M3 support \(Closes: \#454\) [\#455](https://github.com/rytilahti/python-miio/pull/455) ([syssi](https://github.com/syssi))
- Add a "Reviewed by Hound" badge [\#453](https://github.com/rytilahti/python-miio/pull/453) ([salbertson](https://github.com/salbertson))
- Air Humidifier: Add set\_led method [\#451](https://github.com/rytilahti/python-miio/pull/451) ([syssi](https://github.com/syssi))
- Air Humidifier CA1: Fix led brightness command [\#450](https://github.com/rytilahti/python-miio/pull/450) ([syssi](https://github.com/syssi))
- Handle "resp invalid json" error \(Closes: \#205\) [\#449](https://github.com/rytilahti/python-miio/pull/449) ([syssi](https://github.com/syssi))
- Air Conditioning Partner: Extract the return value of the plug\_state request properly [\#448](https://github.com/rytilahti/python-miio/pull/448) ([syssi](https://github.com/syssi))
- Expose power\_socket property at AirConditioningCompanionStatus.\_\_repr\_\_\(\) [\#447](https://github.com/rytilahti/python-miio/pull/447) ([syssi](https://github.com/syssi))
- Air Conditioning Companion: Fix some conversion issues [\#446](https://github.com/rytilahti/python-miio/pull/446) ([syssi](https://github.com/syssi))
- Add support v7 version for Xiaomi AirPurifier PRO [\#443](https://github.com/rytilahti/python-miio/pull/443) ([quamilek](https://github.com/quamilek))
- Add control for persistent maps, no-go zones and barriers [\#438](https://github.com/rytilahti/python-miio/pull/438) ([rytilahti](https://github.com/rytilahti))
- Moonlight: Fix parameters of the set\_rgb api call [\#435](https://github.com/rytilahti/python-miio/pull/435) ([syssi](https://github.com/syssi))
- yeelight bedside: fix set\_name and set\_color\_temp [\#434](https://github.com/rytilahti/python-miio/pull/434) ([rytilahti](https://github.com/rytilahti))
- AC Partner: Add swing mode 7 with unknown meaning [\#431](https://github.com/rytilahti/python-miio/pull/431) ([syssi](https://github.com/syssi))
- Philips Moonlight: Align signature of set\_brightness\_and\_rgb [\#430](https://github.com/rytilahti/python-miio/pull/430) ([syssi](https://github.com/syssi))
- Add support for next generation of the Xiaomi Mi Smart Plug [\#428](https://github.com/rytilahti/python-miio/pull/428) ([syssi](https://github.com/syssi))
- Add Xiaomi Air Quality Monitor 2gen \(cgllc.airmonitor.b1\) support [\#420](https://github.com/rytilahti/python-miio/pull/420) ([syssi](https://github.com/syssi))
- Add initial support for aqara camera \(lumi.camera.aq1\) [\#375](https://github.com/rytilahti/python-miio/pull/375) ([rytilahti](https://github.com/rytilahti))
## [0.4.4](https://github.com/rytilahti/python-miio/tree/0.4.4) (2018-12-03)
This release adds support for the following new devices:
* Air Purifier 2s
* Vacuums roborock.vacuum.e2 and roborock.vacuum.c1 (limited features, sound packs are known not to be working)
Fixes & Enhancements:
* AC Partner V3: Add socket support
* AC Parner & AirHumidifer: improved autodetection
* Cooker: fixed model confusion
* Vacuum: add last_clean_details() to directly access the information from latest cleaning
* Yeelight: RGB support
* Waterpurifier: improved support
API changes:
* Vacuum: returning a list for clean_details() is deprecated and to be removed in the future.
* Philips Moonlight: RGB values are expected and delivered as tuples instead of an integer
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.3...0.4.4)
**Implemented enhancements:**
- Not working with Rockrobo Xiaowa \(roborock.vacuum.e2\) [\#364](https://github.com/rytilahti/python-miio/issues/364)
- Support for new vacuum model Xiaowa E20 [\#348](https://github.com/rytilahti/python-miio/issues/348)
**Fixed bugs:**
- No working with Xiaowa \(roborock.vacuum.c1 v1.3.0\) [\#370](https://github.com/rytilahti/python-miio/issues/370)
- Send multiple params broken result [\#73](https://github.com/rytilahti/python-miio/issues/73)
**Closed issues:**
- Add lumi.gateway.aqhm01 as unsuppported gateway [\#424](https://github.com/rytilahti/python-miio/issues/424)
- Unsupported device zhimi.airpurifier.mc1 [\#403](https://github.com/rytilahti/python-miio/issues/403)
- xiaomi repeater v1 [\#396](https://github.com/rytilahti/python-miio/issues/396)
- Control Air Conditioner Companion like Xiaomi Mi Smart WiFi Socket [\#337](https://github.com/rytilahti/python-miio/issues/337)
**Merged pull requests:**
- Improve discovery a specific device models [\#421](https://github.com/rytilahti/python-miio/pull/421) ([syssi](https://github.com/syssi))
- Fix PEP8 lint issue: unexpected spaces around keyword / parameter equals [\#416](https://github.com/rytilahti/python-miio/pull/416) ([syssi](https://github.com/syssi))
- AC Partner V3: Add socket support \(Closes \#337\) [\#415](https://github.com/rytilahti/python-miio/pull/415) ([syssi](https://github.com/syssi))
- Moonlight: Provide property rgb as tuple [\#414](https://github.com/rytilahti/python-miio/pull/414) ([syssi](https://github.com/syssi))
- fix last\_clean\_details to return the latest, not the oldest [\#413](https://github.com/rytilahti/python-miio/pull/413) ([rytilahti](https://github.com/rytilahti))
- generate docs for more modules [\#412](https://github.com/rytilahti/python-miio/pull/412) ([rytilahti](https://github.com/rytilahti))
- Use pause instead of stop for home command [\#411](https://github.com/rytilahti/python-miio/pull/411) ([rytilahti](https://github.com/rytilahti))
- Add .readthedocs.yml [\#410](https://github.com/rytilahti/python-miio/pull/410) ([rytilahti](https://github.com/rytilahti))
- Fix serial number reporting for some devices, add locale command [\#409](https://github.com/rytilahti/python-miio/pull/409) ([rytilahti](https://github.com/rytilahti))
- Force parameters to be an empty list if none is given [\#408](https://github.com/rytilahti/python-miio/pull/408) ([rytilahti](https://github.com/rytilahti))
- Cooker: Fix mixed model name [\#406](https://github.com/rytilahti/python-miio/pull/406) ([syssi](https://github.com/syssi))
- Waterpurifier: Divide properties into multiple requests \(Closes: \#73\) [\#405](https://github.com/rytilahti/python-miio/pull/405) ([syssi](https://github.com/syssi))
- Add Xiaomi Air Purifier 2s support [\#404](https://github.com/rytilahti/python-miio/pull/404) ([syssi](https://github.com/syssi))
- Fixed typo in log message [\#402](https://github.com/rytilahti/python-miio/pull/402) ([microraptor](https://github.com/microraptor))
## [0.4.3](https://github.com/rytilahti/python-miio/tree/0.4.3)
This is a bugfix release which provides improved compatibility.
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.2...0.4.3)
**Closed issues:**
- unsupported device zhimi airmonitor v1 [\#393](https://github.com/rytilahti/python-miio/issues/393)
- Unsupported device found: chuangmi.ir.v2 [\#392](https://github.com/rytilahti/python-miio/issues/392)
- TypeError: not all arguments converted during string formatting [\#385](https://github.com/rytilahti/python-miio/issues/385)
- Status not worked for AirHumidifier CA1 [\#383](https://github.com/rytilahti/python-miio/issues/383)
- Xiaomi Rice Cooker Normal5: get\_prop only works if "all" properties are requested [\#380](https://github.com/rytilahti/python-miio/issues/380)
- python-construct-2.9.45 [\#374](https://github.com/rytilahti/python-miio/issues/374)
**Merged pull requests:**
- Update commands in manual [\#398](https://github.com/rytilahti/python-miio/pull/398) ([olskar](https://github.com/olskar))
- Add cli interface for yeelight devices [\#397](https://github.com/rytilahti/python-miio/pull/397) ([rytilahti](https://github.com/rytilahti))
- Add last\_clean\_details to return information from the last clean [\#395](https://github.com/rytilahti/python-miio/pull/395) ([rytilahti](https://github.com/rytilahti))
- Add discovery of the Xiaomi Air Quality Monitor \(PM2.5\) \(Closes: \#393\) [\#394](https://github.com/rytilahti/python-miio/pull/394) ([syssi](https://github.com/syssi))
- Add miiocli support for the Air Humidifier CA1 [\#391](https://github.com/rytilahti/python-miio/pull/391) ([syssi](https://github.com/syssi))
- Add property LED to the Xiaomi Air Fresh [\#390](https://github.com/rytilahti/python-miio/pull/390) ([syssi](https://github.com/syssi))
- Fix Cooker Normal5: get\_prop only works if "all" properties are requested \(Closes: \#380\) [\#389](https://github.com/rytilahti/python-miio/pull/389) ([syssi](https://github.com/syssi))
- Improve the support of the Air Humidifier CA1 \(Closes: \#383\) [\#388](https://github.com/rytilahti/python-miio/pull/388) ([syssi](https://github.com/syssi))
## [0.4.2](https://github.com/rytilahti/python-miio/tree/0.4.2)
This release removes the version pinning for "construct" library as its API has been stabilized and we don't want to force our downstreams for our version choices.
Another notable change is dropping the "mirobo" package which has been deprecated for a very long time, and everyone using it should have had converted to use "miio" already.
Furthermore the client tools work now with click's version 7+.
This release also changes the behavior of vacuum's `got_error` property to signal properly if an error has occured. The previous behavior was based on checking the state instead of the error number, which changed after an error to 'idle' after a short while.
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.1...0.4.2)
**Fixed bugs:**
- Zoned cleanup start and stops imediately [\#355](https://github.com/rytilahti/python-miio/issues/355)
**Closed issues:**
- STATE not supported: Updating, state\_code: 14 [\#381](https://github.com/rytilahti/python-miio/issues/381)
- cant get it to work with xiaomi robot vacuum cleaner s50 [\#378](https://github.com/rytilahti/python-miio/issues/378)
- airfresh problem [\#377](https://github.com/rytilahti/python-miio/issues/377)
- get device token is 000000000000000000 [\#366](https://github.com/rytilahti/python-miio/issues/366)
- Rockrobo firmware 3.3.9\_003254 [\#358](https://github.com/rytilahti/python-miio/issues/358)
- No response from the device on Xiaomi Roborock v2 [\#349](https://github.com/rytilahti/python-miio/issues/349)
- Information : Xiaomi Aqara Smart Camera Hack [\#347](https://github.com/rytilahti/python-miio/issues/347)
**Merged pull requests:**
- Fix click7 compatibility [\#387](https://github.com/rytilahti/python-miio/pull/387) ([rytilahti](https://github.com/rytilahti))
- Expand documentation for token from Android backup [\#382](https://github.com/rytilahti/python-miio/pull/382) ([sgtio](https://github.com/sgtio))
- vacuum's got\_error: compare against error code, not against the state [\#379](https://github.com/rytilahti/python-miio/pull/379) ([rytilahti](https://github.com/rytilahti))
- Add tqdm to requirements list [\#369](https://github.com/rytilahti/python-miio/pull/369) ([pluehne](https://github.com/pluehne))
- Improve repr format [\#368](https://github.com/rytilahti/python-miio/pull/368) ([syssi](https://github.com/syssi))
## [0.4.1](https://github.com/rytilahti/python-miio/tree/0.4.1)
This release provides support for some new devices, improved support of existing devices and various fixes.
New devices:
* Xiaomi Mijia Smartmi Fresh Air System Wall-Mounted (@syssi)
* Xiaomi Philips Zhirui Bedside Lamp (@syssi)
Improvements:
* Vacuum: Support of multiple zones for app\_zoned\_cleaning added (@ciB89)
* Fan: SA1 and ZA1 support added as well as various fixes and improvements (@syssi)
* Chuangmi Plug V3: Measurement unit of the power consumption fixed (@syssi)
* Air Humidifier: Strong mode property added (@syssi)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.4.0...0.4.1)
**Closed issues:**
- Xiaomi Rice Cooker component not working [\#365](https://github.com/rytilahti/python-miio/issues/365)
- vacuum refuses to answer if the access to internet is blocked [\#353](https://github.com/rytilahti/python-miio/issues/353)
- Xiaomi Philips Zhirui Bedside Lamp [\#351](https://github.com/rytilahti/python-miio/issues/351)
- Unable to get Xiaomi miplug working on HA [\#350](https://github.com/rytilahti/python-miio/issues/350)
- Error codes [\#346](https://github.com/rytilahti/python-miio/issues/346)
- miiocli plug does not show the USB power status [\#344](https://github.com/rytilahti/python-miio/issues/344)
- could you pls add support to gateway's functions of security and light? [\#340](https://github.com/rytilahti/python-miio/issues/340)
- miplug discover throws exception [\#339](https://github.com/rytilahti/python-miio/issues/339)
- miioclio: raw\_command\(\) got an unexpected keyword argument 'parameters' [\#335](https://github.com/rytilahti/python-miio/issues/335)
- qmi.powerstrip.v1 no longer working on 0.40 [\#334](https://github.com/rytilahti/python-miio/issues/334)
- Starting the vacuum clean up after remote control [\#235](https://github.com/rytilahti/python-miio/issues/235)
**Merged pull requests:**
- Fan: Fix broken model names [\#363](https://github.com/rytilahti/python-miio/pull/363) ([syssi](https://github.com/syssi))
- Xiaomi Mi Smart Pedestal Fan: Add ZA1 \(zimi.fan.za1\) support [\#362](https://github.com/rytilahti/python-miio/pull/362) ([syssi](https://github.com/syssi))
- ignore cli and test files from test coverage to get correct coverage percentage [\#361](https://github.com/rytilahti/python-miio/pull/361) ([rytilahti](https://github.com/rytilahti))
- Add Xiaomi Airfresh VA2 support [\#360](https://github.com/rytilahti/python-miio/pull/360) ([syssi](https://github.com/syssi))
- Add basic Philips Moonlight support \(Closes: \#351\) [\#359](https://github.com/rytilahti/python-miio/pull/359) ([syssi](https://github.com/syssi))
- Xiaomi Mi Smart Pedestal Fan: Add SA1 \(zimi.fan.sa1\) support [\#354](https://github.com/rytilahti/python-miio/pull/354) ([syssi](https://github.com/syssi))
- Fix "miplug discover" method \(Closes: \#339\) [\#342](https://github.com/rytilahti/python-miio/pull/342) ([syssi](https://github.com/syssi))
- Fix ChuangmiPlugStatus repr format [\#341](https://github.com/rytilahti/python-miio/pull/341) ([syssi](https://github.com/syssi))
- Chuangmi Plug V3: Fix measurement unit \(W\) of the power consumption \(load\_power\) [\#338](https://github.com/rytilahti/python-miio/pull/338) ([syssi](https://github.com/syssi))
- miiocli: Fix raw\_command parameters \(Closes: \#335\) [\#336](https://github.com/rytilahti/python-miio/pull/336) ([syssi](https://github.com/syssi))
- Fan: Fix a KeyError if button\_pressed isn't available [\#333](https://github.com/rytilahti/python-miio/pull/333) ([syssi](https://github.com/syssi))
- Fan: Add test for the natural speed setter [\#332](https://github.com/rytilahti/python-miio/pull/332) ([syssi](https://github.com/syssi))
- Fan: Divide the retrieval of properties into multiple requests [\#331](https://github.com/rytilahti/python-miio/pull/331) ([syssi](https://github.com/syssi))
- Support of multiple zones for app\_zoned\_cleaning [\#311](https://github.com/rytilahti/python-miio/pull/311) ([ciB89](https://github.com/ciB89))
- Air Humidifier: Strong mode property added and docstrings updated [\#300](https://github.com/rytilahti/python-miio/pull/300) ([syssi](https://github.com/syssi))
## [0.4.0](https://github.com/rytilahti/python-miio/tree/0.4.0)
The highlight of this release is a crisp, unified and scalable command line interface called `miiocli` (thanks @yawor). Each supported device of this library is already integrated.
New devices:
* Xiaomi Mi Smart Electric Rice Cooker (@syssi)
Improvements:
* Unified and scalable command line interface (@yawor)
* Air Conditioning Companion: Support for captured infrared commands added (@syssi)
* Air Conditioning Companion: LED property fixed (@syssi)
* Air Quality Monitor: Night mode added (@syssi)
* Chuangi Plug V3 support fixed (@syssi)
* Pedestal Fan: Improved support of both versions
* Power Strip: Both versions are fully supported now (@syssi)
* Vacuum: New commands app\_goto\_target and app\_zoned\_clean added (@ciB89)
* Vacuum: Carpet mode support (@rytilahti)
* WiFi Repeater: WiFi roaming and signal strange indicator added (@syssi)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.9...0.4.0)
**Implemented enhancements:**
- Extend the Air Quality Monitor PM2.5 support [\#283](https://github.com/rytilahti/python-miio/issues/283)
- Support for Xiaomi Mi Smart Electric Rice Cooker [\#282](https://github.com/rytilahti/python-miio/issues/282)
- Improved support of the Xiaomi Smart Fan [\#244](https://github.com/rytilahti/python-miio/issues/244)
- Extended support of the Philips LED Ceiling Lamp [\#209](https://github.com/rytilahti/python-miio/issues/209)
- Add JSON output for easier integration with other tools [\#98](https://github.com/rytilahti/python-miio/issues/98)
- Xiaomi Mi Water Purifier support [\#71](https://github.com/rytilahti/python-miio/issues/71)
- Xiaomi WiFi Speaker support [\#69](https://github.com/rytilahti/python-miio/issues/69)
- Air Quality Monitor: Full support of the night mode [\#294](https://github.com/rytilahti/python-miio/pull/294) ([syssi](https://github.com/syssi))
**Fixed bugs:**
- Unable to extract token from Android backup [\#138](https://github.com/rytilahti/python-miio/issues/138)
- Xiaomi-Philips Eyecare control fail [\#74](https://github.com/rytilahti/python-miio/issues/74)
- Working with water purifier [\#48](https://github.com/rytilahti/python-miio/issues/48)
**Closed issues:**
- miiocli: Provide an error message for unknown commands [\#327](https://github.com/rytilahti/python-miio/issues/327)
- miplug status crash [\#326](https://github.com/rytilahti/python-miio/issues/326)
- IR remote chuangmiir module [\#325](https://github.com/rytilahti/python-miio/issues/325)
- Qing Mi Smart Power Strip cannot be setup,device id is 04b8824e [\#318](https://github.com/rytilahti/python-miio/issues/318)
- I can not start mirobo [\#316](https://github.com/rytilahti/python-miio/issues/316)
- acpartner-v3 [\#312](https://github.com/rytilahti/python-miio/issues/312)
- Vacuum v1 new firmware [\#305](https://github.com/rytilahti/python-miio/issues/305)
- Xiaomi Power Strip V1 is unable to handle some V2 properties [\#302](https://github.com/rytilahti/python-miio/issues/302)
- TypeError: isinstance\(\) arg 2 must be a type or tuple of types [\#296](https://github.com/rytilahti/python-miio/issues/296)
- Extend the Power Strip support [\#286](https://github.com/rytilahti/python-miio/issues/286)
- when i try to send a command [\#277](https://github.com/rytilahti/python-miio/issues/277)
- Obtain token for given IP address [\#263](https://github.com/rytilahti/python-miio/issues/263)
- Unable to discover the device [\#259](https://github.com/rytilahti/python-miio/issues/259)
- xiaomi vaccum cleaner not responding [\#92](https://github.com/rytilahti/python-miio/issues/92)
- xiaomi vacuum, manual moving mode: duration definition incorrect [\#62](https://github.com/rytilahti/python-miio/issues/62)
**Merged pull requests:**
- Chuangmi Plug V3: Make a local copy of the available properties [\#330](https://github.com/rytilahti/python-miio/pull/330) ([syssi](https://github.com/syssi))
- miiocli: Handle unknown commands \(Closes: \#327\) [\#329](https://github.com/rytilahti/python-miio/pull/329) ([syssi](https://github.com/syssi))
- Fix a name clash of click\_common and the argument "command" [\#328](https://github.com/rytilahti/python-miio/pull/328) ([syssi](https://github.com/syssi))
- Update README [\#324](https://github.com/rytilahti/python-miio/pull/324) ([syssi](https://github.com/syssi))
- Migrate miplug cli to the new ChuangmiPlug class \(Fixes: \#296\) [\#323](https://github.com/rytilahti/python-miio/pull/323) ([syssi](https://github.com/syssi))
- Link to the Home Assistant custom component "xiaomi\_cooker" added [\#320](https://github.com/rytilahti/python-miio/pull/320) ([syssi](https://github.com/syssi))
- Improve the Xiaomi Rice Cooker support [\#319](https://github.com/rytilahti/python-miio/pull/319) ([syssi](https://github.com/syssi))
- Air Conditioning Companion: Rewrite a captured command before replay [\#317](https://github.com/rytilahti/python-miio/pull/317) ([syssi](https://github.com/syssi))
- Air Conditioning Companion: Led property fixed [\#315](https://github.com/rytilahti/python-miio/pull/315) ([syssi](https://github.com/syssi))
- mDNS names of the cooker fixed [\#314](https://github.com/rytilahti/python-miio/pull/314) ([syssi](https://github.com/syssi))
- mDNS names of the Air Conditioning Companion \(AC partner\) added [\#313](https://github.com/rytilahti/python-miio/pull/313) ([syssi](https://github.com/syssi))
- Added new commands app\_goto\_target and app\_zoned\_clean [\#310](https://github.com/rytilahti/python-miio/pull/310) ([ciB89](https://github.com/ciB89))
- Link to the Home Assistant custom component "xiaomi\_raw" added [\#309](https://github.com/rytilahti/python-miio/pull/309) ([syssi](https://github.com/syssi))
- Improved support of the Xiaomi Smart Fan [\#306](https://github.com/rytilahti/python-miio/pull/306) ([syssi](https://github.com/syssi))
- mDNS discovery: Xiaomi Smart Fans added [\#304](https://github.com/rytilahti/python-miio/pull/304) ([syssi](https://github.com/syssi))
- Xiaomi Power Strip V1 is unable to handle some V2 properties [\#303](https://github.com/rytilahti/python-miio/pull/303) ([syssi](https://github.com/syssi))
- mDNS discovery: Additional Philips Candle Light added [\#301](https://github.com/rytilahti/python-miio/pull/301) ([syssi](https://github.com/syssi))
- Add support for vacuum's carpet mode, which requires a recent firmware version [\#299](https://github.com/rytilahti/python-miio/pull/299) ([rytilahti](https://github.com/rytilahti))
- Air Conditioning Companion: Extended parsing of model and state [\#297](https://github.com/rytilahti/python-miio/pull/297) ([syssi](https://github.com/syssi))
- Air Quality Monitor: Type and payload example of the time\_state property updated [\#293](https://github.com/rytilahti/python-miio/pull/293) ([syssi](https://github.com/syssi))
- WiFi Speaker support improved [\#291](https://github.com/rytilahti/python-miio/pull/291) ([syssi](https://github.com/syssi))
- Imports optimized [\#290](https://github.com/rytilahti/python-miio/pull/290) ([syssi](https://github.com/syssi))
- Support of the unified command line interface for all devices [\#289](https://github.com/rytilahti/python-miio/pull/289) ([syssi](https://github.com/syssi))
- Power Strip support extended by additional attributes [\#288](https://github.com/rytilahti/python-miio/pull/288) ([syssi](https://github.com/syssi))
- Basic support for Xiaomi Mi Smart Electric Rice Cooker [\#287](https://github.com/rytilahti/python-miio/pull/287) ([syssi](https://github.com/syssi))
- WiFi Repeater: Wifi roaming and signal strange indicator added [\#285](https://github.com/rytilahti/python-miio/pull/285) ([syssi](https://github.com/syssi))
- Preparation of release 0.3.9 [\#281](https://github.com/rytilahti/python-miio/pull/281) ([syssi](https://github.com/syssi))
- Unified and scalable command line interface [\#191](https://github.com/rytilahti/python-miio/pull/191) ([yawor](https://github.com/yawor))
## [0.3.9](https://github.com/rytilahti/python-miio/tree/0.3.9)
This release provides support for some new devices, improved support of existing devices and various fixes.
New devices:
* Xiaomi Mi WiFi Repeater 2 (@syssi)
* Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp (@syssi)
Improvements:
* Repr of the AirPurifierStatus fixed (@sq5gvm)
* Chuangmi Plug V1, V2, V3 and M1 merged into a common class (@syssi)
* Water Purifier: Some properties added (@syssi)
* Air Conditioning Companion: LED status fixed (@syssi)
* Air Conditioning Companion: Target temperature property renamed (@syssi)
* Air Conditioning Companion: Swing mode property returns the enum now (@syssi)
* Move some generic util functions from vacuumcontainers to utils module (@rytilahti)
* Construct version bumped (@syssi)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.8...0.3.9)
**Closed issues:**
- Xiaomi Mi WiFi Amplifier 2 support [\#275](https://github.com/rytilahti/python-miio/issues/275)
- TypeError: not enough arguments for format string in airpurifier.py [\#264](https://github.com/rytilahti/python-miio/issues/264)
- Issue vaccum gen 2 - HA 0.64 -\> 0.65 Python 3.6.0 -\> 3.7.0 [\#261](https://github.com/rytilahti/python-miio/issues/261)
- Add support for Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp [\#243](https://github.com/rytilahti/python-miio/issues/243)
- Basic support for the Yeelight LED Ceiling Lamp v4 [\#240](https://github.com/rytilahti/python-miio/issues/240)
- from Construct developer, a note [\#222](https://github.com/rytilahti/python-miio/issues/222)
**Merged pull requests:**
- construct version bumped [\#280](https://github.com/rytilahti/python-miio/pull/280) ([syssi](https://github.com/syssi))
- Support for the Xiaomi Mi WiFi Repeater 2 added [\#278](https://github.com/rytilahti/python-miio/pull/278) ([syssi](https://github.com/syssi))
- Move some generic util functions from vacuumcontainers to utils module [\#276](https://github.com/rytilahti/python-miio/pull/276) ([rytilahti](https://github.com/rytilahti))
- Air Conditioning Companion: Swing mode property returns the enum now [\#274](https://github.com/rytilahti/python-miio/pull/274) ([syssi](https://github.com/syssi))
- Air Conditioning Companion: Target temperature property properly named [\#273](https://github.com/rytilahti/python-miio/pull/273) ([syssi](https://github.com/syssi))
- Air Conditioning Companion: LED status fixed [\#272](https://github.com/rytilahti/python-miio/pull/272) ([syssi](https://github.com/syssi))
- Water Purifier: Some properties added [\#271](https://github.com/rytilahti/python-miio/pull/271) ([syssi](https://github.com/syssi))
- Merge of the Chuangmi Plug V1, V2, V3 and M1 [\#270](https://github.com/rytilahti/python-miio/pull/270) ([syssi](https://github.com/syssi))
- Improve test coverage [\#269](https://github.com/rytilahti/python-miio/pull/269) ([syssi](https://github.com/syssi))
- Support for Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp [\#268](https://github.com/rytilahti/python-miio/pull/268) ([syssi](https://github.com/syssi))
- Air Purifier: Duplicate property removed from \_\_repr\_\_ [\#267](https://github.com/rytilahti/python-miio/pull/267) ([syssi](https://github.com/syssi))
- Tests for reprs of the status classes [\#266](https://github.com/rytilahti/python-miio/pull/266) ([syssi](https://github.com/syssi))
- Repr of the AirPurifierStatus fixed [\#265](https://github.com/rytilahti/python-miio/pull/265) ([sq5gvm](https://github.com/sq5gvm))
## [0.3.8](https://github.com/rytilahti/python-miio/tree/0.3.8)
Goodbye Python 3.4! This release marks end of support for python versions older than 3.5, paving a way for cleaner code and a nicer API for a future asyncio support. Highlights of this release:
* Support for several new devices, improvements to existing devices and various fixes thanks to @syssi.
* Firmware updates for vacuums (@rytilahti), the most prominent use case being installing custom firmwares (e.g. for rooting your device). Installing sound packs is also streamlined with a self-hosting server.
* The protocol quirks handling was extended to handle invalid messages from the cloud (thanks @jschmer), improving interoperability for Dustcloud.
New devices:
* Chuangmi Plug V3 (@syssi)
* Xiaomi Air Humidifier CA (@syssi)
* Xiaomi Air Purifier V3 (@syssi)
* Xiaomi Philips LED Ceiling Light 620mm (@syssi)
Improvements:
* Provide the mac address as property of the device info (@syssi)
* Air Purifier: button_pressed property added (@syssi)
* Generalize and move configure\_wifi to the Device class (@rytilahti)
* Power Strip: The wifi led and power price can be controlled now (@syssi)
* Try to fix decrypted payload quirks if it fails to parse as json (@jschmer)
* Air Conditioning Companion: Turn on/off and LED property added, load power fixed (@syssi)
* Strict check for version equality of construct (@arekbulski)
* Firmware update functionality (@rytilahti)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.7...0.3.8)
**Closed issues:**
- Can't retrieve token from Android app [\#246](https://github.com/rytilahti/python-miio/issues/246)
- Unsupported device found! chuangmi.ir.v2 [\#242](https://github.com/rytilahti/python-miio/issues/242)
- Improved support of the Air Humidifier [\#241](https://github.com/rytilahti/python-miio/issues/241)
- Add support for the Xiaomi Philips LED Ceiling Light 620mm \(philips.light.zyceiling\) [\#234](https://github.com/rytilahti/python-miio/issues/234)
- Support Xiaomi Air Purifier v3 [\#231](https://github.com/rytilahti/python-miio/issues/231)
**Merged pull requests:**
- Add --ip for install\_sound, update\_firmware & update docs [\#262](https://github.com/rytilahti/python-miio/pull/262) ([rytilahti](https://github.com/rytilahti))
- Provide the mac address as property of the device info [\#260](https://github.com/rytilahti/python-miio/pull/260) ([syssi](https://github.com/syssi))
- Tests: Non-essential code removed [\#258](https://github.com/rytilahti/python-miio/pull/258) ([syssi](https://github.com/syssi))
- Support of the Chuangmi Plug V3 [\#257](https://github.com/rytilahti/python-miio/pull/257) ([syssi](https://github.com/syssi))
- Air Purifier V3: Response example updated [\#255](https://github.com/rytilahti/python-miio/pull/255) ([syssi](https://github.com/syssi))
- Support of the Air Purifier V3 added \(Closes: \#231\) [\#254](https://github.com/rytilahti/python-miio/pull/254) ([syssi](https://github.com/syssi))
- Air Purifier: Property "button\_pressed" added [\#253](https://github.com/rytilahti/python-miio/pull/253) ([syssi](https://github.com/syssi))
- Respond with an error after the retry counter is down to zero, log retries into debug logger [\#252](https://github.com/rytilahti/python-miio/pull/252) ([rytilahti](https://github.com/rytilahti))
- Drop python 3.4 support, which paves a way for nicer API for asyncio among other things [\#251](https://github.com/rytilahti/python-miio/pull/251) ([rytilahti](https://github.com/rytilahti))
- Generalize and move configure\_wifi to the Device class [\#250](https://github.com/rytilahti/python-miio/pull/250) ([rytilahti](https://github.com/rytilahti))
- Support of the Xiaomi Air Humidifier CA \(zhimi.humidifier.ca1\) [\#249](https://github.com/rytilahti/python-miio/pull/249) ([syssi](https://github.com/syssi))
- Xiaomi AC Companion: LED property added [\#248](https://github.com/rytilahti/python-miio/pull/248) ([syssi](https://github.com/syssi))
- Some misleading docstrings updated [\#245](https://github.com/rytilahti/python-miio/pull/245) ([syssi](https://github.com/syssi))
- Powerstrip support improved [\#239](https://github.com/rytilahti/python-miio/pull/239) ([syssi](https://github.com/syssi))
- Repr of the AirQualityMonitorStatus fixed [\#238](https://github.com/rytilahti/python-miio/pull/238) ([syssi](https://github.com/syssi))
- mDNS discovery: Additional philips light added [\#237](https://github.com/rytilahti/python-miio/pull/237) ([syssi](https://github.com/syssi))
- Try to fix decrypted payload quirks if it fails to parse as json [\#236](https://github.com/rytilahti/python-miio/pull/236) ([jschmer](https://github.com/jschmer))
- Device support of the Xiaomi Air Conditioning Companion improved [\#233](https://github.com/rytilahti/python-miio/pull/233) ([syssi](https://github.com/syssi))
- Construct related, strict check for version equality [\#232](https://github.com/rytilahti/python-miio/pull/232) ([arekbulski](https://github.com/arekbulski))
- Implement firmware update functionality [\#153](https://github.com/rytilahti/python-miio/pull/153) ([rytilahti](https://github.com/rytilahti))
## [0.3.7](https://github.com/rytilahti/python-miio/tree/0.3.7)
This is a bugfix release which provides improved stability and compatibility.
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.6...0.3.7)
**Closed issues:**
- construct.core.StreamError: could not write bytes, expected 4, found 8 [\#227](https://github.com/rytilahti/python-miio/issues/227)
- yeelink.light.color1 unsupported [\#225](https://github.com/rytilahti/python-miio/issues/225)
- Cant decode token \(invalid start byte\) [\#224](https://github.com/rytilahti/python-miio/issues/224)
- from Construct developer, a note [\#222](https://github.com/rytilahti/python-miio/issues/222)
**Merged pull requests:**
- Proper handling of the device\_id representation [\#228](https://github.com/rytilahti/python-miio/pull/228) ([syssi](https://github.com/syssi))
- Construct related, support upto 2.9.31 [\#226](https://github.com/rytilahti/python-miio/pull/226) ([arekbulski](https://github.com/arekbulski))
## [0.3.6](https://github.com/rytilahti/python-miio/tree/0.3.6)
This is a bugfix release because of further breaking changes of the underlying library construct.
Improvements:
* Lazy discovery on demand (@syssi)
* Support of construct 2.9.23 to 2.9.30 (@yawor, @syssi)
* Avoid device crash on wrap around of the sequence number (@syssi)
* Extended support of the Philips Ceiling Lamp (@syssi)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.5...0.3.6)
**Closed issues:**
- Unable to discover a device [\#217](https://github.com/rytilahti/python-miio/issues/217)
- AirPurifier set\_mode [\#213](https://github.com/rytilahti/python-miio/issues/213)
- Construct 2.9.28 breaks the Chuangmi IR packet assembly [\#212](https://github.com/rytilahti/python-miio/issues/212)
- Set mode for Air Purifier 2 not working [\#207](https://github.com/rytilahti/python-miio/issues/207)
- Trying to get map data without rooting [\#206](https://github.com/rytilahti/python-miio/issues/206)
- Unknown miio device found [\#204](https://github.com/rytilahti/python-miio/issues/204)
- Supporting raw and pronto optional parameter without type specifier. [\#199](https://github.com/rytilahti/python-miio/issues/199)
**Merged pull requests:**
- Fixes for the API change of construct v2.9.30 [\#220](https://github.com/rytilahti/python-miio/pull/220) ([syssi](https://github.com/syssi))
- Philips Ceiling Lamp: New setter "bricct" added [\#216](https://github.com/rytilahti/python-miio/pull/216) ([syssi](https://github.com/syssi))
- Lazy discovery on demand [\#215](https://github.com/rytilahti/python-miio/pull/215) ([syssi](https://github.com/syssi))
- Chuangmi IR: Fix Construct 2.9.28 regression [\#214](https://github.com/rytilahti/python-miio/pull/214) ([yawor](https://github.com/yawor))
- Philips Bulb crashs if \_id is 0 [\#211](https://github.com/rytilahti/python-miio/pull/211) ([syssi](https://github.com/syssi))
## [0.3.5](https://github.com/rytilahti/python-miio/tree/0.3.5)
This release provides major improvements for various supported devices. Special thanks goes to @yawor for his awesome work!
Additionally, a compatibility issue when using construct version 2.9.23 and greater -- causing timeouts and inability to control devices -- has been fixed again.
Device errors are now wrapped in a exception (DeviceException) for easier handling.
New devices:
* Air Purifier: Some additional models added to the list of supported and discovered devices by mDNS (@syssi)
* Air Humidifier CA added to the list of supported and discovered devices by mDNS (@syssi)
Improvements:
* Air Conditioning Companion: Extended device support (@syssi)
* Air Humidifier: Device support tested and improved (@syssi)
* Air Purifier Pro: Second motor speed and filter type detection added (@yawor)
* Air Purifier: Some additional properties added (@syssi)
* Air Quality Monitor: Additional property "time_state" added (@syssi)
* Revise error handling to be more consistent for library users (@rytilahti)
* Chuangmi IR: Ability to send any Pronto Hex encoded IR command added (@yawor)
* Chuangmi IR: Command type autodetection added (@yawor)
* Philips Bulb: New command "bricct" added (@syssi)
* Command line interface: Make discovery to work with no IP addr and token, courtesy of @M0ses (@rytilahti)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.4...0.3.5)
**Fixed bugs:**
- TypeError: build\(\) takes 2 positional arguments but 3 were given [\#201](https://github.com/rytilahti/python-miio/issues/201)
- Error on build message [\#197](https://github.com/rytilahti/python-miio/issues/197)
**Closed issues:**
- Control Air purifier and Humidifier? [\#177](https://github.com/rytilahti/python-miio/issues/177)
- Construct error, "subcon should be a Construct field" [\#167](https://github.com/rytilahti/python-miio/issues/167)
**Merged pull requests:**
- mDNS discovery: Additional air humidifier model \(zhimi-humidifier-ca1\) added [\#200](https://github.com/rytilahti/python-miio/pull/200) ([syssi](https://github.com/syssi))
- Make discovery to work with no IP addr and token, courtesy of M0ses [\#198](https://github.com/rytilahti/python-miio/pull/198) ([rytilahti](https://github.com/rytilahti))
- Minimum supported version of construct specified [\#196](https://github.com/rytilahti/python-miio/pull/196) ([syssi](https://github.com/syssi))
- Chuangmi IR command type autodetection [\#195](https://github.com/rytilahti/python-miio/pull/195) ([yawor](https://github.com/yawor))
- Point hound-ci to the flake8 configuration. Second try. [\#193](https://github.com/rytilahti/python-miio/pull/193) ([syssi](https://github.com/syssi))
- Fix a breaking change of construct 2.9.23 [\#192](https://github.com/rytilahti/python-miio/pull/192) ([syssi](https://github.com/syssi))
- Air Purifier: SleepMode enum added. SleepMode isn't a subset of OperationMode [\#190](https://github.com/rytilahti/python-miio/pull/190) ([syssi](https://github.com/syssi))
- Point hound-ci to the flake8 configuration [\#189](https://github.com/rytilahti/python-miio/pull/189) ([syssi](https://github.com/syssi))
- Features of mixed air purifier models added [\#188](https://github.com/rytilahti/python-miio/pull/188) ([syssi](https://github.com/syssi))
- Air Quality Monitor: New property "time\_state" added [\#187](https://github.com/rytilahti/python-miio/pull/187) ([syssi](https://github.com/syssi))
- Philips Bulb: New setter "bricct" added [\#186](https://github.com/rytilahti/python-miio/pull/186) ([syssi](https://github.com/syssi))
- Tests for the Chuangmi IR controller [\#184](https://github.com/rytilahti/python-miio/pull/184) ([syssi](https://github.com/syssi))
- Chuangmi IR: Add ability to send any Pronto Hex encoded IR command. [\#183](https://github.com/rytilahti/python-miio/pull/183) ([yawor](https://github.com/yawor))
- Tests for the Xiaomi Air Conditioning Companion [\#182](https://github.com/rytilahti/python-miio/pull/182) ([syssi](https://github.com/syssi))
- Flake8 configuration updated [\#181](https://github.com/rytilahti/python-miio/pull/181) ([syssi](https://github.com/syssi))
- Revise error handling to be more consistent for library users [\#180](https://github.com/rytilahti/python-miio/pull/180) ([rytilahti](https://github.com/rytilahti))
- All device specific exceptions should derive from DeviceException [\#179](https://github.com/rytilahti/python-miio/pull/179) ([syssi](https://github.com/syssi))
- Air Purifier Pro second motor speed [\#176](https://github.com/rytilahti/python-miio/pull/176) ([yawor](https://github.com/yawor))
- Tests of the Air Purifier improved [\#174](https://github.com/rytilahti/python-miio/pull/174) ([syssi](https://github.com/syssi))
- New properties of the Xiaomi Air Humidifier added [\#173](https://github.com/rytilahti/python-miio/pull/173) ([syssi](https://github.com/syssi))
- Return type of the property "volume" should be Optional [\#172](https://github.com/rytilahti/python-miio/pull/172) ([syssi](https://github.com/syssi))
- Missing dependency "appdirs" added [\#171](https://github.com/rytilahti/python-miio/pull/171) ([syssi](https://github.com/syssi))
- Xiaomi Air Humidifier: Unavailable property "led" removed. [\#170](https://github.com/rytilahti/python-miio/pull/170) ([syssi](https://github.com/syssi))
- Extended Air Conditioning Companion support [\#169](https://github.com/rytilahti/python-miio/pull/169) ([syssi](https://github.com/syssi))
## [0.3.4](https://github.com/rytilahti/python-miio/tree/0.3.4)
The most significant change for this release is unbreaking the communication when using a recent versions of construct library (thanks to @syssi).
On top of that there are various smaller fixes and improvements, e.g. support for sound packs and running python-miio on Windows.
New devices:
* Air Purifier 2S added to the list of supported and discovered devices by mDNS (@harnash)
Improvements:
* Air Purifier Pro: support for sound volume level and illuminance sensor (@yawor)
* Vacuum: added sound pack handling and ability to change the sound volume (@rytilahti)
* Vacuum: better support for status information on the 2nd gen model (@hastarin)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.3...0.3.4)
**Fixed bugs:**
- Error with info command [\#156](https://github.com/rytilahti/python-miio/issues/156)
- Change hard coded /tmp to cross-platform tempfile [\#148](https://github.com/rytilahti/python-miio/issues/148)
**Closed issues:**
- mirobo vacuum sound volume control [\#159](https://github.com/rytilahti/python-miio/issues/159)
- wifi signal strength [\#155](https://github.com/rytilahti/python-miio/issues/155)
- xiaomi philips bulb & philips ceiling [\#151](https://github.com/rytilahti/python-miio/issues/151)
- Vaccum Timer / Timezone issue [\#149](https://github.com/rytilahti/python-miio/issues/149)
- Exception when displaying Power load using Plug CLI [\#144](https://github.com/rytilahti/python-miio/issues/144)
- Missing states and error\_codes [\#57](https://github.com/rytilahti/python-miio/issues/57)
**Merged pull requests:**
- Use appdirs' user\_cache\_dir for sequence file [\#165](https://github.com/rytilahti/python-miio/pull/165) ([rytilahti](https://github.com/rytilahti))
- Add a more helpful error message when info\(\) fails with an empty payload [\#164](https://github.com/rytilahti/python-miio/pull/164) ([rytilahti](https://github.com/rytilahti))
- Adding "Go to target" state description for Roborock S50. [\#163](https://github.com/rytilahti/python-miio/pull/163) ([hastarin](https://github.com/hastarin))
- Add ability to change the volume [\#162](https://github.com/rytilahti/python-miio/pull/162) ([rytilahti](https://github.com/rytilahti))
- Added Air Purifier 2S to supported devices [\#161](https://github.com/rytilahti/python-miio/pull/161) ([harnash](https://github.com/harnash))
- Modified to support zoned cleaning mode of Roborock S50. [\#160](https://github.com/rytilahti/python-miio/pull/160) ([hastarin](https://github.com/hastarin))
- Fix for a breaking change of construct 2.8.22 [\#158](https://github.com/rytilahti/python-miio/pull/158) ([syssi](https://github.com/syssi))
- Air Purifier Pro: support for sound volume level and fix for bright propery [\#157](https://github.com/rytilahti/python-miio/pull/157) ([yawor](https://github.com/yawor))
- Add preliminary support for managing sound files [\#154](https://github.com/rytilahti/python-miio/pull/154) ([rytilahti](https://github.com/rytilahti))
## [0.3.3](https://github.com/rytilahti/python-miio/tree/0.3.3)
This release brings support for Air Conditioner Companion along some improvements and an increase in the test coverage for future-proofing the code-base. Special thanks for this release goes to @syssi & to all new contributors!
A bug exposed in python-miio when using version 2.8.17 or newer of the underlying construct library -- causing timeouts and inability to control devices -- has also been fixed in this release.
New supported devices:
* Xiaomi Mi Home Air Conditioner Companion
Improvements:
* Mi Vacuum 2nd generation is now detected by discovery
* Air Purifier 2: expose additional properties
* Yeelight: parse RGB properly
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.2...0.3.3)
**Implemented enhancements:**
- Xiaomi Mi Home Air Conditioner Companion support [\#76](https://github.com/rytilahti/python-miio/issues/76)
**Closed issues:**
- Philip Eye Care Lamp Got error when receiving: timed out [\#146](https://github.com/rytilahti/python-miio/issues/146)
- Can't reach my mirobo [\#145](https://github.com/rytilahti/python-miio/issues/145)
- installiation problems [\#130](https://github.com/rytilahti/python-miio/issues/130)
- Unable to discover Xiaomi Philips LED Bulb [\#106](https://github.com/rytilahti/python-miio/issues/106)
- Xiaomi Mi Robot Vacuum 2nd support [\#90](https://github.com/rytilahti/python-miio/issues/90)
**Merged pull requests:**
- Update for Rock Robot \(Mi Robot gen 2\) [\#143](https://github.com/rytilahti/python-miio/pull/143) ([fanthos](https://github.com/fanthos))
- Unbreak the communication when using construct v2.8.17 [\#142](https://github.com/rytilahti/python-miio/pull/142) ([rytilahti](https://github.com/rytilahti))
- fix powerstate invalid [\#139](https://github.com/rytilahti/python-miio/pull/139) ([roiff](https://github.com/roiff))
- Unit tests for the Chuang Mi Plug V1 [\#137](https://github.com/rytilahti/python-miio/pull/137) ([syssi](https://github.com/syssi))
- Unit tests of the Xiaomi Power Strip extended [\#136](https://github.com/rytilahti/python-miio/pull/136) ([syssi](https://github.com/syssi))
- Unit tests for the Xiaomi Air Quality Monitor [\#135](https://github.com/rytilahti/python-miio/pull/135) ([syssi](https://github.com/syssi))
- Unit tests for the Xiaomi Air Humidifier [\#134](https://github.com/rytilahti/python-miio/pull/134) ([syssi](https://github.com/syssi))
- Unit tests for philips lights [\#133](https://github.com/rytilahti/python-miio/pull/133) ([syssi](https://github.com/syssi))
- Additional properties of the Xiaomi Air Purifier 2 introduced [\#132](https://github.com/rytilahti/python-miio/pull/132) ([syssi](https://github.com/syssi))
- Fix Yeelight RGB parsing [\#131](https://github.com/rytilahti/python-miio/pull/131) ([Sduniii](https://github.com/Sduniii))
- Xiaomi Air Conditioner Companion support [\#129](https://github.com/rytilahti/python-miio/pull/129) ([syssi](https://github.com/syssi))
- Fix manual\_control error message typo [\#127](https://github.com/rytilahti/python-miio/pull/127) ([skorokithakis](https://github.com/skorokithakis))
- bump to 0.3.2, add RELEASING.md for describing the process [\#126](https://github.com/rytilahti/python-miio/pull/126) ([rytilahti](https://github.com/rytilahti))
## [0.3.2](https://github.com/rytilahti/python-miio/tree/0.3.2)
This release includes small improvements for powerstrip and vacuum support.
Furthermore this is the first release with proper documentation.
Generated docs are available at https://python-miio.readthedocs.io - patches to improve them are more than welcome!
Improvements:
* Powerstrip: expose correct load power, works also now without cloud connectivity
* Vacuum: added ability to reset consumable states
* Vacuum: exposes time left before next sensor clean-up
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.1...0.3.2)
**Closed issues:**
- philips.light.ceiling Unsupported device found! [\#118](https://github.com/rytilahti/python-miio/issues/118)
- Xiaomi Philips ceiling light automation [\#116](https://github.com/rytilahti/python-miio/issues/116)
- Unsupported device found [\#112](https://github.com/rytilahti/python-miio/issues/112)
- PM2.5 Faster Readout [\#111](https://github.com/rytilahti/python-miio/issues/111)
**Merged pull requests:**
- add pure text LICENSE [\#125](https://github.com/rytilahti/python-miio/pull/125) ([rytilahti](https://github.com/rytilahti))
- Add GPLv3 license [\#124](https://github.com/rytilahti/python-miio/pull/124) ([pluehne](https://github.com/pluehne))
- Don’t require typing with Python 3.5 and newer [\#123](https://github.com/rytilahti/python-miio/pull/123) ([pluehne](https://github.com/pluehne))
- Powerstrip fixes [\#121](https://github.com/rytilahti/python-miio/pull/121) ([rytilahti](https://github.com/rytilahti))
- Added time left for recommended sensor cleaning [\#119](https://github.com/rytilahti/python-miio/pull/119) ([bbbenji](https://github.com/bbbenji))
- Load power of the PowerStrip fixed and removed from the Plug [\#117](https://github.com/rytilahti/python-miio/pull/117) ([syssi](https://github.com/syssi))
- Reset consumable by name [\#115](https://github.com/rytilahti/python-miio/pull/115) ([mrin](https://github.com/mrin))
- Model name of the Xiaomi Philips Ceiling Lamp updated [\#113](https://github.com/rytilahti/python-miio/pull/113) ([syssi](https://github.com/syssi))
- Update apidocs for sphinx-generated documentation, which follows at l… [\#93](https://github.com/rytilahti/python-miio/pull/93) ([rytilahti](https://github.com/rytilahti))
## [0.3.1](https://github.com/rytilahti/python-miio/tree/0.3.1) (2017-11-01)
New supported devices:
* Xioami Philips Smart LED Ball Lamp
Improvements:
* Vacuum: add ability to configure used wifi network
* Plug V1: improved discovery, add temperature reporting
* Airpurifier: setting of favorite level works now
* Eyecare: safer mapping of properties
Breaking:
* Strip has been renamed to PowerStrip to avoid confusion
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.3.0...0.3.1)
**Fixed bugs:**
- AirPurifier: set\_favorite\_level not working [\#103](https://github.com/rytilahti/python-miio/issues/103)
**Closed issues:**
- Unsupported device [\#108](https://github.com/rytilahti/python-miio/issues/108)
- Xiaomi Vacuum resume cleaning session from dock capability? [\#102](https://github.com/rytilahti/python-miio/issues/102)
**Merged pull requests:**
- Chuang Mi Plug V1: Property "temperature" added & discovery fixed [\#109](https://github.com/rytilahti/python-miio/pull/109) ([syssi](https://github.com/syssi))
- Add the ability to define a timezone for configure\_wifi [\#107](https://github.com/rytilahti/python-miio/pull/107) ([rytilahti](https://github.com/rytilahti))
- Make vacuum robot wifi settings configurable via CLI [\#105](https://github.com/rytilahti/python-miio/pull/105) ([infinitydev](https://github.com/infinitydev))
- API call set\_favorite\_level \(method: set\_level\_favorite\) updated [\#104](https://github.com/rytilahti/python-miio/pull/104) ([syssi](https://github.com/syssi))
- use upstream android\_backup [\#101](https://github.com/rytilahti/python-miio/pull/101) ([rytilahti](https://github.com/rytilahti))
- add some tests to vacuum [\#100](https://github.com/rytilahti/python-miio/pull/100) ([rytilahti](https://github.com/rytilahti))
- Add a base to allow easier testing of devices [\#99](https://github.com/rytilahti/python-miio/pull/99) ([rytilahti](https://github.com/rytilahti))
- Rename of Strip to PowerStrip to avoid confusion with led strips [\#97](https://github.com/rytilahti/python-miio/pull/97) ([syssi](https://github.com/syssi))
- Some typing hints added and the code order aligned [\#96](https://github.com/rytilahti/python-miio/pull/96) ([syssi](https://github.com/syssi))
- Philips Eyecare: More safety property mapping of the device status [\#95](https://github.com/rytilahti/python-miio/pull/95) ([syssi](https://github.com/syssi))
- Device support of the Xioami Philips Smart LED Ball Lamp [\#94](https://github.com/rytilahti/python-miio/pull/94) ([syssi](https://github.com/syssi))
## [0.3.0](https://github.com/rytilahti/python-miio/tree/0.3.0) (2017-10-21)
Good bye to python-mirobo, say hello to python-miio!
As the library is getting more mature and supports so many other devices besides the vacuum sporting the miIO protocol,
it was decided that the project deserves a new name.
The name python-miio was previously used by a fork of python-mirobo, and we are thankful to SchumyHao for releasing the name for us.
The old "mirobo" package will continue working (and is API compatible) for the foreseeable future,
however, developers using this package (if any) are encouraged to port their code over to use the the new "miio" package.
The old command-line tools remain as they are.
In order to simplify the initial configuration, a tool to extract tokens from a Mi Home's backup (Android) or its database (Apple, Android) is added. It will also decrypt the tokens if needed, a change which was introduced recently how they are stored in the database of iOS devices.
Improvements:
* Vacuum: add support for configuring scheduled cleaning
* Vacuum: more user-friendly do-not-disturb reporting
* Vacuum: VacuumState's 'dnd' and 'in_cleaning' properties are deprecated in favor of 'dnd_status' and 'is_on'.
* Power Strip: load power is returned now correctly
* Yeelight: allow configuring 'developer mode', 'save state on change', and internal name
* Properties common for several devices are now named more consistently
New supported devices:
* Xiaomi PM2.5 Air Quality Monitor
* Xiaomi Water Purifier
* Xiaomi Air Humidifier
* Xiaomi Smart Wifi Speaker (incomplete, help wanted)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.2.0...0.3.0)
**Implemented enhancements:**
- Column ZToken of the iOS app contains a 96 character token [\#75](https://github.com/rytilahti/python-miio/issues/75)
- Xiaomi PM2.5 Air Quality Monitor support [\#70](https://github.com/rytilahti/python-miio/issues/70)
**Closed issues:**
- Calling message handler 'onHeartbeat'. [\#82](https://github.com/rytilahti/python-miio/issues/82)
- How do I find more features? [\#10](https://github.com/rytilahti/python-miio/issues/10)
**Merged pull requests:**
- Device support of the Xiaomi PM2.5 Air Quality Monitor introduced [\#89](https://github.com/rytilahti/python-miio/pull/89) ([syssi](https://github.com/syssi))
- wrap vacuum's dnd status inside an object [\#87](https://github.com/rytilahti/python-miio/pull/87) ([rytilahti](https://github.com/rytilahti))
- Initial support for wifi speakers [\#86](https://github.com/rytilahti/python-miio/pull/86) ([rytilahti](https://github.com/rytilahti))
- Extend yeelight support [\#85](https://github.com/rytilahti/python-miio/pull/85) ([rytilahti](https://github.com/rytilahti))
- Discovery: Device name of the zimi powerstrip v2 fixed [\#84](https://github.com/rytilahti/python-miio/pull/84) ([syssi](https://github.com/syssi))
- Rename the project to python-miio [\#83](https://github.com/rytilahti/python-miio/pull/83) ([rytilahti](https://github.com/rytilahti))
- Device support of the Xiaomi Power Strip updated [\#81](https://github.com/rytilahti/python-miio/pull/81) ([syssi](https://github.com/syssi))
- WIP: Extract Android backups, yield devices instead of just echoing [\#80](https://github.com/rytilahti/python-miio/pull/80) ([rytilahti](https://github.com/rytilahti))
- add a note about miio-extract-tokens [\#79](https://github.com/rytilahti/python-miio/pull/79) ([rytilahti](https://github.com/rytilahti))
- Implement adding, deleting and updating the timer [\#78](https://github.com/rytilahti/python-miio/pull/78) ([rytilahti](https://github.com/rytilahti))
- Add miio-extract-tokens tool for extracting tokens from sqlite databases [\#77](https://github.com/rytilahti/python-miio/pull/77) ([rytilahti](https://github.com/rytilahti))
- WIP: Avoid discovery flooding [\#72](https://github.com/rytilahti/python-miio/pull/72) ([syssi](https://github.com/syssi))
- mDNS discovery: New air purifier model \(zhimi-airpurifier-m2\) [\#68](https://github.com/rytilahti/python-miio/pull/68) ([syssi](https://github.com/syssi))
- First draft of the water purifier support [\#67](https://github.com/rytilahti/python-miio/pull/67) ([syssi](https://github.com/syssi))
- Device support of the Xiaomi Air Humidifier [\#66](https://github.com/rytilahti/python-miio/pull/66) ([syssi](https://github.com/syssi))
- Device info extended by two additional properties [\#65](https://github.com/rytilahti/python-miio/pull/65) ([syssi](https://github.com/syssi))
- Abstract device model exteded by model name \(identifier\) [\#64](https://github.com/rytilahti/python-miio/pull/64) ([syssi](https://github.com/syssi))
- Adjust property names of some devices [\#63](https://github.com/rytilahti/python-miio/pull/63) ([syssi](https://github.com/syssi))
## [0.2.0](https://github.com/rytilahti/python-miio/tree/0.2.0) (2017-09-05)
Considering how far this project has evolved from being just an interface for the Xiaomi vacuum, it is time to leave 0.1 series behind and call this 0.2.0.
This release brings support to a couple of new devices, and contains fixes for some already supported ones.
All thanks for the improvements in this release go to syssi!
* Extended mDNS discovery to support more devices (@syssi)
* Improved support for the following devices:
* Air purifier (@syssi)
* Philips ball / Ceiling lamp (@syssi)
* Xiaomi Strip (@syssi)
* New supported devices:
* Chuangmi IR Remote control (@syssi)
* Xiaomi Mi Smart Fan (@syssi)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.1.4...0.2.0)
**Closed issues:**
- Error in new mirobo/protocol.py [\#54](https://github.com/rytilahti/python-miio/issues/54)
- Some element about Xiaomi Philips Bulb [\#43](https://github.com/rytilahti/python-miio/issues/43)
- Philips Bulb and ceiling how to get token ? [\#42](https://github.com/rytilahti/python-miio/issues/42)
- Add support for other devices using the same protocol [\#17](https://github.com/rytilahti/python-miio/issues/17)
- Allow sending discovery packets to static IP address [\#5](https://github.com/rytilahti/python-miio/issues/5)
**Merged pull requests:**
- trivial: fix typo in automatic discovery description. [\#61](https://github.com/rytilahti/python-miio/pull/61) ([haim0n](https://github.com/haim0n))
- Some typos fixed [\#60](https://github.com/rytilahti/python-miio/pull/60) ([syssi](https://github.com/syssi))
- Fixes an AttributeError: PlugStatus object has no attribute current [\#59](https://github.com/rytilahti/python-miio/pull/59) ([syssi](https://github.com/syssi))
- Fixes various lint issues [\#58](https://github.com/rytilahti/python-miio/pull/58) ([syssi](https://github.com/syssi))
- Air Purifier: Set favorite level fixed [\#55](https://github.com/rytilahti/python-miio/pull/55) ([syssi](https://github.com/syssi))
- mDNS name of the Chuangmi Infrared Controller [\#53](https://github.com/rytilahti/python-miio/pull/53) ([syssi](https://github.com/syssi))
- Device support for the Xiaomi Mi Smart Fan [\#52](https://github.com/rytilahti/python-miio/pull/52) ([syssi](https://github.com/syssi))
- mDNS device map extended [\#51](https://github.com/rytilahti/python-miio/pull/51) ([syssi](https://github.com/syssi))
- Power strip: Fixes calculation of the instantaneous current [\#50](https://github.com/rytilahti/python-miio/pull/50) ([syssi](https://github.com/syssi))
- Air purifier: defaultdict used for safety and transparency [\#49](https://github.com/rytilahti/python-miio/pull/49) ([syssi](https://github.com/syssi))
- Device support for the Chuangmi IR Remote Controller [\#46](https://github.com/rytilahti/python-miio/pull/46) ([syssi](https://github.com/syssi))
- Xiaomi Ceiling Lamp: Some refactoring and fault tolerance if a philips light ball is used [\#45](https://github.com/rytilahti/python-miio/pull/45) ([syssi](https://github.com/syssi))
- New dependency "zeroconf" added. It's used for discovery now. [\#44](https://github.com/rytilahti/python-miio/pull/44) ([syssi](https://github.com/syssi))
- Readme for firmware \>= 3.3.9\_003077 \(Vacuum robot\) [\#41](https://github.com/rytilahti/python-miio/pull/41) ([mthoretton](https://github.com/mthoretton))
- Some improvements of the air purifier support [\#40](https://github.com/rytilahti/python-miio/pull/40) ([syssi](https://github.com/syssi))
## [0.1.4](https://github.com/rytilahti/python-miio/tree/0.1.4) (2017-08-23)
Fix dependencies
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.1.3...0.1.4)
## [0.1.3](https://github.com/rytilahti/python-miio/tree/0.1.3) (2017-08-22)
* New commands:
* --version to print out library version
* info to return information about a device (requires token to be set)
* serial_number (vacuum only)
* timezone (getting and setting the timezone, vacuum only)
* sound (querying)
* Supports for the following new devices thanks to syssi and kuduka:
* Xiaomi Smart Power Strip (WiFi, 6 Ports) (@syssi)
* Xiaomi Mi Air Purifier 2 (@syssi)
* Xiaomi Mi Smart Socket Plug (1 Socket, 1 USB Port) (@syssi)
* Xiaomi Philips Eyecare Smart Lamp 2 (@kuduka)
* Xiaomi Philips LED Ceiling Lamp (@kuduka)
* Xiaomi Philips LED Ball Lamp (@kuduka)
* Discovery now uses mDNS instead of handshake protocol. Old behavior still available with `--handshake true`
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.1.2...0.1.3)
**Closed issues:**
- After updating to new firmware - can [\#37](https://github.com/rytilahti/python-miio/issues/37)
- CLI tool demands an IP address always [\#36](https://github.com/rytilahti/python-miio/issues/36)
- Use of both app and script not possible? [\#30](https://github.com/rytilahti/python-miio/issues/30)
- Moving from custom\_components to HA version not working [\#28](https://github.com/rytilahti/python-miio/issues/28)
- Xiaomi Robot new Device ID [\#27](https://github.com/rytilahti/python-miio/issues/27)
**Merged pull requests:**
- Supported devices added to README.md and version bumped [\#39](https://github.com/rytilahti/python-miio/pull/39) ([syssi](https://github.com/syssi))
- Fix Home Assistant link to doc, using new `vacuum` component [\#38](https://github.com/rytilahti/python-miio/pull/38) ([azogue](https://github.com/azogue))
- Added support for Xiaomi Philips LED Ceiling Lamp [\#35](https://github.com/rytilahti/python-miio/pull/35) ([kuduka](https://github.com/kuduka))
- Added support for Xiaomi Philips Eyecare Smart Lamp 2 [\#34](https://github.com/rytilahti/python-miio/pull/34) ([kuduka](https://github.com/kuduka))
- Device support for the chuangmi plug v1 added [\#33](https://github.com/rytilahti/python-miio/pull/33) ([syssi](https://github.com/syssi))
- Device support for the xiaomi power strip added. [\#32](https://github.com/rytilahti/python-miio/pull/32) ([syssi](https://github.com/syssi))
- Device support for the xiaomi air purifier added. [\#31](https://github.com/rytilahti/python-miio/pull/31) ([syssi](https://github.com/syssi))
## [0.1.2](https://github.com/rytilahti/python-miio/tree/0.1.2) (2017-07-22)
* Add support for Wifi plugs (thanks to syssi)
* Make communication more robust by retrying automatically on errors
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.1.1...0.1.2)
**Closed issues:**
- Pause not working in remote control mode [\#26](https://github.com/rytilahti/python-miio/issues/26)
- geht error singe 0.1.0 [\#25](https://github.com/rytilahti/python-miio/issues/25)
- Check that given token has correct length [\#11](https://github.com/rytilahti/python-miio/issues/11)
**Merged pull requests:**
- Device support for the xiaomi smart wifi socket added [\#29](https://github.com/rytilahti/python-miio/pull/29) ([syssi](https://github.com/syssi))
## [0.1.1](https://github.com/rytilahti/python-miio/tree/0.1.1) (2017-07-10)
add 'typing' requirement for python <3.5
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.1.0...0.1.1)
**Closed issues:**
- No module named 'typing' [\#24](https://github.com/rytilahti/python-miio/issues/24)
## [0.1.0](https://github.com/rytilahti/python-miio/tree/0.1.0) (2017-07-08)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.0.9...0.1.0)
**Closed issues:**
- Error: Invalid value for "--id-file" [\#23](https://github.com/rytilahti/python-miio/issues/23)
- error on execute mirobo discover [\#22](https://github.com/rytilahti/python-miio/issues/22)
- Only one command working [\#21](https://github.com/rytilahti/python-miio/issues/21)
- Integration in home assistant [\#4](https://github.com/rytilahti/python-miio/issues/4)
## [0.0.9](https://github.com/rytilahti/python-miio/tree/0.0.9) (2017-07-06)
fixes communication with newer firmwares
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.0.8...0.0.9)
**Closed issues:**
- Feature request: show cleaning map [\#20](https://github.com/rytilahti/python-miio/issues/20)
- Command "map" and "raw\_command" - what do they do? [\#19](https://github.com/rytilahti/python-miio/issues/19)
- mirobo "DND enabled: 0", after change to 1 [\#18](https://github.com/rytilahti/python-miio/issues/18)
- Xiaomi vaccum control from Raspberry pi + iPad Mi app at the same time - token: b'ffffffffffffffffffffffffffffffff' [\#16](https://github.com/rytilahti/python-miio/issues/16)
- Not working with newest firmware version 3.3.9\_003073 [\#14](https://github.com/rytilahti/python-miio/issues/14)
## [0.0.8](https://github.com/rytilahti/python-miio/tree/0.0.8) (2017-06-05)
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.0.7...0.0.8)
**Closed issues:**
- WIFI Switch for HA [\#12](https://github.com/rytilahti/python-miio/issues/12)
- Bug when having multiple network interfaces \(discovery\) [\#9](https://github.com/rytilahti/python-miio/issues/9)
- Get token from android App [\#8](https://github.com/rytilahti/python-miio/issues/8)
- Hello Thank you ! [\#7](https://github.com/rytilahti/python-miio/issues/7)
- WARNING:root:could not open file'/etc/apt/sources.list' [\#6](https://github.com/rytilahti/python-miio/issues/6)
## [0.0.7](https://github.com/rytilahti/python-miio/tree/0.0.7) (2017-04-14)
cleanup in preparation for homeassistant inclusion
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.0.6...0.0.7)
## [0.0.6](https://github.com/rytilahti/python-miio/tree/0.0.6) (2017-04-14)
cli improvements, total cleaning stats, remaining time for consumables
[Full Changelog](https://github.com/rytilahti/python-miio/compare/0.0.5...0.0.6)
**Closed issues:**
- some concern about the new version [\#3](https://github.com/rytilahti/python-miio/issues/3)
- can't find my robot [\#2](https://github.com/rytilahti/python-miio/issues/2)
- \[Error\] Timout when querying for status [\#1](https://github.com/rytilahti/python-miio/issues/1)
## [0.0.5](https://github.com/rytilahti/python-miio/tree/0.0.5) (2017-04-14)
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1519088451.0
python-miio-0.5.0.1/LICENSE 0000644 0001750 0001750 00000104513 00000000000 014575 0 ustar 00tpr tpr 0000000 0000000 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1519088451.0
python-miio-0.5.0.1/LICENSE.md 0000644 0001750 0001750 00000104146 00000000000 015176 0 ustar 00tpr tpr 0000000 0000000 # GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 [Free Software Foundation, Inc.](http://fsf.org/)
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
## Preamble
The GNU General Public License is a free, copyleft license for software and
other kinds of works.
The licenses for most software and other practical works are designed to take
away your freedom to share and change the works. By contrast, the GNU General
Public License is intended to guarantee your freedom to share and change all
versions of a program--to make sure it remains free software for all its users.
We, the Free Software Foundation, use the GNU General Public License for most
of our software; it applies also to any other work released this way by its
authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom to
distribute copies of free software (and charge for them if you wish), that you
receive source code or can get it if you want it, that you can change the
software or use pieces of it in new free programs, and that you know you can do
these things.
To protect your rights, we need to prevent others from denying you these rights
or asking you to surrender the rights. Therefore, you have certain
responsibilities if you distribute copies of the software, or if you modify it:
responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for
a fee, you must pass on to the recipients the same freedoms that you received.
You must make sure that they, too, receive or can get the source code. And you
must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps:
1. assert copyright on the software, and
2. offer you this License giving you legal permission to copy, distribute
and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that
there is no warranty for this free software. For both users' and authors' sake,
the GPL requires that modified versions be marked as changed, so that their
problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified
versions of the software inside them, although the manufacturer can do so. This
is fundamentally incompatible with the aim of protecting users' freedom to
change the software. The systematic pattern of such abuse occurs in the area of
products for individuals to use, which is precisely where it is most
unacceptable. Therefore, we have designed this version of the GPL to prohibit
the practice for those products. If such problems arise substantially in other
domains, we stand ready to extend this provision to those domains in future
versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States
should not allow patents to restrict development and use of software on
general-purpose computers, but in those that do, we wish to avoid the special
danger that patents applied to a free program could make it effectively
proprietary. To prevent this, the GPL assures that patents cannot be used to
render the program non-free.
The precise terms and conditions for copying, distribution and modification
follow.
## TERMS AND CONDITIONS
### 0. Definitions.
*This License* refers to version 3 of the GNU General Public License.
*Copyright* also means copyright-like laws that apply to other kinds of works,
such as semiconductor masks.
*The Program* refers to any copyrightable work licensed under this License.
Each licensee is addressed as *you*. *Licensees* and *recipients* may be
individuals or organizations.
To *modify* a work means to copy from or adapt all or part of the work in a
fashion requiring copyright permission, other than the making of an exact copy.
The resulting work is called a *modified version* of the earlier work or a work
*based on* the earlier work.
A *covered work* means either the unmodified Program or a work based on the
Program.
To *propagate* a work means to do anything with it that, without permission,
would make you directly or secondarily liable for infringement under applicable
copyright law, except executing it on a computer or modifying a private copy.
Propagation includes copying, distribution (with or without modification),
making available to the public, and in some countries other activities as well.
To *convey* a work means any kind of propagation that enables other parties to
make or receive copies. Mere interaction with a user through a computer
network, with no transfer of a copy, is not conveying.
An interactive user interface displays *Appropriate Legal Notices* to the
extent that it includes a convenient and prominently visible feature that
1. displays an appropriate copyright notice, and
2. tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the work
under this License, and how to view a copy of this License.
If the interface presents a list of user commands or options, such as a menu, a
prominent item in the list meets this criterion.
### 1. Source Code.
The *source code* for a work means the preferred form of the work for making
modifications to it. *Object code* means any non-source form of a work.
A *Standard Interface* means an interface that either is an official standard
defined by a recognized standards body, or, in the case of interfaces specified
for a particular programming language, one that is widely used among developers
working in that language.
The *System Libraries* of an executable work include anything, other than the
work as a whole, that (a) is included in the normal form of packaging a Major
Component, but which is not part of that Major Component, and (b) serves only
to enable use of the work with that Major Component, or to implement a Standard
Interface for which an implementation is available to the public in source code
form. A *Major Component*, in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system (if any) on
which the executable work runs, or a compiler used to produce the work, or an
object code interpreter used to run it.
The *Corresponding Source* for a work in object code form means all the source
code needed to generate, install, and (for an executable work) run the object
code and to modify the work, including scripts to control those activities.
However, it does not include the work's System Libraries, or general-purpose
tools or generally available free programs which are used unmodified in
performing those activities but which are not part of the work. For example,
Corresponding Source includes interface definition files associated with source
files for the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require, such as
by intimate data communication or control flow between those subprograms and
other parts of the work.
The Corresponding Source need not include anything that users can regenerate
automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
### 2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on
the Program, and are irrevocable provided the stated conditions are met. This
License explicitly affirms your unlimited permission to run the unmodified
Program. The output from running a covered work is covered by this License only
if the output, given its content, constitutes a covered work. This License
acknowledges your rights of fair use or other equivalent, as provided by
copyright law.
You may make, run and propagate covered works that you do not convey, without
conditions so long as your license otherwise remains in force. You may convey
covered works to others for the sole purpose of having them make modifications
exclusively for you, or provide you with facilities for running those works,
provided that you comply with the terms of this License in conveying all
material for which you do not control copyright. Those thus making or running
the covered works for you must do so exclusively on your behalf, under your
direction and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes it
unnecessary.
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure
under any applicable law fulfilling obligations under article 11 of the WIPO
copyright treaty adopted on 20 December 1996, or similar laws prohibiting or
restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention is
effected by exercising rights under this License with respect to the covered
work, and you disclaim any intention to limit operation or modification of the
work as a means of enforcing, against the work's users, your or third parties'
legal rights to forbid circumvention of technological measures.
### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it,
in any medium, provided that you conspicuously and appropriately publish on
each copy an appropriate copyright notice; keep intact all notices stating that
this License and any non-permissive terms added in accord with section 7 apply
to the code; keep intact all notices of the absence of any warranty; and give
all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may
offer support or warranty protection for a fee.
### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it
from the Program, in the form of source code under the terms of section 4,
provided that you also meet all of these conditions:
- a) The work must carry prominent notices stating that you modified it, and
giving a relevant date.
- b) The work must carry prominent notices stating that it is released under
this License and any conditions added under section 7. This requirement
modifies the requirement in section 4 to *keep intact all notices*.
- c) You must license the entire work, as a whole, under this License to
anyone who comes into possession of a copy. This License will therefore
apply, along with any applicable section 7 additional terms, to the whole
of the work, and all its parts, regardless of how they are packaged. This
License gives no permission to license the work in any other way, but it
does not invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your work need
not make them do so.
A compilation of a covered work with other separate and independent works,
which are not by their nature extensions of the covered work, and which are not
combined with it such as to form a larger program, in or on a volume of a
storage or distribution medium, is called an *aggregate* if the compilation and
its resulting copyright are not used to limit the access or legal rights of the
compilation's users beyond what the individual works permit. Inclusion of a
covered work in an aggregate does not cause this License to apply to the other
parts of the aggregate.
### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4
and 5, provided that you also convey the machine-readable Corresponding Source
under the terms of this License, in one of these ways:
- a) Convey the object code in, or embodied in, a physical product (including
a physical distribution medium), accompanied by the Corresponding Source
fixed on a durable physical medium customarily used for software
interchange.
- b) Convey the object code in, or embodied in, a physical product (including
a physical distribution medium), accompanied by a written offer, valid for
at least three years and valid for as long as you offer spare parts or
customer support for that product model, to give anyone who possesses the
object code either
1. a copy of the Corresponding Source for all the software in the product
that is covered by this License, on a durable physical medium
customarily used for software interchange, for a price no more than your
reasonable cost of physically performing this conveying of source, or
2. access to copy the Corresponding Source from a network server at no
charge.
- c) Convey individual copies of the object code with a copy of the written
offer to provide the Corresponding Source. This alternative is allowed only
occasionally and noncommercially, and only if you received the object code
with such an offer, in accord with subsection 6b.
- d) Convey the object code by offering access from a designated place
(gratis or for a charge), and offer equivalent access to the Corresponding
Source in the same way through the same place at no further charge. You
need not require recipients to copy the Corresponding Source along with the
object code. If the place to copy the object code is a network server, the
Corresponding Source may be on a different server operated by you or a
third party) that supports equivalent copying facilities, provided you
maintain clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the Corresponding
Source, you remain obligated to ensure that it is available for as long as
needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission, provided you
inform other peers where the object code and Corresponding Source of the
work are being offered to the general public at no charge under subsection
6d.
A separable portion of the object code, whose source code is excluded from the
Corresponding Source as a System Library, need not be included in conveying the
object code work.
A *User Product* is either
1. a *consumer product*, which means any tangible personal property which is
normally used for personal, family, or household purposes, or
2. anything designed or sold for incorporation into a dwelling.
In determining whether a product is a consumer product, doubtful cases shall be
resolved in favor of coverage. For a particular product received by a
particular user, *normally used* refers to a typical or common use of that
class of product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected to use,
the product. A product is a consumer product regardless of whether the product
has substantial commercial, industrial or non-consumer uses, unless such uses
represent the only significant mode of use of the product.
*Installation Information* for a User Product means any methods, procedures,
authorization keys, or other information required to install and execute
modified versions of a covered work in that User Product from a modified
version of its Corresponding Source. The information must suffice to ensure
that the continued functioning of the modified object code is in no case
prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as part of a
transaction in which the right of possession and use of the User Product is
transferred to the recipient in perpetuity or for a fixed term (regardless of
how the transaction is characterized), the Corresponding Source conveyed under
this section must be accompanied by the Installation Information. But this
requirement does not apply if neither you nor any third party retains the
ability to install modified object code on the User Product (for example, the
work has been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates for a
work that has been modified or installed by the recipient, or for the User
Product in which it has been modified or installed. Access to a network may be
denied when the modification itself materially and adversely affects the
operation of the network or violates the rules and protocols for communication
across the network.
Corresponding Source conveyed, and Installation Information provided, in accord
with this section must be in a format that is publicly documented (and with an
implementation available to the public in source code form), and must require
no special password or key for unpacking, reading or copying.
### 7. Additional Terms.
*Additional permissions* are terms that supplement the terms of this License by
making exceptions from one or more of its conditions. Additional permissions
that are applicable to the entire Program shall be treated as though they were
included in this License, to the extent that they are valid under applicable
law. If additional permissions apply only to part of the Program, that part may
be used separately under those permissions, but the entire Program remains
governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any
additional permissions from that copy, or from any part of it. (Additional
permissions may be written to require their own removal in certain cases when
you modify the work.) You may place additional permissions on material, added
by you to a covered work, for which you have or can give appropriate copyright
permission.
Notwithstanding any other provision of this License, for material you add to a
covered work, you may (if authorized by the copyright holders of that material)
supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the terms of
sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or author
attributions in that material or in the Appropriate Legal Notices displayed
by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in reasonable
ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors or authors
of the material; or
- e) Declining to grant rights under trademark law for use of some trade
names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that material by
anyone who conveys the material (or modified versions of it) with
contractual assumptions of liability to the recipient, for any liability
that these contractual assumptions directly impose on those licensors and
authors.
All other non-permissive additional terms are considered *further restrictions*
within the meaning of section 10. If the Program as you received it, or any
part of it, contains a notice stating that it is governed by this License along
with a term that is a further restriction, you may remove that term. If a
license document contains a further restriction but permits relicensing or
conveying under this License, you may add to a covered work material governed
by the terms of that license document, provided that the further restriction
does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place,
in the relevant source files, a statement of the additional terms that apply to
those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a
separately written license, or stated as exceptions; the above requirements
apply either way.
### 8. Termination.
You may not propagate or modify a covered work except as expressly provided
under this License. Any attempt otherwise to propagate or modify it is void,
and will automatically terminate your rights under this License (including any
patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a
particular copyright holder is reinstated
- a) provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and
- b) permanently, if the copyright holder fails to notify you of the
violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated
permanently if the copyright holder notifies you of the violation by some
reasonable means, this is the first time you have received notice of violation
of this License (for any work) from that copyright holder, and you cure the
violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses
of parties who have received copies or rights from you under this License. If
your rights have been terminated and not permanently reinstated, you do not
qualify to receive new licenses for the same material under section 10.
### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy
of the Program. Ancillary propagation of a covered work occurring solely as a
consequence of using peer-to-peer transmission to receive a copy likewise does
not require acceptance. However, nothing other than this License grants you
permission to propagate or modify any covered work. These actions infringe
copyright if you do not accept this License. Therefore, by modifying or
propagating a covered work, you indicate your acceptance of this License to do
so.
### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a
license from the original licensors, to run, modify and propagate that work,
subject to this License. You are not responsible for enforcing compliance by
third parties with this License.
An *entity transaction* is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered work
results from an entity transaction, each party to that transaction who receives
a copy of the work also receives whatever licenses to the work the party's
predecessor in interest had or could give under the previous paragraph, plus a
right to possession of the Corresponding Source of the work from the
predecessor in interest, if the predecessor has it or can get it with
reasonable efforts.
You may not impose any further restrictions on the exercise of the rights
granted or affirmed under this License. For example, you may not impose a
license fee, royalty, or other charge for exercise of rights granted under this
License, and you may not initiate litigation (including a cross-claim or
counterclaim in a lawsuit) alleging that any patent claim is infringed by
making, using, selling, offering for sale, or importing the Program or any
portion of it.
### 11. Patents.
A *contributor* is a copyright holder who authorizes use under this License of
the Program or a work on which the Program is based. The work thus licensed is
called the contributor's *contributor version*.
A contributor's *essential patent claims* are all patent claims owned or
controlled by the contributor, whether already acquired or hereafter acquired,
that would be infringed by some manner, permitted by this License, of making,
using, or selling its contributor version, but do not include claims that would
be infringed only as a consequence of further modification of the contributor
version. For purposes of this definition, *control* includes the right to grant
patent sublicenses in a manner consistent with the requirements of this
License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent
license under the contributor's essential patent claims, to make, use, sell,
offer for sale, import and otherwise run, modify and propagate the contents of
its contributor version.
In the following three paragraphs, a *patent license* is any express agreement
or commitment, however denominated, not to enforce a patent (such as an express
permission to practice a patent or covenant not to sue for patent
infringement). To *grant* such a patent license to a party means to make such
an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the
Corresponding Source of the work is not available for anyone to copy, free of
charge and under the terms of this License, through a publicly available
network server or other readily accessible means, then you must either
1. cause the Corresponding Source to be so available, or
2. arrange to deprive yourself of the benefit of the patent license for this
particular work, or
3. arrange, in a manner consistent with the requirements of this License, to
extend the patent license to downstream recipients.
*Knowingly relying* means you have actual knowledge that, but for the patent
license, your conveying the covered work in a country, or your recipient's use
of the covered work in a country, would infringe one or more identifiable
patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you
convey, or propagate by procuring conveyance of, a covered work, and grant a
patent license to some of the parties receiving the covered work authorizing
them to use, propagate, modify or convey a specific copy of the covered work,
then the patent license you grant is automatically extended to all recipients
of the covered work and works based on it.
A patent license is *discriminatory* if it does not include within the scope of
its coverage, prohibits the exercise of, or is conditioned on the non-exercise
of one or more of the rights that are specifically granted under this License.
You may not convey a covered work if you are a party to an arrangement with a
third party that is in the business of distributing software, under which you
make payment to the third party based on the extent of your activity of
conveying the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory patent
license
- a) in connection with copies of the covered work conveyed by you (or copies
made from those copies), or
- b) primarily for and in connection with specific products or compilations
that contain the covered work, unless you entered into that arrangement, or
that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied
license or other defenses to infringement that may otherwise be available to
you under applicable patent law.
### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not excuse
you from the conditions of this License. If you cannot convey a covered work so
as to satisfy simultaneously your obligations under this License and any other
pertinent obligations, then as a consequence you may not convey it at all. For
example, if you agree to terms that obligate you to collect a royalty for
further conveying from those to whom you convey the Program, the only way you
could satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
### 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to
link or combine any covered work with a work licensed under version 3 of the
GNU Affero General Public License into a single combined work, and to convey
the resulting work. The terms of this License will continue to apply to the
part which is the covered work, but the special requirements of the GNU Affero
General Public License, section 13, concerning interaction through a network
will apply to the combination as such.
### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU
General Public License from time to time. Such new versions will be similar in
spirit to the present version, but may differ in detail to address new problems
or concerns.
Each version is given a distinguishing version number. If the Program specifies
that a certain numbered version of the GNU General Public License *or any later
version* applies to it, you have the option of following the terms and
conditions either of that numbered version or of any later version published by
the Free Software Foundation. If the Program does not specify a version number
of the GNU General Public License, you may choose any version ever published by
the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the
GNU General Public License can be used, that proxy's public statement of
acceptance of a version permanently authorizes you to choose that version for
the Program.
Later license versions may give you additional or different permissions.
However, no additional obligations are imposed on any author or copyright
holder as a result of your choosing to follow a later version.
### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER
PARTIES PROVIDE THE PROGRAM *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE
QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS
PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY
HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot
be given local legal effect according to their terms, reviewing courts shall
apply local law that most closely approximates an absolute waiver of all civil
liability in connection with the Program, unless a warranty or assumption of
liability accompanies a copy of the Program in return for a fee.
## END OF TERMS AND CONDITIONS ###
### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively state the exclusion
of warranty; and each file should have at least the *copyright* line and a
pointer to where the full notice is found.
Python library & console tool for controlling Xiaomi smart appliances
Copyright (C) 2017 Teemu R.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like
this when it starts in an interactive mode:
python-miio Copyright (C) 2017 Teemu R.
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w` and `show c` should show the appropriate
parts of the General Public License. Of course, your program's commands might
be different; for a GUI interface, you would use an *about box*.
You should also get your employer (if you work as a programmer) or school, if
any, to sign a *copyright disclaimer* for the program, if necessary. For more
information on this, and how to apply and follow the GNU GPL, see
[http://www.gnu.org/licenses/](http://www.gnu.org/licenses/).
The GNU General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may consider
it more useful to permit linking proprietary applications with the library. If
this is what you want to do, use the GNU Lesser General Public License instead
of this License. But first, please read
[http://www.gnu.org/philosophy/why-not-lgpl.html](http://www.gnu.org/philosophy/why-not-lgpl.html).
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/MANIFEST.in 0000644 0001750 0001750 00000000353 00000000000 015323 0 ustar 00tpr tpr 0000000 0000000 include *.md
include *.txt
include *.yaml
include *.yml
include LICENSE
include tox.ini
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs Makefile
recursive-include miio *.json
recursive-include miio *.py
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1585507846.5139406
python-miio-0.5.0.1/PKG-INFO 0000644 0001750 0001750 00000013545 00000000000 014671 0 ustar 00tpr tpr 0000000 0000000 Metadata-Version: 2.1
Name: python-miio
Version: 0.5.0.1
Summary: Python library for interfacing with Xiaomi smart appliances
Home-page: https://github.com/rytilahti/python-miio
Author: Teemu Rytilahti
Author-email: tpr@iki.fi
License: GPLv3
Description: python-miio
===========
|PyPI version| |Build Status| |Coverage Status| |Docs| |Black| |Hound|
This library (and its accompanying cli tool) is used to interface with devices using Xiaomi's `miIO protocol `__.
Supported devices
-----------------
- Xiaomi Mi Robot Vacuum V1, S5, M1S
- Xiaomi Mi Home Air Conditioner Companion
- Xiaomi Mi Air Purifier
- Xiaomi Aqara Camera
- Xiaomi Aqara Gateway (basic implementation, alarm, lights)
- Xiaomi Mijia 360 1080p
- Xiaomi Mijia STYJ02YM (Viomi)
- Xiaomi Mi Smart WiFi Socket
- Xiaomi Chuangmi Plug V1 (1 Socket, 1 USB Port)
- Xiaomi Chuangmi Plug V3 (1 Socket, 2 USB Ports)
- Xiaomi Smart Power Strip V1 and V2 (WiFi, 6 Ports)
- Xiaomi Philips Eyecare Smart Lamp 2
- Xiaomi Philips RW Read (philips.light.rwread)
- Xiaomi Philips LED Ceiling Lamp
- Xiaomi Philips LED Ball Lamp (philips.light.bulb)
- Xiaomi Philips LED Ball Lamp White (philips.light.hbulb)
- Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp
- Xiaomi Philips Zhirui Bedroom Smart Lamp
- Xiaomi Universal IR Remote Controller (Chuangmi IR)
- Xiaomi Mi Smart Pedestal Fan V2, V3, SA1, ZA1, ZA3, ZA4, P5
- Xiaomi Mi Air Humidifier V1, CA1, CB1, MJJSQ, JSQ001
- Xiaomi Mi Water Purifier (Basic support: Turn on & off)
- Xiaomi PM2.5 Air Quality Monitor V1, B1, S1
- Xiaomi Smart WiFi Speaker
- Xiaomi Mi WiFi Repeater 2
- Xiaomi Mi Smart Rice Cooker
- Xiaomi Smartmi Fresh Air System VA2 (zhimi.airfresh.va2), T2017 (dmaker.airfresh.t2017)
- Yeelight lights (basic support, we recommend using `python-yeelight `__)
- Xiaomi Mi Air Dehumidifier
- Xiaomi Tinymu Smart Toilet Cover
- Xiaomi 16 Relays Module
- Xiaomi Xiao AI Smart Alarm Clock
- Smartmi Radiant Heater Smart Version (ZA1 version)
- Xiaomi Mi Smart Space Heater
*Feel free to create a pull request to add support for new devices as
well as additional features for supported devices.*
Getting started
---------------
Refer `the manual `__ for getting started.
Contributing
------------
We welcome all sorts of contributions from patches to add improvements or fixing bugs to improving the documentation.
To ease the process of setting up a development environment we have prepared `a short guide `__ for getting you started.
Home Assistant support
----------------------
- `Xiaomi Mi Robot Vacuum `__
- `Xiaomi Philips Light `__
- `Xiaomi Mi Air Purifier and Air Humidifier `__
- `Xiaomi Smart WiFi Socket and Smart Power Strip `__
- `Xiaomi Universal IR Remote Controller `__
- `Xiaomi Mi Air Quality Monitor (PM2.5) `__
- `Xiaomi Aqara Gateway Alarm `__
- `Xiaomi Mi Home Air Conditioner Companion `__
- `Xiaomi Mi WiFi Repeater 2 `__
- `Xiaomi Mi Smart Pedestal Fan `__
- `Xiaomi Mi Smart Rice Cooker `__
- `Xiaomi Raw Sensor `__
.. |PyPI version| image:: https://badge.fury.io/py/python-miio.svg
:target: https://badge.fury.io/py/python-miio
.. |Build Status| image:: https://travis-ci.org/rytilahti/python-miio.svg?branch=master
:target: https://travis-ci.org/rytilahti/python-miio
.. |Coverage Status| image:: https://coveralls.io/repos/github/rytilahti/python-miio/badge.svg?branch=master
:target: https://coveralls.io/github/rytilahti/python-miio?branch=master
.. |Docs| image:: https://readthedocs.org/projects/python-miio/badge/?version=latest
:alt: Documentation status
:target: https://python-miio.readthedocs.io/en/latest/?badge=latest
.. |Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
:alt: Hound
:target: https://houndci.com
.. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
Keywords: xiaomi miio vacuum
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.6
Provides-Extra: Android backup extraction
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500788.0
python-miio-0.5.0.1/README.rst 0000644 0001750 0001750 00000010660 00000000000 015256 0 ustar 00tpr tpr 0000000 0000000 python-miio
===========
|PyPI version| |Build Status| |Coverage Status| |Docs| |Black| |Hound|
This library (and its accompanying cli tool) is used to interface with devices using Xiaomi's `miIO protocol `__.
Supported devices
-----------------
- Xiaomi Mi Robot Vacuum V1, S5, M1S
- Xiaomi Mi Home Air Conditioner Companion
- Xiaomi Mi Air Purifier
- Xiaomi Aqara Camera
- Xiaomi Aqara Gateway (basic implementation, alarm, lights)
- Xiaomi Mijia 360 1080p
- Xiaomi Mijia STYJ02YM (Viomi)
- Xiaomi Mi Smart WiFi Socket
- Xiaomi Chuangmi Plug V1 (1 Socket, 1 USB Port)
- Xiaomi Chuangmi Plug V3 (1 Socket, 2 USB Ports)
- Xiaomi Smart Power Strip V1 and V2 (WiFi, 6 Ports)
- Xiaomi Philips Eyecare Smart Lamp 2
- Xiaomi Philips RW Read (philips.light.rwread)
- Xiaomi Philips LED Ceiling Lamp
- Xiaomi Philips LED Ball Lamp (philips.light.bulb)
- Xiaomi Philips LED Ball Lamp White (philips.light.hbulb)
- Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp
- Xiaomi Philips Zhirui Bedroom Smart Lamp
- Xiaomi Universal IR Remote Controller (Chuangmi IR)
- Xiaomi Mi Smart Pedestal Fan V2, V3, SA1, ZA1, ZA3, ZA4, P5
- Xiaomi Mi Air Humidifier V1, CA1, CB1, MJJSQ, JSQ001
- Xiaomi Mi Water Purifier (Basic support: Turn on & off)
- Xiaomi PM2.5 Air Quality Monitor V1, B1, S1
- Xiaomi Smart WiFi Speaker
- Xiaomi Mi WiFi Repeater 2
- Xiaomi Mi Smart Rice Cooker
- Xiaomi Smartmi Fresh Air System VA2 (zhimi.airfresh.va2), T2017 (dmaker.airfresh.t2017)
- Yeelight lights (basic support, we recommend using `python-yeelight `__)
- Xiaomi Mi Air Dehumidifier
- Xiaomi Tinymu Smart Toilet Cover
- Xiaomi 16 Relays Module
- Xiaomi Xiao AI Smart Alarm Clock
- Smartmi Radiant Heater Smart Version (ZA1 version)
- Xiaomi Mi Smart Space Heater
*Feel free to create a pull request to add support for new devices as
well as additional features for supported devices.*
Getting started
---------------
Refer `the manual `__ for getting started.
Contributing
------------
We welcome all sorts of contributions from patches to add improvements or fixing bugs to improving the documentation.
To ease the process of setting up a development environment we have prepared `a short guide `__ for getting you started.
Home Assistant support
----------------------
- `Xiaomi Mi Robot Vacuum `__
- `Xiaomi Philips Light `__
- `Xiaomi Mi Air Purifier and Air Humidifier `__
- `Xiaomi Smart WiFi Socket and Smart Power Strip `__
- `Xiaomi Universal IR Remote Controller `__
- `Xiaomi Mi Air Quality Monitor (PM2.5) `__
- `Xiaomi Aqara Gateway Alarm `__
- `Xiaomi Mi Home Air Conditioner Companion `__
- `Xiaomi Mi WiFi Repeater 2 `__
- `Xiaomi Mi Smart Pedestal Fan `__
- `Xiaomi Mi Smart Rice Cooker `__
- `Xiaomi Raw Sensor `__
.. |PyPI version| image:: https://badge.fury.io/py/python-miio.svg
:target: https://badge.fury.io/py/python-miio
.. |Build Status| image:: https://travis-ci.org/rytilahti/python-miio.svg?branch=master
:target: https://travis-ci.org/rytilahti/python-miio
.. |Coverage Status| image:: https://coveralls.io/repos/github/rytilahti/python-miio/badge.svg?branch=master
:target: https://coveralls.io/github/rytilahti/python-miio?branch=master
.. |Docs| image:: https://readthedocs.org/projects/python-miio/badge/?version=latest
:alt: Documentation status
:target: https://python-miio.readthedocs.io/en/latest/?badge=latest
.. |Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
:alt: Hound
:target: https://houndci.com
.. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1548870856.0
python-miio-0.5.0.1/RELEASING.md 0000644 0001750 0001750 00000001474 00000000000 015425 0 ustar 00tpr tpr 0000000 0000000 1. Update the version number
```bash
nano miio/version.py
```
2. Generate changelog since the last release
```bash
# gem install github_changelog_generator --pre
export CHANGELOG_GITHUB_TOKEN=token
~/.gem/ruby/2.4.0/bin/github_changelog_generator --user rytilahti --project python-miio --since-tag 0.3.0 -o newchanges
```
3. Copy the changelog block over to CHANGELOG.md and write a short and understandable summary.
4. Commit the changed files
```
git commit -av
```
5. Tag a release (and add short changelog as a tag commit message)
```bash
git tag -a 0.3.1
```
6. Push to git
```bash
git push --tags
```
7. Upload new version to pypi
```bash
python setup.py sdist bdist_wheel upload
```
8. Click the "Draft a new release" button on github, select the new tag and copy & paste the changelog into the description.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585505438.0
python-miio-0.5.0.1/azure-pipelines.yml 0000644 0001750 0001750 00000002042 00000000000 017421 0 ustar 00tpr tpr 0000000 0000000 trigger:
- master
pr:
- master
pool:
vmImage: 'ubuntu-latest'
strategy:
matrix:
Python36:
python.version: '3.6'
Python37:
python.version: '3.7'
# Python38:
# python.version: '3.8'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(python.version)'
displayName: 'Use Python $(python.version)'
- script: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-azurepipelines pytest-cov
displayName: 'Install dependencies'
- script: |
pre-commit run black --all-files
displayName: 'Code formating (black)'
- script: |
pre-commit run flake8 --all-files
displayName: 'Code formating (flake8)'
#- script: |
# pre-commit run mypy --all-files
# displayName: 'Typing checks (mypy)'
- script: |
pre-commit run isort --all-files
displayName: 'Order of imports (isort)'
- script: |
pytest --cov miio --cov-report html
displayName: 'Tests'
- script: |
pre-commit run check-manifest --all-files
displayName: 'Check MANIFEST.in'
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1585507846.5039406
python-miio-0.5.0.1/docs/ 0000755 0001750 0001750 00000000000 00000000000 014514 5 ustar 00tpr tpr 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1519088451.0
python-miio-0.5.0.1/docs/Makefile 0000644 0001750 0001750 00000001143 00000000000 016153 0 ustar 00tpr tpr 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python -msphinx
SPHINXPROJ = python-miio
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1519088451.0
python-miio-0.5.0.1/docs/ceil.rst 0000644 0001750 0001750 00000000356 00000000000 016166 0 ustar 00tpr tpr 0000000 0000000 Ceil
====
.. todo::
Pull requests for documentation are welcome!
See :ref:`miceil --help ` for usage.
.. _miceil_help:
`miceil --help`
~~~~~~~~~~~~~~~
.. click:: miio.ceil_cli:cli
:prog: miceil
:show-nested:
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/docs/conf.py 0000644 0001750 0001750 00000012664 00000000000 016024 0 ustar 00tpr tpr 0000000 0000000 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# python-miio documentation build configuration file, created by
# sphinx-quickstart on Wed Oct 18 03:50:00 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath(".."))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx_autodoc_typehints",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx.ext.githubpages",
"sphinx.ext.intersphinx",
"sphinx_click.ext",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# General information about the project.
project = "python-miio"
copyright = "2017, Teemu Rytilahti"
author = "Teemu Rytilahti"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = ""
# The full version, including alpha/beta/rc tags.
release = ""
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv"]
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = []
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
"**": [
"about.html",
"navigation.html",
"relations.html", # needs 'show_related': True theme option to display
"searchbox.html",
"donate.html",
]
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "python-miiodoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(
master_doc,
"python-miio.tex",
"python-miio Documentation",
"Teemu Rytilahti",
"manual",
)
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "python-miio", "python-miio Documentation", [author], 1)]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"python-miio",
"python-miio Documentation",
author,
"python-miio",
"One line description of project.",
"Miscellaneous",
)
]
intersphinx_mapping = {"python": ("https://docs.python.org/3.6", None)}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/docs/discovery.rst 0000644 0001750 0001750 00000025603 00000000000 017263 0 ustar 00tpr tpr 0000000 0000000 Getting started
***************
Installation
============
The easiest way to install the package is to use pip:
``pip3 install python-miio`` . `Using
virtualenv `__
is recommended.
Please make sure you have ``libffi`` and ``openssl`` headers installed, you can
do this on Debian-based systems (like Rasperry Pi) with
.. code-block:: bash
apt-get install libffi-dev libssl-dev
Depending on your installation, the setuptools version may be too old
for some dependencies so before reporting an issue please try to update
the setuptools package with
.. code-block:: bash
pip3 install -U setuptools
In case you get an error similar like
``ImportError: No module named 'packaging'`` during the installation,
you need to upgrade pip and setuptools:
.. code-block:: bash
pip3 install -U pip setuptools
Device discovery
================
Devices already connected on the same network where the command-line tool
is run are automatically detected when ``mirobo discover`` is invoked.
To be able to communicate with devices their IP address and a device-specific
encryption token must be known.
If the returned a token is with characters other than ``0``\ s or ``f``\ s,
it is likely a valid token which can be used directly for communication.
If not, the token needs to be extracted from the Mi Home Application,
see :ref:`logged_tokens` for information how to do this.
.. IMPORTANT::
For some devices (e.g. the vacuum cleaner) the automatic discovery works only before the device has been connected over the app to your local wifi.
This does not work starting from firmware version 3.3.9\_003077 onwards, in which case the procedure shown in :ref:`creating_backup` has to be used
to obtain the token.
.. NOTE::
Some devices also do not announce themselves via mDNS (e.g. Philips' bulbs,
and the vacuum when not connected to the Internet),
but are nevertheless discoverable by using a miIO discovery.
See :ref:`handshake_discovery` for more information about the topic.
.. _handshake_discovery:
Discovery by a handshake
------------------------
The devices supporting miIO protocol answer to a broadcasted handshake packet,
which also sometime contain the required token.
Executing ``mirobo discover`` with ``--handshake 1`` option will send
a broadcast handshake.
Devices supporting the protocol will response with a message
potentially containing a valid token.
.. code-block:: bash
$ mirobo discover --handshake 1
INFO:miio.device: IP 192.168.8.1: Xiaomi Mi Robot Vacuum - token: b'ffffffffffffffffffffffffffffffff'
.. NOTE::
This method can also be useful for devices not yet connected to any network.
In those cases the device trying to do the discovery has to connect to the
network advertised by the corresponding device (e.g. rockrobo-XXXX for vacuum)
Tokens full of ``0``\ s or ``f``\ s (as above) are either already paired
with the mobile app or will not yield a token through this method.
In those cases the procedure shown in :ref:`logged_tokens` has to be used.
.. _logged_tokens:
Tokens from Mi Home logs
========================
The easiest way to obtain tokens is to browse through log files of the Mi Home
app version 5.4.49 for Android. It seems that version was released with debug
messages turned on by mistake. An APK file with the old version can be easily
found using one of the popular web search engines. After downgrading use a file
browser to navigate to directory ``SmartHome/logs/plug_DeviceManager``, then
open the most recent file and search for the token. When finished, use Google
Play to get the most recent version back.
.. _creating_backup:
Tokens from backups
===================
Extracting tokens from a Mi Home backup is the preferred way to obtain tokens
if they cannot be looked up in the Mi Home app version 5.4.49 log files
(e.g. no Android device around).
For this to work the devices have to be added to the app beforehand
before the database (or backup) is extracted.
Creating a backup
-----------------
The first step to do this is to extract a backup
or database from the Mi Home app.
The procedure is briefly described below,
but you may find the following links also useful:
- https://github.com/jghaanstra/com.xiaomi-miio/blob/master/docs/obtain_token.md
- https://github.com/homeassistantchina/custom_components/blob/master/doc/chuang_mi_ir_remote.md
Android
~~~~~~~
Start by installing the newest version of the Mi Home app from Google Play and
setting up your account. When the app asks you which server you want to use,
it's important to pick one that is also available in older versions of Mi
Home (we'll see why a bit later). U.S or china servers are OK, but the european
server is not supported by the old app. Then, set up your Xiaomi device with the
Mi Home app.
After the setup is completed, and the device has been connected to the Wi-Fi
network of your choice, it is necessary to downgrade the Mi Home app to some
version equal or below 5.0.19. As explained `here `_
and `in github issue #185 `_, newer versions
of the app do not download the token into the local database, which means that
we can't retrieve the token from the backup. You can find older versions of the
Mi Home app in `apkmirror `_.
Download, install and start up the older version of the Mi Home app. When the
app asks which server should be used, pick the same one you used with the newer
version of the app. Then, log into your account.
After this point, you are ready to perform the backup and extract the token.
Please note that it's possible that your device does not show under the old app.
As long as you picked the same server, it should be OK, and the token should
have been downloaded and stored into the database.
To do a backup of an Android app you need to have the developer mode active, and
your device has to be accessible with ``adb``.
.. TODO::
Add a link how to check and enable the developer mode.
This part of documentation needs your help!
Please consider submitting a pull request to update this.
After you have connected your device to your computer,
and installed the Android developer tools,
you can use ``adb`` tool to create a backup.
.. code-block:: bash
adb backup -noapk com.xiaomi.smarthome -f backup.ab
.. NOTE::
Depending on your Android version you may need to insert a password
and/or accept the backup, so check your phone at this point!
If everything went fine and you got a ``backup.ab`` file,
please continue to :ref:`token_extraction`.
Apple
~~~~~
Create a new unencrypted iOS backup to your computer.
To do that you've to follow these steps:
- Connect your iOS device to the computer
- Open iTunes
- Click on your iOS device (sidebar left or icon on top navigation bar)
- In the Summary view check the following settings
- Automatically Back Up: ``This Computer``
- **Disable** ``Encrypt iPhone backup``
- Click ``Back Up Now``
When the backup is finished, download `iBackup Viewer `_ and follow these steps:
- Open iBackup Viewer
- Click on your newly created backup
- Click on the ``Raw Files`` icon (looks like a file tree)
- On the left column, search for ``AppDomain-com.xiaomi.mihome`` and select it
- Click on the search icon in the header
- Enter ``_mihome`` in the search field
- Select the ``Documents/0123456789_mihome.sqlite`` file (the one with the number prefixed)
- Click ``Export -> Selected…`` in the header and store the file
Now you've exported the SQLite database to your Mac and you can extract the tokens.
.. note::
See also `jghaanstra's obtain token docs `_ for alternative ways.
.. _token_extraction:
Extracting tokens
-----------------
Now having extract either a backup or a database from the application,
the ``miio-extract-tokens`` can be used to extract the tokens from it.
At the moment extracting tokens from a backup (Android),
or from an extracted database (Android, Apple) are supported.
Encrypted tokens as `recently introduced on iOS devices `_ will be automatically decrypted.
For decrypting Android backups the password has to be provided
to the tool with ``--password ``.
*Please feel free to submit pull requests to simplify this procedure!*
.. code-block:: bash
$ miio-extract-tokens backup.ab
Opened backup/backup.ab
Extracting to /tmp/tmpvbregact
Reading tokens from Android DB
Gateway
Model: lumi.gateway.v3
IP address: 192.168.XXX.XXX
Token: 91c52a27eff00b954XXX
MAC: 28:6C:07:XX:XX:XX
room1
Model: yeelink.light.color1
IP address: 192.168.XXX.XXX
Token: 4679442a069f09883XXX
MAC: F0:B4:29:XX:XX:XX
room2
Model: yeelink.light.color1
IP address: 192.168.XXX.XXX
Token: 7433ab14222af5792XXX
MAC: 28:6C:07:XX:XX:XX
Flower Care
Model: hhcc.plantmonitor.v1
IP address: 134.XXX.XXX.XXX
Token: 124f90d87b4b90673XXX
MAC: C4:7C:8D:XX:XX:XX
Mi Robot Vacuum
Model: rockrobo.vacuum.v1
IP address: 192.168.XXX.XXX
Token: 476e6b70343055483XXX
MAC: 28:6C:07:XX:XX:XX
Extracting tokens manually
--------------------------
Run the following SQLite command:
.. code-block:: bash
sqlite3 "select ZNAME,ZLOCALIP,ZTOKEN from ZDEVICE"
You should get a list which looks like this:
.. code-block:: text
Device 1|x.x.x.x|0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
Device 2|x.x.x.x|0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
Device 3|x.x.x.x|0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
These are your device names, IP addresses and tokens. However, the tokens are encrypted and you need to decrypt them.
The command for decrypting the token manually is:
.. code-block:: bash
echo '0: ' | xxd -r -p | openssl enc -d -aes-128-ecb -nopad -nosalt -K 00000000000000000000000000000000
Environment variables for command-line tools
============================================
To simplify the use, instead of passing the IP and the token as a
parameter for the tool, you can simply set the following environment variables.
The following works for `mirobo`, for other tools you should consult
the documentation of corresponding tool.
.. code-block:: bash
export MIROBO_IP=192.168.1.2
export MIROBO_TOKEN=476e6b70343055483230644c53707a12
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1519088451.0
python-miio-0.5.0.1/docs/eyecare.rst 0000644 0001750 0001750 00000000411 00000000000 016657 0 ustar 00tpr tpr 0000000 0000000 Philips Eyecare
===============
.. todo::
Pull requests for documentation are welcome!
See :ref:`mieye --help ` for usage.
.. _mieye_help:
`mieye --help`
~~~~~~~~~~~~~~~
.. click:: miio.philips_eyecare_cli:cli
:prog: mieye
:show-nested:
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1549371166.0
python-miio-0.5.0.1/docs/index.rst 0000644 0001750 0001750 00000002302 00000000000 016352 0 ustar 00tpr tpr 0000000 0000000 .. python-miio documentation master file, created by
sphinx-quickstart on Wed Oct 18 03:50:00 2017.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. include:: ../README.rst
History
-------
This project was started to allow controlling locally available Xiaomi
Vacuum cleaner robot with Python (hence the old name ``python-mirobo``),
however, thanks to contributors it has been extended to allow
controlling other Xiaomi devices using the same protocol `miIO protocol `__.
(`another source for vacuum-specific
documentation `__)
First and foremost thanks for the nice people over `ioBroker
forum `__ who
figured out the encryption to make this library possible.
Furthermore thanks goes to contributors of this project
who have helped to extend this to cover not only the vacuum cleaner.
.. toctree::
:maxdepth: 2
:caption: Contents:
Home
discovery
new_devices
vacuum
plug
ceil
eyecare
yeelight
API
troubleshooting
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584295509.0
python-miio-0.5.0.1/docs/miio.rst 0000644 0001750 0001750 00000010335 00000000000 016205 0 ustar 00tpr tpr 0000000 0000000 miio package
============
Submodules
----------
miio\.airconditioningcompanion module
-------------------------------------
.. automodule:: miio.airconditioningcompanion
:members:
:show-inheritance:
:undoc-members:
miio\.airfresh module
---------------------
.. automodule:: miio.airfresh
:members:
:show-inheritance:
:undoc-members:
miio\.airhumidifier module
--------------------------
.. automodule:: miio.airhumidifier
:members:
:show-inheritance:
:undoc-members:
miio\.airpurifier module
------------------------
.. automodule:: miio.airpurifier
:members:
:show-inheritance:
:undoc-members:
miio\.airpurifier_miot module
-----------------------------
.. automodule:: miio.airpurifier_miot
:members:
:show-inheritance:
:undoc-members:
miio\.airqualitymonitor module
------------------------------
.. automodule:: miio.airqualitymonitor
:members:
:show-inheritance:
:undoc-members:
miio\.aqaracamera module
------------------------
.. automodule:: miio.aqaracamera
:members:
:show-inheritance:
:undoc-members:
miio\.ceil module
-----------------
.. automodule:: miio.ceil
:members:
:show-inheritance:
:undoc-members:
miio\.chuangmi\_camera module
-----------------------------
.. automodule:: miio.chuangmi_camera
:members:
:show-inheritance:
:undoc-members:
miio\.chuangmi\_ir module
-------------------------
.. automodule:: miio.chuangmi_ir
:members:
:show-inheritance:
:undoc-members:
miio\.cooker module
-------------------
.. automodule:: miio.cooker
:members:
:show-inheritance:
:undoc-members:
miio\.device module
-------------------
.. automodule:: miio.device
:members:
:show-inheritance:
:undoc-members:
miio\.miot_device module
------------------------
.. automodule:: miio.miot_device
:members:
:show-inheritance:
:undoc-members:
miio\.discovery module
----------------------
.. automodule:: miio.discovery
:members:
:show-inheritance:
:undoc-members:
miio\.extract\_tokens module
----------------------------
.. automodule:: miio.extract_tokens
:members:
:show-inheritance:
:undoc-members:
miio\.fan module
----------------
.. automodule:: miio.fan
:members:
:show-inheritance:
:undoc-members:
miio\.philips\_bulb module
--------------------------
.. automodule:: miio.philips_bulb
:members:
:show-inheritance:
:undoc-members:
miio\.philips\_eyecare module
-----------------------------
.. automodule:: miio.philips_eyecare
:members:
:show-inheritance:
:undoc-members:
miio\.philips\_moonlight module
-------------------------------
.. automodule:: miio.philips_moonlight
:members:
:show-inheritance:
:undoc-members:
miio\.chuangmi_plug module
--------------------------
.. automodule:: miio.chuangmi_plug
:members:
:show-inheritance:
:undoc-members:
miio\.protocol module
---------------------
.. automodule:: miio.protocol
:members:
:show-inheritance:
:undoc-members:
miio\.powerstrip module
-----------------------
.. automodule:: miio.powerstrip
:members:
:show-inheritance:
:undoc-members:
miio\.vacuum module
-------------------
.. automodule:: miio.vacuum
:members:
:show-inheritance:
:undoc-members:
miio\.vacuumcontainers module
-----------------------------
.. automodule:: miio.vacuumcontainers
:members:
:show-inheritance:
:undoc-members:
miio\.version module
--------------------
.. automodule:: miio.version
:members:
:show-inheritance:
:undoc-members:
miio\.waterpurifier module
--------------------------
.. automodule:: miio.waterpurifier
:members:
:show-inheritance:
:undoc-members:
miio\.wifirepeater module
-------------------------
.. automodule:: miio.wifirepeater
:members:
:show-inheritance:
:undoc-members:
miio\.wifispeaker module
------------------------
.. automodule:: miio.wifispeaker
:members:
:show-inheritance:
:undoc-members:
miio\.yeelight module
---------------------
.. automodule:: miio.yeelight
:members:
:show-inheritance:
:undoc-members:
Module contents
---------------
.. automodule:: miio
:members:
:show-inheritance:
:undoc-members:
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/docs/new_devices.rst 0000644 0001750 0001750 00000006455 00000000000 017553 0 ustar 00tpr tpr 0000000 0000000 Contributing
************
Contributions of any sort are more than welcome,
so we hope this short introduction will help you to get started!
Shortly put: we use black_ to format our code, isort_ to sort our imports, pytest_ to test our code,
flake8_ to do its checks, and doc8_ for documentation checks.
Development environment
-----------------------
This section will shortly go through how to get you started with a working development environment.
We assume that you are familiar with virtualenv_ and are using it somehow (be it a manual setup, pipenv_, ..).
The easiest way to start is to use pip_ to install dependencies::
pip install -r requirements.txt
followed by installing the package in `development mode `__ ::
pip install -e .
To verify the installation, simply launch tox_ to run all the checks::
tox
In order to make feedback loops faster, we automate our code checks by using precommit_ hooks.
Therefore the first step after setting up the development environment is to install them::
pre-commit install
You can always `execute the checks <#code-checks>`_ also without doing a commit.
Code checks
~~~~~~~~~~~
Instead of running all available checks during development,
it is also possible to execute only the code checks by calling.
This will execute the same checks that would be done automatically by precommit_ when you make a commit::
tox -e lint
Tests
~~~~~
We prefer to have tests for our code, so we use pytest_ you can also use by executing::
pytest miio
When adding support for a new device or extending an already existing one,
please do not forget to create tests for your code.
Generating documentation
~~~~~~~~~~~~~~~~~~~~~~~~
To install necessary packages to compile the documentation, run::
pip install -r requirements_docs.txt
After that, you can compile the documentation and open it locally in your browser::
cd docs
make html
$BROWSER _build/html/index.html
Replace `$BROWSER` with your preferred browser if the environment variable is not set.
Adding support for new devices
------------------------------
The `miio javascript library `__
contains some hints on devices which could be supported, however, the
Xiaomi Smart Home gateway (`Home Assistant
component `__ already work in
progress) as well as Yeelight bulbs are currently not in the scope of
this project.
.. TODO::
Add instructions how to extract protocol from network captures
Adding tests
------------
.. TODO::
Describe how to create tests.
This part of documentation needs your help!
Please consider submitting a pull request to update this.
Documentation
-------------
.. TODO::
Describe how to write documentation.
This part of documentation needs your help!
Please consider submitting a pull request to update this.
.. _virtualenv: https://virtualenv.pypa.io
.. _isort: https://github.com/timothycrosley/isort
.. _pipenv: https://github.com/pypa/pipenv
.. _tox: https://tox.readthedocs.io
.. _pytest: https://docs.pytest.org
.. _black: https://github.com/psf/black
.. _pip: https://pypi.org/project/pip/
.. _precommit: https://pre-commit.com
.. _flake8: http://flake8.pycqa.org
.. _doc8: https://pypi.org/project/doc8/
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1519088451.0
python-miio-0.5.0.1/docs/plug.rst 0000644 0001750 0001750 00000000355 00000000000 016220 0 ustar 00tpr tpr 0000000 0000000 Plug
====
.. todo::
Pull requests for documentation are welcome!
See :ref:`miplug --help ` for usage.
.. _miplug_help:
`miplug --help`
~~~~~~~~~~~~~~~
.. click:: miio.plug_cli:cli
:prog: miplug
:show-nested:
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/docs/troubleshooting.rst 0000644 0001750 0001750 00000005364 00000000000 020505 0 ustar 00tpr tpr 0000000 0000000 Troubleshooting
===============
Discover devices across subnets
-------------------------------
Discovering across different subnets may fail for some devices with the following exception:
.. code-block:: text
miio.exceptions.DeviceException: Unable to discover the device x.x.x.x
This behaviour has been experienced on the following device types:
- Xiaomi Zhimi Humidifier (aka ``zhimi.humidifier.v1``)
- Xiaomi Smartmi Evaporative Humidifier 2 (aka ``zhimi.humidifier.ca1``)
- Xiaomi IR Remote (aka ``chuangmi_ir``)
It's currently unclear if this is a bug or a security feature of the Xiaomi device.
.. note::
The root cause is the source address in the UDP packet. The device won't react/respond to the miIO request, in case the source address of the UDP packet doesn't belong to the subnet of the device itself. This behaviour was experienced and described in `github issue #422 `_.
Fortunately there are some workarounds to get the communication working.
The most obvious one would be placing the miIO client & MI device in the same subnet.
You can also dual-home your client and put it in multiple subnets.
This can be achieved either physically (e.g. multiple ethernet cables) or virtually (multiple VLAN's).
.. hint::
You might have had your reasons for multiple subnets and they're probably security-related. If so, remember to configure a local firewall on your client so that incoming connections from untrusted subnets are restricted.
If you're in control of the router in between, then you have one more chance to get the communication up & running.
You can configure IP masquearding on the outgoing routing interface for the subnet where the MI device resides.
IP masquerading (NAT) basically changes the source address in the UDP packet to the IP address of the
outbound routing interface.
.. note::
Read more about `Network address translation on Wikipedia `_.
Intermittent connection issues, timeouts (Xiaomi Vacuum)
--------------------------------------------------------
Blocking the network access from vacuums is known to cause connectivity problems, presenting themselves as connection timeouts (discussed in `github issue #92 `_):
.. code-block:: text
mirobo.device.DeviceException: Unable to discover the device x.x.x.x
The root cause lies in the software running on the device, which will hang when it is unable to receive responses.
The connectivity will get restored by device's internal watchdog restarting the service (``miio_client``).
.. hint::
If you want to keep your device out from the Internet, use REJECT instead of DROP in your firewall confinguration.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1549371166.0
python-miio-0.5.0.1/docs/vacuum.rst 0000644 0001750 0001750 00000013651 00000000000 016554 0 ustar 00tpr tpr 0000000 0000000 Vacuum
======
Following features of the vacuum cleaner are currently supported:
- Starting, stopping, pausing, locating.
- Controlling the fan speed.
- Fetching the current status.
- Fetching and reseting the state of consumables.
- Fetching and setting the schedules.
- Setting and querying the timezone.
- Installing sound packs.
- Installing firmware updates.
- Manual control of the robot. **Patches for a nicer API are very welcome.**
Use :ref:`mirobo --help `
for help on available commands and their parameters.
Usage examples
--------------
Status reporting
~~~~~~~~~~~~~~~~
::
$ mirobo
State: Charging
Battery: 100
Fanspeed: 60
Cleaning since: 0:00:00
Cleaned area: 0.0 m²
DND enabled: 0
Map present: 1
in_cleaning: 0
Start cleaning
~~~~~~~~~~~~~~
::
$ mirobo start
Starting cleaning: 0
Return home
~~~~~~~~~~~
::
$ mirobo home
Requesting return to home: 0
Setting the fanspeed
~~~~~~~~~~~~~~~~~~~~
::
$ mirobo fanspeed 30
Setting fan speed to 30
State of consumables
~~~~~~~~~~~~~~~~~~~~
::
$ mirobo consumables
main: 9:24:48, side: 9:24:48, filter: 9:24:48, sensor dirty: 1:27:12
Schedule information
~~~~~~~~~~~~~~~~~~~~
::
$ mirobo timer
Timer #0, id 1488667794112 (ts: 2017-03-04 23:49:54.111999)
49 22 * * 6
At 14:49 every Saturday
Timer #1, id 1488667777661 (ts: 2017-03-04 23:49:37.661000)
49 21 * * 3,4,5,6
At 13:49 every Wednesday, Thursday, Friday and Saturday
Timer #2, id 1488667756246 (ts: 2017-03-04 23:49:16.246000)
49 20 * * 0,1,2
At 12:49 every Sunday, Monday and Tuesday
Timer #3, id 1488667742238 (ts: 2017-03-04 23:49:02.237999)
49 19 * * 0,6
At 11:49 every Sunday and Saturday
Timer #4, id 1488667726378 (ts: 2017-03-04 23:48:46.378000)
48 18 * * 1,2,3,4,5
At 10:48 every Monday, Tuesday, Wednesday, Thursday and Friday
Timer #5, id 1488667715725 (ts: 2017-03-04 23:48:35.724999)
48 17 * * 0,1,2,3,4,5,6
At 09:48 every Sunday, Monday, Tuesday, Wednesday, Thursday, Friday and Saturday
Timer #6, id 1488667697356 (ts: 2017-03-04 23:48:17.355999)
48 16 5 3 *
At 08:48 on the 5th of March
Adding a new timer
::
$ mirobo timer add --cron '* * * * *'
Activating/deactivating an existing timer, use ``mirobo timer`` to get
the required id.
::
$ mirobo timer update [--enable|--disable]
Deleting a timer
::
$ mirobo timer delete
Cleaning history
~~~~~~~~~~~~~~~~
::
$ mirobo cleaning-history
Total clean count: 43
Clean #0: 2017-03-05 19:09:40-2017-03-05 19:09:50 (complete: False, unknown: 0)
Area cleaned: 0.0 m²
Duration: (0:00:00)
Clean #1: 2017-03-05 16:17:52-2017-03-05 17:14:59 (complete: False, unknown: 0)
Area cleaned: 32.16 m²
Duration: (0:23:54)
Sounds
~~~~~~
To get information about current sound settings:
::
mirobo sound
You can use dustcloud's `audio generator`_ to create your own language packs,
which will handle both generation and encrypting the package for you.
There are two ways to install install sound packs:
1. Install by using self-hosting server, where you just need to point the sound pack you want to install.
::
mirobo install-sound my_sounds.pkg
2. Install from an URL, in which case you need to pass the md5 hash of the file as a second parameter.
::
mirobo install-sound http://10.10.20.1:8000/my_sounds.pkg b50cfea27e52ebd5f46038ac7b9330c8
`--sid` can be used to select the sound ID (SID) for the new file,
using an existing SID will overwrite the old.
If the automatic detection of the IP address for self-hosting server is not working,
you can override this by using `--ip` option.
.. _audio generator: https://github.com/dgiese/dustcloud/tree/master/devices/xiaomi.vacuum/audio_generator
Firmware update
~~~~~~~~~~~~~~~
This can be useful if you want to downgrade or do updates without connecting to the cloud,
or if you want to use a custom rooted firmware.
`Dustcloud project `_ provides a way to generate your own firmware images,
and they also have `a firmware archive `_
for original firmwares.
.. WARNING::
Updating firmware should not be taken lightly even when the device will automatically roll-back
to the previous version when failing to do an update.
Using custom firmwares may hamper the functionality of your vacuum,
and it is unknown how the factory reset works in these cases.
This feature works similarly to the sound updates,
so passing a local file will create a self-hosting server
and updating from an URL requires you to pass the md5 hash of the file.
::
mirobo update-firmware v11_003094.pkg
DND functionality
~~~~~~~~~~~~~~~~~
To disable:
::
mirobo dnd off
To enable (dnd 22:00-0600):
::
mirobo dnd on 22 0 6 0
Carpet mode
~~~~~~~~~~~
Carpet mode increases the suction when encountering a carpet.
The optional parameters (when using miiocli) are unknown and set as
they were in the original firmware.
To enable:
::
mirobo carpet-mode 1 (or any other true-value, such as 'true')
To disable:
::
mirobo carpet-mode 0
Raw commands
~~~~~~~~~~~~
It is also possible to run raw commands, which can be useful
for testing new unknown commands or if you want to have full access
to what is being sent to the device:
::
mirobo raw-command app_start
or with parameters (same as above dnd on):
::
mirobo raw-command set_dnd_timer '[22,0,6,0]'
The input is passed as it is to the device as the `params` value,
so it is also possible to pass dicts.
.. NOTE::
If you find a new command please let us know by creating a pull request
or an issue, if you do not want to implement it on your own!
.. _HelpOutput:
`mirobo --help`
~~~~~~~~~~~~~~~
.. click:: miio.vacuum_cli:cli
:prog: mirobo
:show-nested:
:py:class:`API `
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/docs/yeelight.rst 0000644 0001750 0001750 00000004423 00000000000 017063 0 ustar 00tpr tpr 0000000 0000000 Yeelight
========
.. NOTE::
Only basic support for controlling Yeelight lights is implemented at the moment.
You will likely want to use `python-yeelight `_
for controlling your lights.
Currently supported features:
- Querying the status.
- Turning on and off.
- Changing brightness, colors (RGB and HSV), color temperature.
- Changing internal settings (developer mode, saving settings on change)
Use ``miiocli yeelight --help``
for help on available commands and their parameters.
To extract the token from a backup of the official Yeelight app, refer to :ref:`yeelight_token_extraction`.
.. _yeelight_token_extraction:
Token extraction
----------------
In order to extract tokens from the Yeelight Android app,
you need to create a backup like shown below.
.. code-block:: bash
adb backup -noapk com.yeelight.cherry -f backup.ab
If everything went fine and you got a ``backup.ab`` file,
from which you can extract the tokens with ``miio-extract-tokens`` as described in :ref:`token_extraction`.
.. code-block:: bash
miio-extract-tokens /tmp/yeelight.ab --password a
Unable to find miio database file apps/com.xiaomi.smarthome/db/miio2.db: "filename 'apps/com.xiaomi.smarthome/db/miio2.db' not found"
INFO:miio.extract_tokens:Trying to read apps/com.yeelight.cherry/sp/miot.xml
INFO:miio.extract_tokens:Reading tokens from Yeelight Android DB
Yeelight Color Bulb
Model: yeelink.light.color1
IP address: 192.168.xx.xx
Token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MAC: F0:B4:29:xx:xx:xx
Mi Bedside Lamp
Model: yeelink.light.bslamp1
IP address: 192.168.xx.xx
Token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MAC: 7C:49:EB:xx:xx:xx
Usage examples
--------------
Status reporting
~~~~~~~~~~~~~~~~
::
$ miiocli yeelight --ip 192.168.xx.xx --token xxxx status
Name:
Power: False
Brightness: 39
Color mode: 1
RGB: (255, 152, 0)
HSV: None
Temperature: None
Developer mode: True
Update default on change: True
.. NOTE::
If you find a new command please let us know by creating a pull request
or an issue, if you do not want to implement it on your own!
:py:class:`API `
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1585507846.5106072
python-miio-0.5.0.1/miio/ 0000755 0001750 0001750 00000000000 00000000000 014521 5 ustar 00tpr tpr 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500788.0
python-miio-0.5.0.1/miio/__init__.py 0000644 0001750 0001750 00000003474 00000000000 016642 0 ustar 00tpr tpr 0000000 0000000 # flake8: noqa
from miio.airconditioningcompanion import (
AirConditioningCompanion,
AirConditioningCompanionV3,
)
from miio.airdehumidifier import AirDehumidifier
from miio.airfresh import AirFresh
from miio.airfresh_t2017 import AirFreshT2017
from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1
from miio.airhumidifier_jsq import AirHumidifierJsq
from miio.airhumidifier_mjjsq import AirHumidifierMjjsq
from miio.airpurifier import AirPurifier
from miio.airpurifier_miot import AirPurifierMiot
from miio.airqualitymonitor import AirQualityMonitor
from miio.aqaracamera import AqaraCamera
from miio.ceil import Ceil
from miio.chuangmi_camera import ChuangmiCamera
from miio.chuangmi_ir import ChuangmiIr
from miio.chuangmi_plug import ChuangmiPlug, Plug, PlugV1, PlugV3
from miio.cooker import Cooker
from miio.device import Device
from miio.exceptions import DeviceError, DeviceException
from miio.fan import Fan, FanP5, FanSA1, FanV2, FanZA1, FanZA4
from miio.gateway import Gateway
from miio.heater import Heater
from miio.philips_bulb import PhilipsBulb, PhilipsWhiteBulb
from miio.philips_eyecare import PhilipsEyecare
from miio.philips_moonlight import PhilipsMoonlight
from miio.philips_rwread import PhilipsRwread
from miio.powerstrip import PowerStrip
from miio.protocol import Message, Utils
from miio.pwzn_relay import PwznRelay
from miio.toiletlid import Toiletlid
from miio.vacuum import Vacuum, VacuumException
from miio.vacuumcontainers import (
CleaningDetails,
CleaningSummary,
ConsumableStatus,
DNDStatus,
Timer,
VacuumStatus,
)
from miio.viomivacuum import ViomiVacuum
from miio.waterpurifier import WaterPurifier
from miio.wifirepeater import WifiRepeater
from miio.wifispeaker import WifiSpeaker
from miio.yeelight import Yeelight
from miio.discovery import Discovery
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/airconditioningcompanion.py 0000644 0001750 0001750 00000036405 00000000000 022167 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from typing import Optional
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_ACPARTNER_V1 = "lumi.acpartner.v1"
MODEL_ACPARTNER_V2 = "lumi.acpartner.v2"
MODEL_ACPARTNER_V3 = "lumi.acpartner.v3"
MODELS_SUPPORTED = [MODEL_ACPARTNER_V1, MODEL_ACPARTNER_V2, MODEL_ACPARTNER_V3]
class AirConditioningCompanionException(DeviceException):
pass
class OperationMode(enum.Enum):
Heat = 0
Cool = 1
Auto = 2
Dehumidify = 3
Ventilate = 4
class FanSpeed(enum.Enum):
Low = 0
Medium = 1
High = 2
Auto = 3
class SwingMode(enum.Enum):
On = "0"
Off = "1"
Unknown2 = "2"
Unknown7 = "7"
ChigoOn = "C"
ChigoOff = "D"
class Power(enum.Enum):
On = 1
Off = 0
class Led(enum.Enum):
On = "0"
Off = "A"
STORAGE_SLOT_ID = 30
POWER_OFF = "off"
# Command templates per model number (f.e. 0180111111)
# [po], [mo], [wi], [sw], [tt], [tt1], [tt4] and [tt7] are markers which will be replaced
DEVICE_COMMAND_TEMPLATES = {
"fallback": {"deviceType": "generic", "base": "[po][mo][wi][sw][tt][li]"},
"0100010727": {
"deviceType": "gree_2",
"base": "[po][mo][wi][sw][tt]1100190[tt1]205002102000[tt7]0190[tt1]207002000000[tt4]",
"off": "01011101004000205002112000D04000207002000000A0",
},
"0100004795": {
"deviceType": "gree_8",
"base": "[po][mo][wi][sw][tt][li]10009090000500",
},
"0180333331": {"deviceType": "haier_1", "base": "[po][mo][wi][sw][tt]1"},
"0180666661": {"deviceType": "aux_1", "base": "[po][mo][wi][sw][tt]1"},
"0180777771": {"deviceType": "chigo_1", "base": "[po][mo][wi][sw][tt]1"},
}
class AirConditioningCompanionStatus:
"""Container for status reports of the Xiaomi AC Companion."""
def __init__(self, data):
"""
Device model: lumi.acpartner.v2
Response of "get_model_and_state":
['010500978022222102', '010201190280222221', '2']
AC turned on by set_power=on:
['010507950000257301', '011001160100002573', '807']
AC turned off by set_power=off:
['010507950000257301', '010001160100002573', '6']
...
['010507950000257301', '010001160100002573', '1']
Example data payload:
{ 'model_and_state': ['010500978022222102', '010201190280222221', '2'],
'power_socket': 'on' }
"""
self.data = data
self.model = data["model_and_state"][0]
self.state = data["model_and_state"][1]
@property
def load_power(self) -> int:
"""Current power load of the air conditioner."""
return int(self.data["model_and_state"][2])
@property
def power_socket(self) -> Optional[str]:
"""Current socket power state."""
if "power_socket" in self.data and self.data["power_socket"] is not None:
return self.data["power_socket"]
return None
@property
def air_condition_model(self) -> bytes:
"""Model of the air conditioner."""
return bytes.fromhex(self.model)
@property
def model_format(self) -> int:
"""Version number of the model format."""
return self.air_condition_model[0]
@property
def device_type(self) -> int:
"""Device type identifier."""
return self.air_condition_model[1]
@property
def air_condition_brand(self) -> int:
"""
Brand of the air conditioner.
Known brand ids are 0x0182, 0x0097, 0x0037, 0x0202, 0x02782, 0x0197, 0x0192.
"""
return int(self.air_condition_model[2:4].hex(), 16)
@property
def air_condition_remote(self) -> int:
"""
Known remote ids:
0x80111111, 0x80111112 (brand: 0x0182)
0x80222221 (brand: 0x0097)
0x80333331 (brand: 0x0037)
0x80444441 (brand: 0x0202)
0x80555551 (brand: 0x2782)
0x80777771 (brand: 0x0197)
0x80666661 (brand: 0x0192)
"""
return int(self.air_condition_model[4:8].hex(), 16)
@property
def state_format(self) -> int:
"""
Version number of the state format.
Known values are: 1, 2, 3
"""
return int(self.air_condition_model[8])
@property
def air_condition_configuration(self) -> int:
return self.state[2:10]
@property
def power(self) -> str:
"""Current power state."""
return "on" if int(self.state[2:3]) == Power.On.value else "off"
@property
def led(self) -> Optional[bool]:
"""Current LED state."""
state = self.state[8:9]
if state == Led.On.value:
return True
if state == Led.Off.value:
return False
_LOGGER.info("Unsupported LED state: %s", state)
return None
@property
def is_on(self) -> bool:
"""True if the device is turned on."""
return self.power == "on"
@property
def target_temperature(self) -> Optional[int]:
"""Target temperature."""
try:
return int(self.state[6:8], 16)
except TypeError:
return None
@property
def swing_mode(self) -> Optional[SwingMode]:
"""Current swing mode."""
try:
mode = self.state[5:6]
return SwingMode(mode)
except TypeError:
return None
@property
def fan_speed(self) -> Optional[FanSpeed]:
"""Current fan speed."""
try:
speed = int(self.state[4:5])
return FanSpeed(speed)
except TypeError:
return None
@property
def mode(self) -> Optional[OperationMode]:
"""Current operation mode."""
try:
mode = int(self.state[3:4])
return OperationMode(mode)
except TypeError:
return None
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.power_socket,
self.load_power,
self.air_condition_model.hex(),
self.model_format,
self.device_type,
self.air_condition_brand,
self.air_condition_remote,
self.state_format,
self.air_condition_configuration,
self.led,
self.target_temperature,
self.swing_mode,
self.fan_speed,
self.mode,
)
)
return s
def __json__(self):
return self.data
class AirConditioningCompanion(Device):
"""Main class representing Xiaomi Air Conditioning Companion V1 and V2."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_ACPARTNER_V2,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in MODELS_SUPPORTED:
self.model = model
else:
self.model = MODEL_ACPARTNER_V2
_LOGGER.error(
"Device model %s unsupported. Falling back to %s.", model, self.model
)
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Load power: {result.load_power}\n"
"Air Condition model: {result.air_condition_model}\n"
"LED: {result.led}\n"
"Target temperature: {result.target_temperature} °C\n"
"Swing mode: {result.swing_mode}\n"
"Fan speed: {result.fan_speed}\n"
"Mode: {result.mode}\n",
)
)
def status(self) -> AirConditioningCompanionStatus:
"""Return device status."""
status = self.send("get_model_and_state")
return AirConditioningCompanionStatus(dict(model_and_state=status))
@command(default_output=format_output("Powering the air condition on"))
def on(self):
"""Turn the air condition on by infrared."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering the air condition off"))
def off(self):
"""Turn the air condition off by infrared."""
return self.send("set_power", ["off"])
@command(
click.argument("slot", type=int),
default_output=format_output(
"Learning infrared command into storage slot {slot}"
),
)
def learn(self, slot: int = STORAGE_SLOT_ID):
"""Learn an infrared command."""
return self.send("start_ir_learn", [slot])
@command(default_output=format_output("Reading learned infrared commands"))
def learn_result(self):
"""Read the learned command."""
return self.send("get_ir_learn_result")
@command(
click.argument("slot", type=int),
default_output=format_output(
"Learning infrared command into storage slot {slot} stopped"
),
)
def learn_stop(self, slot: int = STORAGE_SLOT_ID):
"""Stop learning of a infrared command."""
return self.send("end_ir_learn", [slot])
@command(
click.argument("model", type=str),
click.argument("code", type=str),
default_output=format_output("Sending the supplied infrared command"),
)
def send_ir_code(self, model: str, code: str, slot: int = 0):
"""Play a captured command.
:param str model: Air condition model
:param str code: Command to execute
:param int slot: Unknown internal register or slot
"""
try:
model = bytes.fromhex(model)
except ValueError:
raise AirConditioningCompanionException(
"Invalid model. A hexadecimal string must be provided"
)
try:
code = bytes.fromhex(code)
except ValueError:
raise AirConditioningCompanionException(
"Invalid code. A hexadecimal string must be provided"
)
if slot < 0 or slot > 134:
raise AirConditioningCompanionException("Invalid slot: %s" % slot)
slot = bytes([121 + slot])
# FE + 0487 + 00007145 + 9470 + 1FFF + 7F + FF + 06 + 0042 + 27 + 4E + 0025002D008500AC01...
command = (
code[0:1]
+ model[2:8]
+ b"\x94\x70\x1F\xFF"
+ slot
+ b"\xFF"
+ code[13:16]
+ b"\x27"
)
checksum = sum(command) & 0xFF
command = command + bytes([checksum]) + code[18:]
return self.send("send_ir_code", [command.hex().upper()])
@command(
click.argument("command", type=str),
default_output=format_output("Sending a command to the air conditioner"),
)
def send_command(self, command: str):
"""Send a command to the air conditioner.
:param str command: Command to execute"""
return self.send("send_cmd", [str(command)])
@command(
click.argument("model", type=str),
click.argument("power", type=EnumType(Power, False)),
click.argument("operation_mode", type=EnumType(OperationMode, False)),
click.argument("target_temperature", type=int),
click.argument("fan_speed", type=EnumType(FanSpeed, False)),
click.argument("swing_mode", type=EnumType(SwingMode, False)),
click.argument("led", type=EnumType(Led, False)),
default_output=format_output("Sending a configuration to the air conditioner"),
)
def send_configuration(
self,
model: str,
power: Power,
operation_mode: OperationMode,
target_temperature: int,
fan_speed: FanSpeed,
swing_mode: SwingMode,
led: Led,
):
prefix = str(model[0:2] + model[8:16])
suffix = model[-1:]
# Static turn off command available?
if (
(power is Power.Off)
and (prefix in DEVICE_COMMAND_TEMPLATES)
and (POWER_OFF in DEVICE_COMMAND_TEMPLATES[prefix])
):
return self.send_command(
prefix + DEVICE_COMMAND_TEMPLATES[prefix][POWER_OFF]
)
if prefix in DEVICE_COMMAND_TEMPLATES:
configuration = prefix + DEVICE_COMMAND_TEMPLATES[prefix]["base"]
else:
configuration = prefix + DEVICE_COMMAND_TEMPLATES["fallback"]["base"]
configuration = configuration.replace("[po]", str(power.value))
configuration = configuration.replace("[mo]", str(operation_mode.value))
configuration = configuration.replace("[wi]", str(fan_speed.value))
configuration = configuration.replace("[sw]", str(swing_mode.value))
configuration = configuration.replace("[tt]", format(target_temperature, "X"))
configuration = configuration.replace("[li]", str(led.value))
temperature = format((1 + target_temperature - 17) % 16, "X")
configuration = configuration.replace("[tt1]", temperature)
temperature = format((4 + target_temperature - 17) % 16, "X")
configuration = configuration.replace("[tt4]", temperature)
temperature = format((7 + target_temperature - 17) % 16, "X")
configuration = configuration.replace("[tt7]", temperature)
configuration = configuration + suffix
return self.send_command(configuration)
class AirConditioningCompanionV3(AirConditioningCompanion):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(
ip, token, start_id, debug, lazy_discover, model=MODEL_ACPARTNER_V3
)
@command(default_output=format_output("Powering socket on"))
def socket_on(self):
"""Socket power on."""
return self.send("toggle_plug", ["on"])
@command(default_output=format_output("Powering socket off"))
def socket_off(self):
"""Socket power off."""
return self.send("toggle_plug", ["off"])
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Power socket: {result.power_socket}\n"
"Load power: {result.load_power}\n"
"Air Condition model: {result.air_condition_model}\n"
"LED: {result.led}\n"
"Target temperature: {result.target_temperature} °C\n"
"Swing mode: {result.swing_mode}\n"
"Fan speed: {result.fan_speed}\n"
"Mode: {result.mode}\n",
)
)
def status(self) -> AirConditioningCompanionStatus:
"""Return device status."""
status = self.send("get_model_and_state")
power_socket = self.send("get_device_prop", ["lumi.0", "plug_state"])
return AirConditioningCompanionStatus(
dict(model_and_state=status, power_socket=power_socket[0])
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/airdehumidifier.py 0000644 0001750 0001750 00000023102 00000000000 020223 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from collections import defaultdict
from typing import Any, Dict, Optional
import click
from .click_common import EnumType, command, format_output
from .device import Device, DeviceInfo
from .exceptions import DeviceError, DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_DEHUMIDIFIER_V1 = "nwt.derh.wdh318efw1"
AVAILABLE_PROPERTIES = {
MODEL_DEHUMIDIFIER_V1: [
"on_off",
"mode",
"fan_st",
"buzzer",
"led",
"child_lock",
"humidity",
"temp",
"compressor_status",
"fan_speed",
"tank_full",
"defrost_status",
"alarm",
"auto",
]
}
class AirDehumidifierException(DeviceException):
pass
class OperationMode(enum.Enum):
On = "on"
Auto = "auto"
DryCloth = "dry_cloth"
class FanSpeed(enum.Enum):
Sleep = 0
Low = 1
Medium = 2
High = 3
Strong = 4
class AirDehumidifierStatus:
"""Container for status reports from the air dehumidifier."""
def __init__(self, data: Dict[str, Any], device_info: DeviceInfo) -> None:
"""
Response of a Air Dehumidifier (nwt.derh.wdh318efw1):
{'on_off': 'on', 'mode': 'auto', 'fan_st': 2,
'buzzer': 'off', 'led': 'on', 'child_lock': 'off',
'humidity': 47, 'temp': 34, 'compressor_status': 'off',
'fan_speed': 0, 'tank_full': 'off', 'defrost_status': 'off,
'alarm': 'ok','auto': 50}
"""
self.data = data
self.device_info = device_info
@property
def power(self) -> str:
"""Power state."""
return self.data["on_off"]
@property
def is_on(self) -> bool:
"""True if device is turned on."""
return self.power == "on"
@property
def mode(self) -> OperationMode:
"""Operation mode. Can be either on, auth or dry_cloth."""
return OperationMode(self.data["mode"])
@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if "temp" in self.data and self.data["temp"] is not None:
return self.data["temp"]
return None
@property
def humidity(self) -> int:
"""Current humidity."""
return self.data["humidity"]
@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["buzzer"] == "on"
@property
def led(self) -> bool:
"""LED brightness if available."""
return self.data["led"] == "on"
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"] == "on"
@property
def target_humidity(self) -> Optional[int]:
"""Target humiditiy. Can be either 40, 50, 60 percent."""
if "auto" in self.data and self.data["auto"] is not None:
return self.data["auto"]
return None
@property
def fan_speed(self) -> Optional[FanSpeed]:
"""Current fan speed."""
if "fan_speed" in self.data and self.data["fan_speed"] is not None:
return FanSpeed(self.data["fan_speed"])
return None
@property
def tank_full(self) -> bool:
"""The remaining amount of water in percent."""
return self.data["tank_full"] == "on"
@property
def compressor_status(self) -> bool:
"""Compressor status."""
return self.data["compressor_status"] == "on"
@property
def defrost_status(self) -> bool:
"""Defrost status."""
return self.data["defrost_status"] == "on"
@property
def fan_st(self) -> int:
"""Fan st."""
return self.data["fan_st"]
@property
def alarm(self) -> str:
"""Alarm."""
return self.data["alarm"]
def __repr__(self) -> str:
s = (
" None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_DEHUMIDIFIER_V1
self.device_info = None
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Mode: {result.mode}\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"Buzzer: {result.buzzer}\n"
"LED : {result.led}\n"
"Child lock: {result.child_lock}\n"
"Target humidity: {result.target_humidity} %\n"
"Fan speed: {result.fan_speed}\n"
"Tank Full: {result.tank_full}\n"
"Compressor Status: {result.compressor_status}\n"
"Defrost Status: {result.defrost_status}\n"
"Fan st: {result.fan_st}\n"
"Alarm: {result.alarm}\n",
)
)
def status(self) -> AirDehumidifierStatus:
"""Retrieve properties."""
if self.device_info is None:
self.device_info = self.info()
properties = AVAILABLE_PROPERTIES[self.model]
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:1]))
_props[:] = _props[1:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return AirDehumidifierStatus(
defaultdict(lambda: None, zip(properties, values)), self.device_info
)
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
try:
return self.send("set_mode", [mode.value])
except DeviceError as error:
# {'code': -6011, 'message': 'device_poweroff'}
if error.code == -6011:
self.on()
return self.send("set_mode", [mode.value])
raise
@command(
click.argument("fan_speed", type=EnumType(FanSpeed, False)),
default_output=format_output("Setting fan level to {fan_level}"),
)
def set_fan_speed(self, fan_speed: FanSpeed):
"""Set the fan speed."""
return self.send("set_fan_level", [fan_speed.value])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
if led:
return self.send("set_led", ["on"])
else:
return self.send("set_led", ["off"])
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if buzzer:
return self.send("set_buzzer", ["on"])
else:
return self.send("set_buzzer", ["off"])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])
@command(
click.argument("humidity", type=int),
default_output=format_output("Setting target humidity to {humidity}"),
)
def set_target_humidity(self, humidity: int):
"""Set the auto target humidity."""
if humidity not in [40, 50, 60]:
raise AirDehumidifierException(
"Invalid auto target humidity: %s" % humidity
)
return self.send("set_auto", [humidity])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584295509.0
python-miio-0.5.0.1/miio/airfilter_util.py 0000644 0001750 0001750 00000002515 00000000000 020114 0 ustar 00tpr tpr 0000000 0000000 import enum
import re
from typing import Optional
class FilterType(enum.Enum):
Regular = "regular"
AntiBacterial = "anti-bacterial"
AntiFormaldehyde = "anti-formaldehyde"
Unknown = "unknown"
FILTER_TYPE_RE = (
(re.compile(r"^\d+:\d+:41:30$"), FilterType.AntiBacterial),
(re.compile(r"^\d+:\d+:(30|0|00):31$"), FilterType.AntiFormaldehyde),
(re.compile(r".*"), FilterType.Regular),
)
class FilterTypeUtil:
"""Utility class for determining xiaomi air filter type."""
_filter_type_cache = {}
def determine_filter_type(
self, rfid_tag: Optional[str], product_id: Optional[str]
) -> Optional[FilterType]:
"""
Determine Xiaomi air filter type based on its product ID.
:param rfid_tag: RFID tag value
:param product_id: Product ID such as "0:0:30:33"
"""
if rfid_tag is None:
return None
if rfid_tag == "0:0:0:0:0:0:0":
return FilterType.Unknown
if product_id is None:
return FilterType.Regular
ft = self._filter_type_cache.get(product_id, None)
if ft is None:
for filter_re, filter_type in FILTER_TYPE_RE:
if filter_re.match(product_id):
ft = self._filter_type_cache[product_id] = filter_type
break
return ft
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/airfresh.py 0000644 0001750 0001750 00000022412 00000000000 016677 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from collections import defaultdict
from typing import Any, Dict, Optional
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
class AirFreshException(DeviceException):
pass
class OperationMode(enum.Enum):
# Supported modes of the Air Fresh VA2 (zhimi.airfresh.va2)
Auto = "auto"
Silent = "silent"
Interval = "interval"
Low = "low"
Middle = "middle"
Strong = "strong"
class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
class AirFreshStatus:
"""Container for status reports from the air fresh."""
def __init__(self, data: Dict[str, Any]) -> None:
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.power == "on"
@property
def aqi(self) -> int:
"""Air quality index."""
return self.data["aqi"]
@property
def average_aqi(self) -> int:
"""Average of the air quality index."""
return self.data["average_aqi"]
@property
def co2(self) -> int:
"""Carbon dioxide."""
return self.data["co2"]
@property
def humidity(self) -> int:
"""Current humidity."""
return self.data["humidity"]
@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if self.data["temp_dec"] is not None:
return self.data["temp_dec"] / 10.0
return None
@property
def mode(self) -> OperationMode:
"""Current operation mode."""
return OperationMode(self.data["mode"])
@property
def led(self) -> bool:
"""Return True if LED is on."""
return self.data["led"] == "on"
@property
def led_brightness(self) -> Optional[LedBrightness]:
"""Brightness of the LED."""
if self.data["led_level"] is not None:
try:
return LedBrightness(self.data["led_level"])
except ValueError:
_LOGGER.error(
"Unsupported LED brightness discarded: %s", self.data["led_level"]
)
return None
return None
@property
def buzzer(self) -> Optional[bool]:
"""Return True if buzzer is on."""
if self.data["buzzer"] is not None:
return self.data["buzzer"] == "on"
return None
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"] == "on"
@property
def filter_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["filter_life"]
@property
def filter_hours_used(self) -> int:
"""How long the filter has been in use."""
return self.data["f1_hour_used"]
@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
def motor_speed(self) -> int:
"""Speed of the motor."""
return self.data["motor1_speed"]
@property
def extra_features(self) -> Optional[int]:
return self.data["app_extra"]
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.aqi,
self.average_aqi,
self.temperature,
self.humidity,
self.co2,
self.mode,
self.led,
self.led_brightness,
self.buzzer,
self.child_lock,
self.filter_life_remaining,
self.filter_hours_used,
self.use_time,
self.motor_speed,
self.extra_features,
)
)
return s
def __json__(self):
return self.data
class AirFresh(Device):
"""Main class representing the air fresh."""
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"AQI: {result.aqi} μg/m³\n"
"Average AQI: {result.average_aqi} μg/m³\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"CO2: {result.co2} %\n"
"Mode: {result.mode.value}\n"
"LED: {result.led}\n"
"LED brightness: {result.led_brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Filter life remaining: {result.filter_life_remaining} %\n"
"Filter hours used: {result.filter_hours_used}\n"
"Use time: {result.use_time} s\n"
"Motor speed: {result.motor_speed} rpm\n",
)
)
def status(self) -> AirFreshStatus:
"""Retrieve properties."""
properties = [
"power",
"temp_dec",
"aqi",
"average_aqi",
"co2",
"buzzer",
"child_lock",
"humidity",
"led_level",
"mode",
"motor1_speed",
"use_time",
"ntcT",
"app_extra",
"f1_hour_used",
"filter_life",
"f_hour",
"favorite_level",
"led",
]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:15]))
_props[:] = _props[15:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return AirFreshStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.send("set_mode", [mode.value])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
if led:
return self.send("set_led", ["on"])
else:
return self.send("set_led", ["off"])
@command(
click.argument("brightness", type=EnumType(LedBrightness, False)),
default_output=format_output("Setting LED brightness to {brightness}"),
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
return self.send("set_led_level", [brightness.value])
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if buzzer:
return self.send("set_buzzer", ["on"])
else:
return self.send("set_buzzer", ["off"])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])
@command(
click.argument("value", type=int),
default_output=format_output("Setting extra to {value}"),
)
def set_extra_features(self, value: int):
"""Storage register to enable extra features at the app."""
if value < 0:
raise AirFreshException("Invalid app extra value: %s" % value)
return self.send("set_app_extra", [value])
@command(default_output=format_output("Resetting filter"))
def reset_filter(self):
"""Resets filter hours used and remaining life."""
return self.send("reset_filter1")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/airfresh_t2017.py 0000644 0001750 0001750 00000030141 00000000000 017532 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from collections import defaultdict
from typing import Any, Dict
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_AIRFRESH_T2017 = "dmaker.airfresh.t2017"
AVAILABLE_PROPERTIES = {
MODEL_AIRFRESH_T2017: [
"power",
"mode",
"pm25",
"co2",
"temperature_outside",
"favourite_speed",
"control_speed",
"filter_intermediate",
"filter_inter_day",
"filter_efficient",
"filter_effi_day",
"ptc_on",
"ptc_level",
"ptc_status",
"child_lock",
"sound",
"display",
"screen_direction",
]
}
class AirFreshException(DeviceException):
pass
class OperationMode(enum.Enum):
Off = "off"
Auto = "auto"
Sleep = "sleep"
Favorite = "favourite"
class PtcLevel(enum.Enum):
Off = "off"
Low = "low"
Medium = "medium"
High = "high"
class DisplayOrientation(enum.Enum):
Portrait = "forward"
LandscapeLeft = "left"
LandscapeRight = "right"
class AirFreshStatus:
"""Container for status reports from the air fresh t2017."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Air Airfresh T2017 (dmaker.airfresh.t2017):
{
'power': true,
'mode': "favourite",
'pm25': 1,
'co2': 550,
'temperature_outside': 24,
'favourite_speed': 241,
'control_speed': 241,
'filter_intermediate': 100,
'filter_inter_day': 90,
'filter_efficient': 100,
'filter_effi_day': 180,
'ptc_on': false,
'ptc_level': "low",
'ptc_status': false,
'child_lock': false,
'sound': true,
'display': false,
'screen_direction': "forward",
}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return "on" if self.data["power"] else "off"
@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.data["power"]
@property
def mode(self) -> OperationMode:
"""Current operation mode."""
return OperationMode(self.data["mode"])
@property
def pm25(self) -> int:
"""Fine particulate patter (PM2.5)."""
return self.data["pm25"]
@property
def co2(self) -> int:
"""Carbon dioxide."""
return self.data["co2"]
@property
def temperature(self) -> int:
"""Current temperature in degree celsions."""
return self.data["temperature_outside"]
@property
def favorite_speed(self) -> int:
"""Favorite speed."""
return self.data["favourite_speed"]
@property
def control_speed(self) -> int:
"""Control speed."""
return self.data["control_speed"]
@property
def dust_filter_life_remaining(self) -> int:
"""Remaining dust filter life in percent."""
return self.data["filter_intermediate"]
@property
def dust_filter_life_remaining_days(self) -> int:
"""Remaining dust filter life in days."""
return self.data["filter_inter_day"]
@property
def upper_filter_life_remaining(self) -> int:
"""Remaining upper filter life in percent."""
return self.data["filter_efficient"]
@property
def upper_filter_life_remaining_days(self) -> int:
"""Remaining upper filter life in days."""
return self.data["filter_effi_day"]
@property
def ptc(self) -> bool:
"""Return True if PTC is on."""
return self.data["ptc_on"]
@property
def ptc_level(self) -> int:
"""PTC level."""
return PtcLevel(self.data["ptc_level"])
@property
def ptc_status(self) -> bool:
"""Return true if PTC status is on."""
return self.data["ptc_status"]
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"]
@property
def buzzer(self) -> bool:
"""Return True if sound is on."""
return self.data["sound"]
@property
def display(self) -> bool:
"""Return True if the display is on."""
return self.data["display"]
@property
def display_orientation(self) -> int:
"""Display orientation."""
return DisplayOrientation(self.data["screen_direction"])
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.mode,
self.pm25,
self.co2,
self.temperature,
self.favorite_speed,
self.control_speed,
self.dust_filter_life_remaining,
self.dust_filter_life_remaining_days,
self.upper_filter_life_remaining,
self.upper_filter_life_remaining_days,
self.ptc,
self.ptc_level,
self.ptc_status,
self.child_lock,
self.buzzer,
self.display,
self.display_orientation,
)
)
return s
def __json__(self):
return self.data
class AirFreshT2017(Device):
"""Main class representing the air fresh t2017."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_AIRFRESH_T2017,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_AIRFRESH_T2017
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Mode: {result.mode}\n"
"PM2.5: {result.pm25}\n"
"CO2: {result.co2}\n"
"Temperature: {result.temperature}\n"
"Favorite speed: {result.favorite_speed}\n"
"Control speed: {result.control_speed}\n"
"Dust filter life: {result.dust_filter_life_remaining} %, "
"{result.dust_filter_life_remaining_days} days\n"
"Upper filter life remaining: {result.upper_filter_life_remaining} %, "
"{result.upper_filter_life_remaining_days} days\n"
"PTC: {result.ptc}\n"
"PTC level: {result.ptc_level}\n"
"PTC status: {result.ptc_status}\n"
"Child lock: {result.child_lock}\n"
"Buzzer: {result.buzzer}\n"
"Display: {result.display}\n"
"Display orientation: {result.display_orientation}\n",
)
)
def status(self) -> AirFreshStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:15]))
_props[:] = _props[15:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return AirFreshStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.send("set_mode", [mode.value])
@command(
click.argument("display", type=bool),
default_output=format_output(
lambda led: "Turning on display" if led else "Turning off display"
),
)
def set_display(self, display: bool):
"""Turn led on/off."""
if display:
return self.send("set_display", ["on"])
else:
return self.send("set_display", ["off"])
@command(
click.argument("orientation", type=EnumType(DisplayOrientation, False)),
default_output=format_output("Setting orientation to '{orientation.value}'"),
)
def set_display_orientation(self, orientation: DisplayOrientation):
"""Set display orientation."""
return self.send("set_screen_direction", [orientation.value])
@command(
click.argument("level", type=EnumType(PtcLevel, False)),
default_output=format_output("Setting ptc level to '{level.value}'"),
)
def set_ptc_level(self, level: PtcLevel):
"""Set PTC level."""
return self.send("set_ptc_level", [level.value])
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set sound on/off."""
if buzzer:
return self.send("set_sound", ["on"])
else:
return self.send("set_sound", ["off"])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])
@command(default_output=format_output("Resetting upper filter"))
def reset_upper_filter(self):
"""Resets filter lifetime of the upper filter."""
return self.send("set_filter_reset", ["efficient"])
@command(default_output=format_output("Resetting dust filter"))
def reset_dust_filter(self):
"""Resets filter lifetime of the dust filter."""
return self.send("set_filter_reset", ["intermediate"])
@command(
click.argument("speed", type=int),
default_output=format_output("Setting favorite speed to {speed}"),
)
def set_favorite_speed(self, speed: int):
"""Storage register to enable extra features at the app."""
if speed < 60 or speed > 300:
raise AirFreshException("Invalid favorite speed: %s" % speed)
return self.send("set_favourite_speed", [speed])
@command()
def set_ptc_timer(self):
"""
value = time.index + '-' +
time.hexSum + '-' +
time.startTime + '-' +
time.ptcTimer.endTime + '-' +
time.level + '-' +
time.status;
return self.send("set_ptc_timer", [value])
"""
raise NotImplementedError()
@command()
def get_ptc_timer(self):
"""Returns a list of PTC timers. Response unknown."""
return self.send("get_ptc_timer")
@command()
def get_timer(self):
"""Response unknown."""
return self.send("get_timer")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/airhumidifier.py 0000644 0001750 0001750 00000033137 00000000000 017723 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from collections import defaultdict
from typing import Any, Dict, Optional
import click
from .click_common import EnumType, command, format_output
from .device import Device, DeviceInfo
from .exceptions import DeviceError, DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_HUMIDIFIER_V1 = "zhimi.humidifier.v1"
MODEL_HUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
MODEL_HUMIDIFIER_CB1 = "zhimi.humidifier.cb1"
AVAILABLE_PROPERTIES_COMMON = [
"power",
"mode",
"humidity",
"buzzer",
"led_b",
"child_lock",
"limit_hum",
"use_time",
"hw_version",
]
AVAILABLE_PROPERTIES = {
MODEL_HUMIDIFIER_V1: AVAILABLE_PROPERTIES_COMMON
+ ["temp_dec", "trans_level", "button_pressed"],
MODEL_HUMIDIFIER_CA1: AVAILABLE_PROPERTIES_COMMON
+ ["temp_dec", "speed", "depth", "dry"],
MODEL_HUMIDIFIER_CB1: AVAILABLE_PROPERTIES_COMMON
+ ["temperature", "speed", "depth", "dry"],
}
class AirHumidifierException(DeviceException):
pass
class OperationMode(enum.Enum):
Silent = "silent"
Medium = "medium"
High = "high"
Auto = "auto"
Strong = "strong"
class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
class AirHumidifierStatus:
"""Container for status reports from the air humidifier."""
def __init__(self, data: Dict[str, Any], device_info: DeviceInfo) -> None:
"""
Response of a Air Humidifier (zhimi.humidifier.v1):
{'power': 'off', 'mode': 'high', 'temp_dec': 294,
'humidity': 33, 'buzzer': 'on', 'led_b': 0,
'child_lock': 'on', 'limit_hum': 40, 'trans_level': 85,
'speed': None, 'depth': None, 'dry': None, 'use_time': 941100,
'hw_version': 0, 'button_pressed': 'led'}
"""
self.data = data
self.device_info = device_info
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""True if device is turned on."""
return self.power == "on"
@property
def mode(self) -> OperationMode:
"""Operation mode. Can be either silent, medium or high."""
return OperationMode(self.data["mode"])
@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if "temp_dec" in self.data and self.data["temp_dec"] is not None:
return self.data["temp_dec"] / 10.0
if "temperature" in self.data and self.data["temperature"] is not None:
return self.data["temperature"]
return None
@property
def humidity(self) -> int:
"""Current humidity."""
return self.data["humidity"]
@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["buzzer"] == "on"
@property
def led_brightness(self) -> Optional[LedBrightness]:
"""LED brightness if available."""
if self.data["led_b"] is not None:
return LedBrightness(self.data["led_b"])
return None
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"] == "on"
@property
def target_humidity(self) -> int:
"""Target humidity. Can be either 30, 40, 50, 60, 70, 80 percent."""
return self.data["limit_hum"]
@property
def trans_level(self) -> Optional[int]:
"""
The meaning of the property is unknown.
The property is used to determine the strong mode is enabled on old firmware.
"""
if "trans_level" in self.data and self.data["trans_level"] is not None:
return self.data["trans_level"]
return None
@property
def strong_mode_enabled(self) -> bool:
if self.firmware_version_minor == 25:
if self.trans_level == 90:
return True
elif self.firmware_version_minor > 25 or self.firmware_version_minor == 0:
return self.mode.value == "strong"
return False
@property
def firmware_version(self) -> str:
"""Returns the fw_ver of miIO.info. For example 1.2.9_5033."""
return self.device_info.firmware_version
@property
def firmware_version_major(self) -> str:
parts = self.firmware_version.rsplit("_", 1)
return parts[0]
@property
def firmware_version_minor(self) -> int:
parts = self.firmware_version.rsplit("_", 1)
try:
return int(parts[1])
except IndexError:
return 0
@property
def motor_speed(self) -> Optional[int]:
"""Current fan speed."""
if "speed" in self.data and self.data["speed"] is not None:
return self.data["speed"]
return None
@property
def depth(self) -> Optional[int]:
"""The remaining amount of water in percent."""
if "depth" in self.data and self.data["depth"] is not None:
return self.data["depth"]
return None
@property
def dry(self) -> Optional[bool]:
"""
Dry mode: The amount of water is not enough to continue to work for about 8 hours.
Return True if dry mode is on if available.
"""
if "dry" in self.data and self.data["dry"] is not None:
return self.data["dry"] == "on"
return None
@property
def use_time(self) -> Optional[int]:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
def hardware_version(self) -> Optional[str]:
"""The hardware version."""
return self.data["hw_version"]
@property
def button_pressed(self) -> Optional[str]:
"""Last pressed button."""
if "button_pressed" in self.data and self.data["button_pressed"] is not None:
return self.data["button_pressed"]
return None
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.mode,
self.temperature,
self.humidity,
self.led_brightness,
self.buzzer,
self.child_lock,
self.target_humidity,
self.trans_level,
self.motor_speed,
self.depth,
self.dry,
self.use_time,
self.hardware_version,
self.button_pressed,
self.strong_mode_enabled,
self.firmware_version_major,
self.firmware_version_minor,
)
)
return s
def __json__(self):
return self.data
class AirHumidifier(Device):
"""Implementation of Xiaomi Mi Air Humidifier."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_HUMIDIFIER_V1,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_HUMIDIFIER_V1
self.device_info = None
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Mode: {result.mode}\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"LED brightness: {result.led_brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Target humidity: {result.target_humidity} %\n"
"Trans level: {result.trans_level}\n"
"Speed: {result.motor_speed}\n"
"Depth: {result.depth}\n"
"Dry: {result.dry}\n"
"Use time: {result.use_time}\n"
"Hardware version: {result.hardware_version}\n"
"Button pressed: {result.button_pressed}\n",
)
)
def status(self) -> AirHumidifierStatus:
"""Retrieve properties."""
if self.device_info is None:
self.device_info = self.info()
properties = AVAILABLE_PROPERTIES[self.model]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props_per_request = 15
# The CA1 and CB1 are limited to a single property per request
if self.model in [MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_CB1]:
_props_per_request = 1
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:_props_per_request]))
_props[:] = _props[_props_per_request:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return AirHumidifierStatus(
defaultdict(lambda: None, zip(properties, values)), self.device_info
)
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
try:
return self.send("set_mode", [mode.value])
except DeviceError as error:
# {'code': -6011, 'message': 'device_poweroff'}
if error.code == -6011:
self.on()
return self.send("set_mode", [mode.value])
raise
@command(
click.argument("brightness", type=EnumType(LedBrightness, False)),
default_output=format_output("Setting LED brightness to {brightness}"),
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
if self.model in [MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_CB1]:
return self.send("set_led_b", [str(brightness.value)])
return self.send("set_led_b", [brightness.value])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
if led:
return self.set_led_brightness(LedBrightness.Bright)
else:
return self.set_led_brightness(LedBrightness.Off)
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if buzzer:
return self.send("set_buzzer", ["on"])
else:
return self.send("set_buzzer", ["off"])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])
@command(
click.argument("humidity", type=int),
default_output=format_output("Setting target humidity to {humidity}"),
)
def set_target_humidity(self, humidity: int):
"""Set the target humidity."""
if humidity not in [30, 40, 50, 60, 70, 80]:
raise AirHumidifierException("Invalid target humidity: %s" % humidity)
return self.send("set_limit_hum", [humidity])
@command(
click.argument("dry", type=bool),
default_output=format_output(
lambda dry: "Turning on dry mode" if dry else "Turning off dry mode"
),
)
def set_dry(self, dry: bool):
"""Set dry mode on/off."""
if dry:
return self.send("set_dry", ["on"])
else:
return self.send("set_dry", ["off"])
class AirHumidifierCA1(AirHumidifier):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(
ip, token, start_id, debug, lazy_discover, model=MODEL_HUMIDIFIER_CA1
)
class AirHumidifierCB1(AirHumidifier):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(
ip, token, start_id, debug, lazy_discover, model=MODEL_HUMIDIFIER_CB1
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500788.0
python-miio-0.5.0.1/miio/airhumidifier_jsq.py 0000644 0001750 0001750 00000021067 00000000000 020577 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from typing import Any, Dict
import click
from .airhumidifier import AirHumidifierException
from .click_common import EnumType, command, format_output
from .device import Device
_LOGGER = logging.getLogger(__name__)
# Xiaomi Zero Fog Humidifier
MODEL_HUMIDIFIER_JSQ001 = "shuii.humidifier.jsq001"
# Array of properties in same order as in humidifier response
AVAILABLE_PROPERTIES = {
MODEL_HUMIDIFIER_JSQ001: [
"temperature", # (degrees, int)
"humidity", # (percentage, int)
"mode", # ( 0: Intelligent, 1: Level1, ..., 5:Level4)
"buzzer", # (0: off, 1: on)
"child_lock", # (0: off, 1: on)
"led_brightness", # (0: off, 1: low, 2: high)
"power", # (0: off, 1: on)
"no_water", # (0: enough, 1: add water)
"lid_opened", # (0: ok, 1: lid is opened)
]
}
class OperationMode(enum.Enum):
Intelligent = 0
Level1 = 1
Level2 = 2
Level3 = 3
Level4 = 4
class LedBrightness(enum.Enum):
Off = 0
Low = 1
High = 2
class AirHumidifierStatus:
"""Container for status reports from the air humidifier jsq."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Status of an Air Humidifier (shuii.humidifier.jsq001):
[24, 30, 1, 1, 0, 2, 0, 0, 0]
Parsed by AirHumidifierJsq device as:
{'temperature': 24, 'humidity': 29, 'mode': 1, 'buzzer': 1,
'child_lock': 0, 'led_brightness': 2, 'power': 0, 'no_water': 0,
'lid_opened': 0}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return "on" if self.data["power"] == 1 else "off"
@property
def is_on(self) -> bool:
"""True if device is turned on."""
return self.power == "on"
@property
def mode(self) -> OperationMode:
"""Operation mode. Can be either low, medium, high or humidity."""
try:
mode = OperationMode(self.data["mode"])
except ValueError as e:
_LOGGER.exception("Cannot parse mode: %s", e)
return OperationMode.Intelligent
return mode
@property
def temperature(self) -> int:
"""Current temperature in degree celsius."""
return self.data["temperature"]
@property
def humidity(self) -> int:
"""Current humidity in percent."""
return self.data["humidity"]
@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["buzzer"] == 1
@property
def led_brightness(self) -> LedBrightness:
"""Buttons illumination Brightness level."""
try:
brightness = LedBrightness(self.data["led_brightness"])
except ValueError as e:
_LOGGER.exception("Cannot parse brightness: %s", e)
return LedBrightness.Off
return brightness
@property
def led(self) -> bool:
"""True if LED is turned on."""
return self.led_brightness is not LedBrightness.Off
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"] == 1
@property
def no_water(self) -> bool:
"""True if the water tank is empty."""
return self.data["no_water"] == 1
@property
def lid_opened(self) -> bool:
"""True if the water tank is detached."""
return self.data["lid_opened"] == 1
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.mode,
self.temperature,
self.humidity,
self.led_brightness,
self.buzzer,
self.child_lock,
self.no_water,
self.lid_opened,
)
)
return s
def __json__(self):
return self.data
class AirHumidifierJsq(Device):
"""
Implementation of Xiaomi Zero Fog Humidifier: shuii.humidifier.jsq001
"""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_HUMIDIFIER_JSQ001,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
self.model = model if model in AVAILABLE_PROPERTIES else MODEL_HUMIDIFIER_JSQ001
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Mode: {result.mode}\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"Buzzer: {result.buzzer}\n"
"LED brightness: {result.led_brightness}\n"
"Child lock: {result.child_lock}\n"
"No water: {result.no_water}\n"
"Lid opened: {result.lid_opened}\n",
)
)
def status(self) -> AirHumidifierStatus:
"""Retrieve properties."""
values = self.send("get_props")
# Response of an Air Humidifier (shuii.humidifier.jsq001):
# [24, 37, 3, 1, 0, 2, 0, 0, 0]
#
# status[0] : temperature (degrees, int)
# status[1]: humidity (percentage, int)
# status[2]: mode ( 0: Intelligent, 1: Level1, ..., 5:Level4)
# status[3]: buzzer (0: off, 1: on)
# status[4]: lock (0: off, 1: on)
# status[5]: brightness (0: off, 1: low, 2: high)
# status[6]: power (0: off, 1: on)
# status[7]: water level state (0: ok, 1: add water)
# status[8]: lid state (0: ok, 1: lid is opened)
properties = AVAILABLE_PROPERTIES[self.model]
if len(properties) != len(values):
_LOGGER.error(
"Count (%s) of requested properties (%s) does not match the "
"count (%s) of received values (%s).",
len(properties),
properties,
len(values),
values,
)
return AirHumidifierStatus({k: v for k, v in zip(properties, values)})
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_start", [1])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_start", [0])
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
value = mode.value
if value not in (om.value for om in OperationMode):
raise AirHumidifierException(
"{} is not a valid OperationMode value".format(value)
)
return self.send("set_mode", [value])
@command(
click.argument("brightness", type=EnumType(LedBrightness, False)),
default_output=format_output("Setting LED brightness to {brightness}"),
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
value = brightness.value
if value not in (lb.value for lb in LedBrightness):
raise AirHumidifierException(
"{} is not a valid LedBrightness value".format(value)
)
return self.send("set_brightness", [value])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
brightness = LedBrightness.High if led else LedBrightness.Off
return self.set_led_brightness(brightness)
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
return self.send("set_buzzer", [int(bool(buzzer))])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
return self.send("set_lock", [int(bool(lock))])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/airhumidifier_mjjsq.py 0000644 0001750 0001750 00000015143 00000000000 021124 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from collections import defaultdict
from typing import Any, Dict
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_HUMIDIFIER_MJJSQ = "deerma.humidifier.mjjsq"
AVAILABLE_PROPERTIES = {
MODEL_HUMIDIFIER_MJJSQ: [
"OnOff_State",
"TemperatureValue",
"Humidity_Value",
"HumiSet_Value",
"Humidifier_Gear",
"Led_State",
"TipSound_State",
"waterstatus",
"watertankstatus",
]
}
class AirHumidifierException(DeviceException):
pass
class OperationMode(enum.Enum):
Low = 1
Medium = 2
High = 3
Humidity = 4
class AirHumidifierStatus:
"""Container for status reports from the air humidifier mjjsq."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Air Humidifier (deerma.humidifier.mjjsq):
{'Humidifier_Gear': 4, 'Humidity_Value': 44, 'HumiSet_Value': 54,
'Led_State': 1, 'OnOff_State': 0, 'TemperatureValue': 21,
'TipSound_State': 1, 'waterstatus': 1, 'watertankstatus': 1}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return "on" if self.data["OnOff_State"] == 1 else "off"
@property
def is_on(self) -> bool:
"""True if device is turned on."""
return self.power == "on"
@property
def mode(self) -> OperationMode:
"""Operation mode. Can be either low, medium, high or humidity."""
return OperationMode(self.data["Humidifier_Gear"])
@property
def temperature(self) -> int:
"""Current temperature in degree celsius."""
return self.data["TemperatureValue"]
@property
def humidity(self) -> int:
"""Current humidity in percent."""
return self.data["Humidity_Value"]
@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["TipSound_State"] == 1
@property
def led(self) -> bool:
"""True if LED is turned on."""
return self.data["Led_State"] == 1
@property
def target_humidity(self) -> int:
"""Target humiditiy in percent."""
return self.data["HumiSet_Value"]
@property
def no_water(self) -> bool:
"""True if the water tank is empty."""
return self.data["waterstatus"] == 0
@property
def water_tank_detached(self) -> bool:
"""True if the water tank is detached."""
return self.data["watertankstatus"] == 0
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.mode,
self.temperature,
self.humidity,
self.led,
self.buzzer,
self.target_humidity,
self.no_water,
self.water_tank_detached,
)
)
return s
def __json__(self):
return self.data
class AirHumidifierMjjsq(Device):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_HUMIDIFIER_MJJSQ,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_HUMIDIFIER_MJJSQ
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Mode: {result.mode}\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"LED: {result.led}\n"
"Buzzer: {result.buzzer}\n"
"Target humidity: {result.target_humidity} %\n"
"No water: {result.no_water}\n"
"Water tank detached: {result.water_tank_detached}\n",
)
)
def status(self) -> AirHumidifierStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:1]))
_props[:] = _props[1:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return AirHumidifierStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("Set_OnOff", [1])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("Set_OnOff", [0])
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.send("Set_HumidifierGears", [mode.value])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
return self.send("SetLedState", [int(led)])
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
return self.send("SetTipSound_Status", [int(buzzer)])
@command(
click.argument("humidity", type=int),
default_output=format_output("Setting target humidity to {humidity}"),
)
def set_target_humidity(self, humidity: int):
"""Set the target humidity in percent."""
if humidity < 0 or humidity > 99:
raise AirHumidifierException("Invalid target humidity: %s" % humidity)
return self.send("Set_HumiValue", [humidity])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/airpurifier.py 0000644 0001750 0001750 00000043312 00000000000 017417 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from collections import defaultdict
from typing import Any, Dict, Optional
import click
from .airfilter_util import FilterType, FilterTypeUtil
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
class AirPurifierException(DeviceException):
pass
class OperationMode(enum.Enum):
# Supported modes of the Air Purifier Pro, 2, V3
Auto = "auto"
Silent = "silent"
Favorite = "favorite"
# Additional supported modes of the Air Purifier 2 and V3
Idle = "idle"
# Additional supported modes of the Air Purifier V3
Medium = "medium"
High = "high"
Strong = "strong"
class SleepMode(enum.Enum):
Off = "poweroff"
Silent = "silent"
Idle = "idle"
class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
class AirPurifierStatus:
"""Container for status reports from the air purifier."""
_filter_type_cache = {}
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Air Purifier Pro (zhimi.airpurifier.v6):
{'power': 'off', 'aqi': 7, 'average_aqi': 18, 'humidity': 45,
'temp_dec': 234, 'mode': 'auto', 'favorite_level': 17,
'filter1_life': 52, 'f1_hour_used': 1664, 'use_time': 2642700,
'motor1_speed': 0, 'motor2_speed': 800, 'purify_volume': 62180,
'f1_hour': 3500, 'led': 'on', 'led_b': None, 'bright': 83,
'buzzer': None, 'child_lock': 'off', 'volume': 50,
'rfid_product_id': '0:0:41:30', 'rfid_tag': '80:52:86:e2:d8:86:4',
'act_sleep': 'close'}
Response of a Air Purifier 2 (zhimi.airpurifier.m1):
{'power': 'on, 'aqi': 10, 'average_aqi': 8, 'humidity': 62,
'temp_dec': 186, 'mode': 'auto', 'favorite_level': 10,
'filter1_life': 80, 'f1_hour_used': 682, 'use_time': 2457000,
'motor1_speed': 354, 'motor2_speed': None, 'purify_volume': 25262,
'f1_hour': 3500, 'led': 'off', 'led_b': 2, 'bright': None,
'buzzer': 'off', 'child_lock': 'off', 'volume': None,
'rfid_product_id': None, 'rfid_tag': None,
'act_sleep': 'close'}
Response of a Air Purifier V3 (zhimi.airpurifier.v3)
{'power': 'off', 'aqi': 0, 'humidity': None, 'temp_dec': None,
'mode': 'idle', 'led': 'off', 'led_b': 10, 'buzzer': 'on',
'child_lock': 'off', 'bright': 43, 'favorite_level': None,
'filter1_life': 26, 'f1_hour_used': 2573, 'use_time': None,
'motor1_speed': 0}
{'power': 'on', 'aqi': 18, 'humidity': None, 'temp_dec': None,
'mode': 'silent', 'led': 'off', 'led_b': 10, 'buzzer': 'on',
'child_lock': 'off', 'bright': 4, 'favorite_level': None,
'filter1_life': 26, 'f1_hour_used': 2574, 'use_time': None,
'motor1_speed': 648}
A request is limited to 16 properties.
"""
self.filter_type_util = FilterTypeUtil()
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.power == "on"
@property
def aqi(self) -> int:
"""Air quality index."""
return self.data["aqi"]
@property
def average_aqi(self) -> int:
"""Average of the air quality index."""
return self.data["average_aqi"]
@property
def humidity(self) -> int:
"""Current humidity."""
return self.data["humidity"]
@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if self.data["temp_dec"] is not None:
return self.data["temp_dec"] / 10.0
return None
@property
def mode(self) -> OperationMode:
"""Current operation mode."""
return OperationMode(self.data["mode"])
@property
def sleep_mode(self) -> Optional[SleepMode]:
"""Operation mode of the sleep state. (Idle vs. Silent)"""
if self.data["sleep_mode"] is not None:
return SleepMode(self.data["sleep_mode"])
return None
@property
def led(self) -> bool:
"""Return True if LED is on."""
return self.data["led"] == "on"
@property
def led_brightness(self) -> Optional[LedBrightness]:
"""Brightness of the LED."""
if self.data["led_b"] is not None:
try:
return LedBrightness(self.data["led_b"])
except ValueError:
return None
return None
@property
def illuminance(self) -> Optional[int]:
"""Environment illuminance level in lux [0-200].
Sensor value is updated only when device is turned on."""
return self.data["bright"]
@property
def buzzer(self) -> Optional[bool]:
"""Return True if buzzer is on."""
if self.data["buzzer"] is not None:
return self.data["buzzer"] == "on"
return None
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"] == "on"
@property
def favorite_level(self) -> int:
"""Return favorite level, which is used if the mode is ``favorite``."""
# Favorite level used when the mode is `favorite`.
return self.data["favorite_level"]
@property
def filter_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["filter1_life"]
@property
def filter_hours_used(self) -> int:
"""How long the filter has been in use."""
return self.data["f1_hour_used"]
@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
def purify_volume(self) -> int:
"""The volume of purified air in cubic meter."""
return self.data["purify_volume"]
@property
def motor_speed(self) -> int:
"""Speed of the motor."""
return self.data["motor1_speed"]
@property
def motor2_speed(self) -> Optional[int]:
"""Speed of the 2nd motor."""
return self.data["motor2_speed"]
@property
def volume(self) -> Optional[int]:
"""Volume of sound notifications [0-100]."""
return self.data["volume"]
@property
def filter_rfid_product_id(self) -> Optional[str]:
"""RFID product ID of installed filter."""
return self.data["rfid_product_id"]
@property
def filter_rfid_tag(self) -> Optional[str]:
"""RFID tag ID of installed filter."""
return self.data["rfid_tag"]
@property
def filter_type(self) -> Optional[FilterType]:
"""Type of installed filter."""
return self.filter_type_util.determine_filter_type(
self.filter_rfid_tag, self.filter_rfid_product_id
)
@property
def learn_mode(self) -> bool:
"""Return True if Learn Mode is enabled."""
return self.data["act_sleep"] == "single"
@property
def sleep_time(self) -> Optional[int]:
return self.data["sleep_time"]
@property
def sleep_mode_learn_count(self) -> Optional[int]:
return self.data["sleep_data_num"]
@property
def extra_features(self) -> Optional[int]:
return self.data["app_extra"]
@property
def turbo_mode_supported(self) -> Optional[bool]:
if self.data["app_extra"] is not None:
return self.data["app_extra"] == 1
return None
@property
def auto_detect(self) -> Optional[bool]:
"""Return True if auto detect is enabled."""
if self.data["act_det"] is not None:
return self.data["act_det"] == "on"
return None
@property
def button_pressed(self) -> Optional[str]:
"""Last pressed button."""
return self.data["button_pressed"]
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.aqi,
self.average_aqi,
self.temperature,
self.humidity,
self.mode,
self.led,
self.led_brightness,
self.illuminance,
self.buzzer,
self.child_lock,
self.favorite_level,
self.filter_life_remaining,
self.filter_hours_used,
self.use_time,
self.purify_volume,
self.motor_speed,
self.motor2_speed,
self.volume,
self.filter_rfid_product_id,
self.filter_rfid_tag,
self.filter_type,
self.learn_mode,
self.sleep_mode,
self.sleep_time,
self.sleep_mode_learn_count,
self.extra_features,
self.turbo_mode_supported,
self.auto_detect,
self.button_pressed,
)
)
return s
def __json__(self):
return self.data
class AirPurifier(Device):
"""Main class representing the air purifier."""
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"AQI: {result.aqi} μg/m³\n"
"Average AQI: {result.average_aqi} μg/m³\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"Mode: {result.mode.value}\n"
"LED: {result.led}\n"
"LED brightness: {result.led_brightness}\n"
"Illuminance: {result.illuminance} lx\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Favorite level: {result.favorite_level}\n"
"Filter life remaining: {result.filter_life_remaining} %\n"
"Filter hours used: {result.filter_hours_used}\n"
"Use time: {result.use_time} s\n"
"Purify volume: {result.purify_volume} m³\n"
"Motor speed: {result.motor_speed} rpm\n"
"Motor 2 speed: {result.motor2_speed} rpm\n"
"Sound volume: {result.volume} %\n"
"Filter RFID product id: {result.filter_rfid_product_id}\n"
"Filter RFID tag: {result.filter_rfid_tag}\n"
"Filter type: {result.filter_type}\n"
"Learn mode: {result.learn_mode}\n"
"Sleep mode: {result.sleep_mode}\n"
"Sleep time: {result.sleep_time}\n"
"Sleep mode learn count: {result.sleep_mode_learn_count}\n"
"AQI sensor enabled on power off: {result.auto_detect}\n",
)
)
def status(self) -> AirPurifierStatus:
"""Retrieve properties."""
properties = [
"power",
"aqi",
"average_aqi",
"humidity",
"temp_dec",
"mode",
"favorite_level",
"filter1_life",
"f1_hour_used",
"use_time",
"motor1_speed",
"motor2_speed",
"purify_volume",
"f1_hour",
"led",
# Second request
"led_b",
"bright",
"buzzer",
"child_lock",
"volume",
"rfid_product_id",
"rfid_tag",
"act_sleep",
"sleep_mode",
"sleep_time",
"sleep_data_num",
"app_extra",
"act_det",
"button_pressed",
]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:15]))
_props[:] = _props[15:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return AirPurifierStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.send("set_mode", [mode.value])
@command(
click.argument("level", type=int),
default_output=format_output("Setting favorite level to {level}"),
)
def set_favorite_level(self, level: int):
"""Set favorite level."""
if level < 0 or level > 17:
raise AirPurifierException("Invalid favorite level: %s" % level)
# Possible alternative property: set_speed_favorite
# Set the favorite level used when the mode is `favorite`,
# should be between 0 and 17.
return self.send("set_level_favorite", [level]) # 0 ... 17
@command(
click.argument("brightness", type=EnumType(LedBrightness, False)),
default_output=format_output("Setting LED brightness to {brightness}"),
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
return self.send("set_led_b", [brightness.value])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
if led:
return self.send("set_led", ["on"])
else:
return self.send("set_led", ["off"])
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if buzzer:
return self.send("set_buzzer", ["on"])
else:
return self.send("set_buzzer", ["off"])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])
@command(
click.argument("volume", type=int),
default_output=format_output("Setting sound volume to {volume}"),
)
def set_volume(self, volume: int):
"""Set volume of sound notifications [0-100]."""
if volume < 0 or volume > 100:
raise AirPurifierException("Invalid volume: %s" % volume)
return self.send("set_volume", [volume])
@command(
click.argument("learn_mode", type=bool),
default_output=format_output(
lambda learn_mode: "Turning on learn mode"
if learn_mode
else "Turning off learn mode"
),
)
def set_learn_mode(self, learn_mode: bool):
"""Set the Learn Mode on/off."""
if learn_mode:
return self.send("set_act_sleep", ["single"])
else:
return self.send("set_act_sleep", ["close"])
@command(
click.argument("auto_detect", type=bool),
default_output=format_output(
lambda auto_detect: "Turning on auto detect"
if auto_detect
else "Turning off auto detect"
),
)
def set_auto_detect(self, auto_detect: bool):
"""Set auto detect on/off. It's a feature of the AirPurifier V1 & V3"""
if auto_detect:
return self.send("set_act_det", ["on"])
else:
return self.send("set_act_det", ["off"])
@command(
click.argument("value", type=int),
default_output=format_output("Setting extra to {value}"),
)
def set_extra_features(self, value: int):
"""Storage register to enable extra features at the app.
app_extra=1 unlocks a turbo mode supported feature
"""
if value < 0:
raise AirPurifierException("Invalid app extra value: %s" % value)
return self.send("set_app_extra", [value])
@command(default_output=format_output("Resetting filter"))
def reset_filter(self):
"""Resets filter hours used and remaining life."""
return self.send("reset_filter1")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584295509.0
python-miio-0.5.0.1/miio/airpurifier_miot.py 0000644 0001750 0001750 00000030727 00000000000 020455 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from typing import Any, Dict, Optional
import click
from .airfilter_util import FilterType, FilterTypeUtil
from .click_common import EnumType, command, format_output
from .exceptions import DeviceException
from .miot_device import MiotDevice
_LOGGER = logging.getLogger(__name__)
_MAPPING = {
# Air Purifier (siid=2)
"power": {"siid": 2, "piid": 2},
"fan_level": {"siid": 2, "piid": 4},
"mode": {"siid": 2, "piid": 5},
# Environment (siid=3)
"humidity": {"siid": 3, "piid": 7},
"temperature": {"siid": 3, "piid": 8},
"aqi": {"siid": 3, "piid": 6},
# Filter (siid=4)
"filter_life_remaining": {"siid": 4, "piid": 3},
"filter_hours_used": {"siid": 4, "piid": 5},
# Alarm (siid=5)
"buzzer": {"siid": 5, "piid": 1},
"buzzer_volume": {"siid": 5, "piid": 2},
# Indicator Light (siid=6)
"led_brightness": {"siid": 6, "piid": 1},
"led": {"siid": 6, "piid": 6},
# Physical Control Locked (siid=7)
"child_lock": {"siid": 7, "piid": 1},
# Motor Speed (siid=10)
"favorite_level": {"siid": 10, "piid": 10},
"favorite_rpm": {"siid": 10, "piid": 7},
"motor_speed": {"siid": 10, "piid": 8},
# Use time (siid=12)
"use_time": {"siid": 12, "piid": 1},
# AQI (siid=13)
"purify_volume": {"siid": 13, "piid": 1},
"average_aqi": {"siid": 13, "piid": 2},
# RFID (siid=14)
"filter_rfid_tag": {"siid": 14, "piid": 1},
"filter_rfid_product_id": {"siid": 14, "piid": 3},
# Other (siid=15)
"app_extra": {"siid": 15, "piid": 1},
}
class AirPurifierMiotException(DeviceException):
pass
class OperationMode(enum.Enum):
Auto = 0
Silent = 1
Favorite = 2
Fan = 3
class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
class AirPurifierMiotStatus:
"""Container for status reports from the air purifier."""
def __init__(self, data: Dict[str, Any]) -> None:
self.filter_type_util = FilterTypeUtil()
self.data = data
@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.data["power"]
@property
def power(self) -> str:
"""Power state."""
return "on" if self.is_on else "off"
@property
def aqi(self) -> int:
"""Air quality index."""
return self.data["aqi"]
@property
def average_aqi(self) -> int:
"""Average of the air quality index."""
return self.data["average_aqi"]
@property
def humidity(self) -> int:
"""Current humidity."""
return self.data["humidity"]
@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if self.data["temperature"] is not None:
return self.data["temperature"]
return None
@property
def fan_level(self) -> int:
"""Current fan level."""
return self.data["fan_level"]
@property
def mode(self) -> OperationMode:
"""Current operation mode."""
return OperationMode(self.data["mode"])
@property
def led(self) -> bool:
"""Return True if LED is on."""
return self.data["led"]
@property
def led_brightness(self) -> Optional[LedBrightness]:
"""Brightness of the LED."""
if self.data["led_brightness"] is not None:
try:
return LedBrightness(self.data["led_brightness"])
except ValueError:
return None
return None
@property
def buzzer(self) -> Optional[bool]:
"""Return True if buzzer is on."""
if self.data["buzzer"] is not None:
return self.data["buzzer"]
return None
@property
def buzzer_volume(self) -> Optional[int]:
"""Return buzzer volume."""
if self.data["buzzer_volume"] is not None:
return self.data["buzzer_volume"]
return None
@property
def child_lock(self) -> bool:
"""Return True if child lock is on."""
return self.data["child_lock"]
@property
def favorite_level(self) -> int:
"""Return favorite level, which is used if the mode is ``favorite``."""
# Favorite level used when the mode is `favorite`.
return self.data["favorite_level"]
@property
def filter_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["filter_life_remaining"]
@property
def filter_hours_used(self) -> int:
"""How long the filter has been in use."""
return self.data["filter_hours_used"]
@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
def purify_volume(self) -> int:
"""The volume of purified air in cubic meter."""
return self.data["purify_volume"]
@property
def motor_speed(self) -> int:
"""Speed of the motor."""
return self.data["motor_speed"]
@property
def filter_rfid_product_id(self) -> Optional[str]:
"""RFID product ID of installed filter."""
return self.data["filter_rfid_product_id"]
@property
def filter_rfid_tag(self) -> Optional[str]:
"""RFID tag ID of installed filter."""
return self.data["filter_rfid_tag"]
@property
def filter_type(self) -> Optional[FilterType]:
"""Type of installed filter."""
return self.filter_type_util.determine_filter_type(
self.filter_rfid_tag, self.filter_rfid_product_id
)
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.aqi,
self.average_aqi,
self.temperature,
self.humidity,
self.fan_level,
self.mode,
self.led,
self.led_brightness,
self.buzzer,
self.buzzer_volume,
self.child_lock,
self.favorite_level,
self.filter_life_remaining,
self.filter_hours_used,
self.use_time,
self.purify_volume,
self.motor_speed,
self.filter_rfid_product_id,
self.filter_rfid_tag,
self.filter_type,
)
)
return s
def __json__(self):
return self.data
class AirPurifierMiot(MiotDevice):
"""Main class representing the air purifier which uses MIoT protocol."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover)
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"AQI: {result.aqi} μg/m³\n"
"Average AQI: {result.average_aqi} μg/m³\n"
"Humidity: {result.humidity} %\n"
"Temperature: {result.temperature} °C\n"
"Fan Level: {result.fan_level}\n"
"Mode: {result.mode}\n"
"LED: {result.led}\n"
"LED brightness: {result.led_brightness}\n"
"Buzzer: {result.buzzer}\n"
"Buzzer vol.: {result.buzzer_volume}\n"
"Child lock: {result.child_lock}\n"
"Favorite level: {result.favorite_level}\n"
"Filter life remaining: {result.filter_life_remaining} %\n"
"Filter hours used: {result.filter_hours_used}\n"
"Use time: {result.use_time} s\n"
"Purify volume: {result.purify_volume} m³\n"
"Motor speed: {result.motor_speed} rpm\n"
"Filter RFID product id: {result.filter_rfid_product_id}\n"
"Filter RFID tag: {result.filter_rfid_tag}\n"
"Filter type: {result.filter_type}\n",
)
)
def status(self) -> AirPurifierMiotStatus:
"""Retrieve properties."""
return AirPurifierMiotStatus(
{
prop["did"]: prop["value"] if prop["code"] == 0 else None
for prop in self.get_properties()
}
)
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.set_property("power", True)
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.set_property("power", False)
@command(
click.argument("level", type=int),
default_output=format_output("Setting fan level to '{level}'"),
)
def set_fan_level(self, level: int):
"""Set fan level."""
if level < 1 or level > 3:
raise AirPurifierMiotException("Invalid fan level: %s" % level)
return self.set_property("fan_level", level)
@command(
click.argument("rpm", type=int),
default_output=format_output("Setting favorite motor speed '{rpm}' rpm"),
)
def set_favorite_rpm(self, rpm: int):
"""Set favorite motor speed."""
# Note: documentation says the maximum is 2300, however, the purifier may return an error for rpm over 2200.
if rpm < 300 or rpm > 2300 or rpm % 10 != 0:
raise AirPurifierMiotException(
"Invalid favorite motor speed: %s. Must be between 300 and 2300 and divisible by 10"
% rpm
)
return self.set_property("favorite_rpm", rpm)
@command(
click.argument("volume", type=int),
default_output=format_output("Setting sound volume to {volume}"),
)
def set_volume(self, volume: int):
"""Set buzzer volume."""
if volume < 0 or volume > 100:
raise AirPurifierMiotException(
"Invalid volume: %s. Must be between 0 and 100" % volume
)
return self.set_property("buzzer_volume", volume)
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.set_property("mode", mode.value)
@command(
click.argument("level", type=int),
default_output=format_output("Setting favorite level to {level}"),
)
def set_favorite_level(self, level: int):
"""Set the favorite level used when the mode is `favorite`,
should be between 0 and 14.
"""
if level < 0 or level > 14:
raise AirPurifierMiotException("Invalid favorite level: %s" % level)
return self.set_property("favorite_level", level)
@command(
click.argument("brightness", type=EnumType(LedBrightness, False)),
default_output=format_output("Setting LED brightness to {brightness}"),
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
return self.set_property("led_brightness", brightness.value)
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
return self.set_property("led", led)
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
return self.set_property("buzzer", buzzer)
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
return self.set_property("child_lock", lock)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/airqualitymonitor.py 0000644 0001750 0001750 00000023657 00000000000 020704 0 ustar 00tpr tpr 0000000 0000000 import logging
from collections import defaultdict
from typing import Optional
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_AIRQUALITYMONITOR_V1 = "zhimi.airmonitor.v1"
MODEL_AIRQUALITYMONITOR_B1 = "cgllc.airmonitor.b1"
MODEL_AIRQUALITYMONITOR_S1 = "cgllc.airmonitor.s1"
AVAILABLE_PROPERTIES_COMMON = [
"power",
"aqi",
"battery",
"usb_state",
"time_state",
"night_state",
"night_beg_time",
"night_end_time",
"sensor_state",
]
AVAILABLE_PROPERTIES_B1 = ["co2e", "humidity", "pm25", "temperature", "tvoc"]
AVAILABLE_PROPERTIES_S1 = ["battery", "co2", "humidity", "pm25", "temperature", "tvoc"]
AVAILABLE_PROPERTIES = {
MODEL_AIRQUALITYMONITOR_V1: AVAILABLE_PROPERTIES_COMMON,
MODEL_AIRQUALITYMONITOR_B1: AVAILABLE_PROPERTIES_B1,
MODEL_AIRQUALITYMONITOR_S1: AVAILABLE_PROPERTIES_S1,
}
class AirQualityMonitorException(DeviceException):
pass
class AirQualityMonitorStatus:
"""Container of air quality monitor status."""
def __init__(self, data):
"""
Response of a Xiaomi Air Quality Monitor (zhimi.airmonitor.v1):
{'power': 'on', 'aqi': 34, 'battery': 100, 'usb_state': 'off', 'time_state': 'on'}
Response of a Xiaomi Air Quality Monitor (cgllc.airmonitor.b1):
{'co2e': 1466, 'humidity': 59.79999923706055, 'pm25': 2, 'temperature': 19.799999237060547,
'temperature_unit': 'c', 'tvoc': 1.3948699235916138, 'tvoc_unit': 'mg_m3'}
Response of a Xiaomi Air Quality Monitor (cgllc.airmonitor.s1):
{'battery': 100, 'co2': 695, 'humidity': 62.1, 'pm25': 19.4, 'temperature': 27.4,
'tvoc': 254}
"""
self.data = data
@property
def power(self) -> Optional[str]:
"""Current power state."""
return self.data.get("power", None)
@property
def is_on(self) -> bool:
"""Return True if the device is turned on."""
return self.power == "on"
@property
def usb_power(self) -> Optional[bool]:
"""Return True if the device's usb is on."""
if "usb_state" in self.data and self.data["usb_state"] is not None:
return self.data["usb_state"] == "on"
return None
@property
def aqi(self) -> Optional[int]:
"""Air quality index value. (0...600)."""
return self.data.get("aqi", None)
@property
def battery(self) -> Optional[int]:
"""Current battery level (0...100)."""
return self.data.get("battery", None)
@property
def display_clock(self) -> Optional[bool]:
"""Display a clock instead the AQI."""
if "time_state" in self.data and self.data["time_state"] is not None:
return self.data["time_state"] == "on"
return None
@property
def night_mode(self) -> Optional[bool]:
"""Return True if the night mode is on."""
if "night_state" in self.data and self.data["night_state"] is not None:
return self.data["night_state"] == "on"
return None
@property
def night_time_begin(self) -> Optional[str]:
"""Return the begin of the night time."""
return self.data.get("night_beg_time", None)
@property
def night_time_end(self) -> Optional[str]:
"""Return the end of the night time."""
return self.data.get("night_end_time", None)
@property
def sensor_state(self) -> Optional[str]:
"""Sensor state."""
return self.data.get("sensor_state", None)
@property
def co2(self) -> Optional[int]:
"""Return co2 value (400...9999ppm)."""
return self.data.get("co2", None)
@property
def co2e(self) -> Optional[int]:
"""Return co2e value (400...9999ppm)."""
return self.data.get("co2e", None)
@property
def humidity(self) -> Optional[float]:
"""Return humidity value (0...100%)."""
return self.data.get("humidity", None)
@property
def pm25(self) -> Optional[float]:
"""Return pm2.5 value (0...999μg/m³)."""
return self.data.get("pm25", None)
@property
def temperature(self) -> Optional[float]:
"""Return temperature value (-10...50°C)."""
return self.data.get("temperature", None)
@property
def tvoc(self) -> Optional[int]:
"""Return tvoc value."""
return self.data.get("tvoc", None)
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.usb_power,
self.battery,
self.aqi,
self.temperature,
self.humidity,
self.co2,
self.co2e,
self.pm25,
self.tvoc,
self.display_clock,
)
)
return s
def __json__(self):
return self.data
class AirQualityMonitor(Device):
"""Xiaomi PM2.5 Air Quality Monitor."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_AIRQUALITYMONITOR_V1,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
elif model is not None:
self.model = MODEL_AIRQUALITYMONITOR_V1
_LOGGER.error(
"Device model %s unsupported. Falling back to %s.", model, self.model
)
else:
"""Force autodetection"""
self.model = None
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"USB power: {result.usb_power}\n"
"Battery: {result.battery}\n"
"AQI: {result.aqi}\n"
"Temperature: {result.temperature}\n"
"Humidity: {result.humidity}\n"
"CO2: {result.co2}\n"
"CO2e: {result.co2e}\n"
"PM2.5: {result.pm25}\n"
"TVOC: {result.tvoc}\n"
"Display clock: {result.display_clock}\n",
)
)
def status(self) -> AirQualityMonitorStatus:
"""Return device status."""
if self.model is None:
"""Autodetection"""
info = self.info()
self.model = info.model
properties = AVAILABLE_PROPERTIES[self.model]
if self.model == MODEL_AIRQUALITYMONITOR_B1:
values = self.send("get_air_data")
else:
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
if (
self.model == MODEL_AIRQUALITYMONITOR_S1
or self.model == MODEL_AIRQUALITYMONITOR_B1
):
return AirQualityMonitorStatus(defaultdict(lambda: None, values))
else:
return AirQualityMonitorStatus(
defaultdict(lambda: None, zip(properties, values))
)
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("display_clock", type=bool),
default_output=format_output(
lambda led: "Turning on display clock"
if led
else "Turning off display clock"
),
)
def set_display_clock(self, display_clock: bool):
"""Enable/disable displaying a clock instead the AQI."""
if display_clock:
self.send("set_time_state", ["on"])
else:
self.send("set_time_state", ["off"])
@command(
click.argument("auto_close", type=bool),
default_output=format_output(
lambda led: "Turning on auto close" if led else "Turning off auto close"
),
)
def set_auto_close(self, auto_close: bool):
"""Purpose unknown."""
if auto_close:
self.send("set_auto_close", ["on"])
else:
self.send("set_auto_close", ["off"])
@command(
click.argument("night_mode", type=bool),
default_output=format_output(
lambda led: "Turning on night mode" if led else "Turning off night mode"
),
)
def set_night_mode(self, night_mode: bool):
"""Decrease the brightness of the display."""
if night_mode:
self.send("set_night_state", ["on"])
else:
self.send("set_night_state", ["off"])
@command(
click.argument("begin_hour", type=int),
click.argument("begin_minute", type=int),
click.argument("end_hour", type=int),
click.argument("end_minute", type=int),
default_output=format_output(
"Setting night time to {begin_hour}:{begin_minute} - {end_hour}:{end_minute}"
),
)
def set_night_time(
self, begin_hour: int, begin_minute: int, end_hour: int, end_minute: int
):
"""Enable night mode daily at bedtime."""
begin = begin_hour * 3600 + begin_minute * 60
end = end_hour * 3600 + end_minute * 60
if begin < 0 or begin > 86399 or end < 0 or end > 86399:
AirQualityMonitorException("Begin or/and end time invalid.")
self.send("set_night_time", [begin, end])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/alarmclock.py 0000644 0001750 0001750 00000021777 00000000000 017221 0 ustar 00tpr tpr 0000000 0000000 import enum
import time
import click
from .click_common import EnumType, command
from .device import Device
class HourlySystem(enum.Enum):
TwentyFour = 24
Twelve = 12
class AlarmType(enum.Enum):
Alarm = "alarm"
Reminder = "reminder"
Timer = "timer"
# TODO names for the tones
class Tone(enum.Enum):
First = "a1.mp3"
Second = "a2.mp3"
Third = "a3.mp3"
Fourth = "a4.mp3"
Fifth = "a5.mp3"
Sixth = "a6.mp3"
Seventh = "a7.mp3"
class Nightmode:
def __init__(self, data):
self._enabled = bool(data[0])
self._start = data[1]
self._end = data[2]
@property
def enabled(self) -> bool:
return self._enabled
@property
def start(self):
return self._start
@property
def end(self):
return self._end
def __repr__(self):
return "" % (self.enabled, self.start, self.end)
class RingTone:
def __init__(self, data):
# {'type': 'reminder', 'ringtone': 'a2.mp3', 'smart_clock': 0}]
self.type = AlarmType(data["type"])
self.tone = Tone(data["ringtone"])
self.smart_clock = data["smart_clock"]
def __repr__(self):
return "<%s %s tone: %s smart: %s>" % (
self.__class__.__name__,
self.type,
self.tone,
self.smart_clock,
)
def __str__(self):
return self.__repr__()
class AlarmClock(Device):
"""
Note, this device is not very responsive to the requests, so it may
take several seconds /tries to get an answer..
"""
@command()
def get_config_version(self):
"""
# values unknown {'result': [4], 'id': 203}
:return:
"""
return self.send("get_config_version", ["audio"])
@command()
def clock_system(self) -> HourlySystem:
"""Returns either 12 or 24 depending on which system is in use.
"""
return HourlySystem(self.send("get_hourly_system")[0])
@command(
click.argument("brightness", type=EnumType(HourlySystem, casesensitive=False))
)
def set_hourly_system(self, hs: HourlySystem):
return self.send("set_hourly_system", [hs.value])
@command()
def get_button_light(self):
"""Get button's light state."""
# ['normal', 'mute', 'offline'] or []
return self.send("get_enabled_key_light")
@command(click.argument("on", type=bool))
def set_button_light(self, on):
"""Enable or disable the button light."""
if on:
return self.send("enable_key_light") == ["OK"]
else:
return self.send("disable_key_light") == ["OK"]
@command()
def volume(self) -> int:
"""Return the volume.
-> 192.168.0.128 data= {"id":251,"method":"set_volume","params":[17]}
<- 192.168.0.57 data= {"result":["OK"],"id":251}
"""
return int(self.send("get_volume")[0])
@command(click.argument("volume", type=int))
def set_volume(self, volume):
"""Set volume [1,100]."""
return self.send("set_volume", [volume]) == ["OK"]
@command(
click.argument(
"alarm_type",
type=EnumType(AlarmType, casesensitive=False),
default=AlarmType.Alarm.name,
)
)
def get_ring(self, alarm_type: AlarmType):
"""Get current ring tone settings."""
return RingTone(self.send("get_ring", [{"type": alarm_type.value}]).pop())
@command(
click.argument("alarm_type", type=EnumType(AlarmType, casesensitive=False)),
click.argument("tone", type=EnumType(Tone, casesensitive=False)),
)
def set_ring(self, alarm_type: AlarmType, ring: RingTone):
"""Set alarm tone.
-> 192.168.0.128 data= {"id":236,"method":"set_ring",
"params":[{"ringtone":"a1.mp3","smart_clock":"","type":"alarm"}]}
<- 192.168.0.57 data= {"result":["OK"],"id":236}
"""
raise NotImplementedError()
# return self.send("set_ring", ) == ["OK"]
@command()
def night_mode(self):
"""Get night mode status.
-> 192.168.0.128 data= {"id":234,"method":"get_night_mode","params":[]}
<- 192.168.0.57 data= {"result":[0],"id":234}
"""
return Nightmode(self.send("get_night_mode"))
@command()
def set_night_mode(self):
"""Set the night mode.
# enable
-> 192.168.0.128 data= {"id":248,"method":"set_night_mode",
"params":[1,"21:00","6:00"]}
<- 192.168.0.57 data= {"result":["OK"],"id":248}
# disable
-> 192.168.0.128 data= {"id":249,"method":"set_night_mode",
"params":[0,"21:00","6:00"]}
<- 192.168.0.57 data= {"result":["OK"],"id":249}
"""
raise NotImplementedError()
@command()
def near_wakeup(self):
"""Status for near wakeup.
-> 192.168.0.128 data= {"id":235,"method":"get_near_wakeup_status",
"params":[]}
<- 192.168.0.57 data= {"result":["disable"],"id":235}
# setters
-> 192.168.0.128 data= {"id":254,"method":"set_near_wakeup_status",
"params":["enable"]}
<- 192.168.0.57 data= {"result":["OK"],"id":254}
-> 192.168.0.128 data= {"id":255,"method":"set_near_wakeup_status",
"params":["disable"]}
<- 192.168.0.57 data= {"result":["OK"],"id":255}
"""
return self.send("get_near_wakeup_status")
@command()
def countdown(self):
"""
-> 192.168.0.128 data= {"id":258,"method":"get_count_down_v2","params":[]}
"""
return self.send("get_count_down_v2")
@command()
def alarmops(self):
"""
NOTE: the alarm_ops method is the one used to create, query and delete
all types of alarms (reminders, alarms, countdowns).
-> 192.168.0.128 data= {"id":263,"method":"alarm_ops",
"params":{"operation":"create","data":[
{"type":"alarm","event":"testlabel","reminder":"","smart_clock":0,
"ringtone":"a2.mp3","volume":100,"circle":"once","status":"on",
"repeat_ringing":0,"delete_datetime":1564291980000,
"disable_datetime":"","circle_extra":"",
"datetime":1564291980000}
],"update_datetime":1564205639326}}
<- 192.168.0.57 data= {"result":[{"id":1,"ack":"OK"}],"id":263}
# query per index, starts from 0 instead of 1 as the ids it seems
-> 192.168.0.128 data= {"id":264,"method":"alarm_ops",
"params":{"operation":"query","req_type":"alarm",
"update_datetime":1564205639593,"index":0}}
<- 192.168.0.57 data= {"result":
[0,[
{"i":"1","c":"once","d":"2019-07-28T13:33:00+0800","s":"on",
"n":"testlabel","a":"a2.mp3","dd":1}
], "America/New_York"
],"id":264}
# result [code, list of alarms, timezone]
-> 192.168.0.128 data= {"id":265,"method":"alarm_ops",
"params":{"operation":"query","index":0,"update_datetime":1564205639596,
"req_type":"reminder"}}
<- 192.168.0.57 data= {"result":[0,[],"America/New_York"],"id":265}
"""
raise NotImplementedError()
@command(click.argument("url"))
def start_countdown(self, url):
"""Start countdown timer playing the given media.
{"id":354,"method":"alarm_ops",
"params":{"operation":"create","update_datetime":1564206432733,
"data":[{"type":"timer",
"background":"http://host.invalid/testfile.mp3",
"offset":1800,
"circle":"once",
"volume":100,
"datetime":1564208232733}]}}
"""
current_ts = int(time.time() * 1000)
payload = {
"operation": "create",
"update_datetime": current_ts,
"data": [
{
"type": "timer",
"background": "http://url_here_for_mp3",
"offset": 30,
"circle": "once",
"volume": 30,
"datetime": current_ts,
}
],
}
return self.send("alarm_ops", payload)
@command()
def query(self):
"""
-> 192.168.0.128 data= {"id":227,"method":"alarm_ops","params":
{"operation":"query","index":0,"update_datetime":1564205198413,"req_type":"reminder"}}
"""
payload = {
"operation": "query",
"index": 0,
"update_datetime": int(time.time() * 1000),
"req_type": "timer",
}
return self.send("alarm_ops", payload)
@command()
def cancel(self):
"""Cancel alarm of the defined type.
"params":{"operation":"cancel","update_datetime":1564206332603,"data":[{"type":"timer"}]}}
"""
import time
payload = {
"operation": "pause",
"update_datetime": int(time.time() * 1000),
"data": [{"type": "timer"}],
}
return self.send("alarm_ops", payload)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/aqaracamera.py 0000644 0001750 0001750 00000022777 00000000000 017350 0 ustar 00tpr tpr 0000000 0000000 """Aqara camera support.
Support for lumi.camera.aq1
.. todo:: add alarm/sound parts (get_music_info, {get,set}_alarming_volume,
set_default_music, play_music_new, set_sound_playing)
.. todo:: add sdcard status & fix all TODOS
.. todo:: add tests
"""
import logging
from enum import IntEnum
from typing import Any, Dict
import attr
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
class CameraException(DeviceException):
pass
@attr.s
class CameraOffset:
"""Container for camera offset data."""
x = attr.ib()
y = attr.ib()
radius = attr.ib()
@attr.s
class ArmStatus:
"""Container for arm statuses."""
is_armed = attr.ib(converter=bool)
arm_wait_time = attr.ib(converter=int)
alarm_volume = attr.ib(converter=int)
class SDCardStatus(IntEnum):
"""State of the SD card."""
NoCardInserted = 0
Ok = 1
FormatRequired = 2
Formating = 3
class MotionDetectionSensitivity(IntEnum):
"""'Default' values for md sensitivity.
Currently unused as the value can also be set arbitrarily.
"""
High = 6000000
Medium = 10000000
Low = 11000000
class CameraStatus:
"""Container for status reports from the Aqara Camera."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a lumi.camera.aq1:
{"p2p_id":"#################","app_type":"celing",
"offset_x":"0","offset_y":"0","offset_radius":"0",
"md_status":1,"video_state":1,"fullstop":0,
"led_status":1,"ir_status":1,"mdsensitivity":6000000,
"channel_id":0,"flip_state":0,
"avID":"####","avPass":"####","id":65001}
"""
self.data = data
@property
def type(self) -> str:
"""TODO: Type of the camera? Name?"""
return self.data["app_type"]
@property
def video_status(self) -> bool:
"""Video state."""
return bool(self.data["video_state"])
@property
def is_on(self) -> bool:
"""True if device is currently on."""
return self.video_status == 1
@property
def md(self) -> bool:
"""Motion detection state."""
return bool(self.data["md_status"])
@property
def md_sensitivity(self):
"""Motion detection sensitivity."""
return self.data["mdsensitivity"]
@property
def ir(self):
"""IR mode."""
return bool(self.data["ir_status"])
@property
def led(self):
"""LED status."""
return bool(self.data["led_status"])
@property
def flipped(self) -> bool:
"""TODO: If camera is flipped?"""
return self.data["flip_state"]
@property
def offsets(self) -> CameraOffset:
"""Camera offset information."""
return CameraOffset(
x=self.data["offset_x"],
y=self.data["offset_y"],
radius=self.data["offset_radius"],
)
@property
def channel_id(self) -> int:
"""TODO: Zigbee channel?"""
return self.data["channel_id"]
@property
def fullstop(self) -> bool:
"""Is alarm triggered by MD."""
return self.data["fullstop"] != 0
@property
def p2p_id(self) -> str:
"""P2P ID for video and audio."""
return self.data["p2p_id"]
@property
def av_id(self) -> str:
"""TODO: What is this? ID for the cloud?"""
return self.data["avID"]
@property
def av_password(self) -> str:
"""TODO: What is this? Password for the cloud?"""
return self.data["avPass"]
def __repr__(self) -> str:
s = (
""
% (
self.is_on,
self.type,
self.offsets,
self.ir,
self.md,
self.md_sensitivity,
self.led,
self.flipped,
self.fullstop,
)
)
return s
def __json__(self):
return self.data
class AqaraCamera(Device):
"""Main class representing the Xiaomi Aqara Camera."""
@command(
default_output=format_output(
"",
"Type: {result.type}\n"
"Video: {result.is_on}\n"
"Offsets: {result.offsets}\n"
"IR: {result.ir_status} %\n"
"MD: {result.md_status} (sensitivity: {result.md_sensitivity}\n"
"LED: {result.led}\n"
"Flipped: {result.flipped}\n"
"Full stop: {result.fullstop}\n"
"P2P ID: {result.p2p_id}\n"
"AV ID: {result.av_id}\n"
"AV password: {result.av_password}\n"
"\n",
)
)
def status(self) -> CameraStatus:
"""Camera status."""
return CameraStatus(self.send("get_ipcprop", ["all"]))
@command(default_output=format_output("Camera on"))
def on(self):
"""Camera on."""
return self.send("set_video", ["on"])
@command(default_output=format_output("Camera off"))
def off(self):
"""Camera off."""
return self.send("set_video", ["off"])
@command(default_output=format_output("IR on"))
def ir_on(self):
"""IR on."""
return self.send("set_ir", ["on"])
@command(default_output=format_output("IR off"))
def ir_off(self):
"""IR off."""
return self.send("set_ir", ["off"])
@command(default_output=format_output("MD on"))
def md_on(self):
"""IR on."""
return self.send("set_md", ["on"])
@command(default_output=format_output("MD off"))
def md_off(self):
"""MD off."""
return self.send("set_md", ["off"])
@command(click.argument("sensitivity", type=int, required=False))
def md_sensitivity(self, sensitivity):
"""Get or set the motion detection sensitivity."""
if sensitivity:
click.echo("Setting MD sensitivity to %s" % sensitivity)
return self.send("set_mdsensitivity", [sensitivity])[0] == "ok"
else:
return self.send("get_mdsensitivity")
@command(default_output=format_output("LED on"))
def led_on(self):
"""LED on."""
return self.send("set_led", ["on"])
@command(default_output=format_output("LED off"))
def led_off(self):
"""LED off."""
return self.send("set_led", ["off"])
@command(default_output=format_output("Flip on"))
def flip_on(self):
"""Flip on."""
return self.send("set_flip", ["on"])
@command(default_output=format_output("Flip off"))
def flip_off(self):
"""Flip off."""
return self.send("set_flip", ["off"])
@command(default_output=format_output("Fullstop on"))
def fullstop_on(self):
"""Fullstop on."""
return self.send("set_fullstop", ["on"])
@command(default_output=format_output("Fullstop off"))
def fullstop_off(self):
"""Fullstop off."""
return self.send("set_fullstop", ["off"])
@command(
click.argument("time", type=int, default=30),
default_output=format_output("Start pairing for {time} seconds"),
)
def pair(self, timeout: int):
"""Start (or stop with "0") pairing."""
if timeout < 0:
raise CameraException("Invalid timeout: %s" % timeout)
return self.send("start_zigbee_join", [timeout])
@command()
def sd_status(self):
"""SD card status."""
return SDCardStatus(self.send("get_sdstatus"))
@command()
def sd_format(self):
"""Format the SD card.
Returns True when formating has started successfully.
"""
return bool(self.send("sdformat"))
@command()
def arm_status(self):
"""Return arming information."""
is_armed = self.send("get_arming")
arm_wait_time = self.send("get_arm_wait_time")
alarm_volume = self.send("get_alarming_volume")
return ArmStatus(
is_armed=bool(is_armed),
arm_wait_time=arm_wait_time,
alarm_volume=alarm_volume,
)
@command(
click.argument("volume", type=int, default=100),
default_output=format_output("Setting alarm volume to {volume}"),
)
def set_alarm_volume(self, volume):
"""Set alarm volume."""
if volume < 0 or volume > 100:
raise CameraException("Volume has to be [0,100], was %s" % volume)
return self.send("set_alarming_volume", [volume])[0] == "ok"
@command(click.argument("sound_id", type=str, required=False, default=None))
def alarm_sound(self, sound_id):
"""List or set the alarm sound."""
if id is None:
sound_status = self.send("get_music_info", [0])
# TODO: make a list out from this.
@attr.s
class SoundList:
default = attr.ib()
total = attr.ib(type=int)
sounds = attr.ib(type=list)
return sound_status
click.echo("Setting alarm sound to %s" % sound_id)
return self.send("set_default_music", [0, sound_id])[0] == "ok"
@command(default_output=format_output("Arming"))
def arm(self):
"""Arm the camera?"""
return self.send("set_arming", ["on"])
@command(default_output=format_output("Disarming"))
def disarm(self):
"""Disarm the camera?"""
return self.send("set_arming", ["off"])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/ceil.py 0000644 0001750 0001750 00000015634 00000000000 016020 0 ustar 00tpr tpr 0000000 0000000 import logging
from collections import defaultdict
from typing import Any, Dict
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
class CeilException(DeviceException):
pass
class CeilStatus:
"""Container for status reports from Xiaomi Philips LED Ceiling Lamp."""
def __init__(self, data: Dict[str, Any]) -> None:
# {'power': 'off', 'bright': 0, 'snm': 4, 'dv': 0,
# 'cctsw': [[0, 3], [0, 2], [0, 1]], 'bl': 1,
# 'mb': 1, 'ac': 1, 'mssw': 1, 'cct': 99}
# NOTE: Only 8 properties can be requested at the same time
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""True if the device is turned on."""
return self.power == "on"
@property
def brightness(self) -> int:
"""Current brightness."""
return self.data["bright"]
@property
def scene(self) -> int:
"""Current fixed scene (brightness & colortemp)."""
return self.data["snm"]
@property
def delay_off_countdown(self) -> int:
"""Countdown until turning off in seconds."""
return self.data["dv"]
@property
def color_temperature(self) -> int:
"""Current color temperature."""
return self.data["cct"]
@property
def smart_night_light(self) -> bool:
"""Smart night mode state."""
return self.data["bl"] == 1
@property
def automatic_color_temperature(self) -> bool:
"""Automatic color temperature state."""
return self.data["ac"] == 1
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.brightness,
self.color_temperature,
self.scene,
self.delay_off_countdown,
self.smart_night_light,
self.automatic_color_temperature,
)
)
return s
def __json__(self):
return self.data
class Ceil(Device):
"""Main class representing Xiaomi Philips LED Ceiling Lamp."""
# TODO: - Auto On/Off Not Supported
# - Adjust Scenes with Wall Switch Not Supported
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Brightness: {result.brightness}\n"
"Color temperature: {result.color_temperature}\n"
"Scene: {result.scene}\n"
"Delayed turn off: {result.delay_off_countdown}\n"
"Smart night light: {result.smart_night_light}\n"
"Automatic color temperature: {result.automatic_color_temperature}\n",
)
)
def status(self) -> CeilStatus:
"""Retrieve properties."""
properties = ["power", "bright", "cct", "snm", "dv", "bl", "ac"]
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return CeilStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering on"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("level", type=int),
default_output=format_output("Setting brightness to {level}"),
)
def set_brightness(self, level: int):
"""Set brightness level."""
if level < 1 or level > 100:
raise CeilException("Invalid brightness: %s" % level)
return self.send("set_bright", [level])
@command(
click.argument("level", type=int),
default_output=format_output("Setting color temperature to {level}"),
)
def set_color_temperature(self, level: int):
"""Set Correlated Color Temperature."""
if level < 1 or level > 100:
raise CeilException("Invalid color temperature: %s" % level)
return self.send("set_cct", [level])
@command(
click.argument("brightness", type=int),
click.argument("cct", type=int),
default_output=format_output(
"Setting brightness to {brightness} and color temperature to {cct}"
),
)
def set_brightness_and_color_temperature(self, brightness: int, cct: int):
"""Set brightness level and the correlated color temperature."""
if brightness < 1 or brightness > 100:
raise CeilException("Invalid brightness: %s" % brightness)
if cct < 1 or cct > 100:
raise CeilException("Invalid color temperature: %s" % cct)
return self.send("set_bricct", [brightness, cct])
@command(
click.argument("seconds", type=int),
default_output=format_output("Setting delayed turn off to {seconds} seconds"),
)
def delay_off(self, seconds: int):
"""Turn off delay in seconds."""
if seconds < 1:
raise CeilException("Invalid value for a delayed turn off: %s" % seconds)
return self.send("delay_off", [seconds])
@command(
click.argument("number", type=int),
default_output=format_output("Setting fixed scene to {number}"),
)
def set_scene(self, number: int):
"""Set a fixed scene. 4 fixed scenes are available (1-4)"""
if number < 1 or number > 4:
raise CeilException("Invalid fixed scene number: %s" % number)
return self.send("apply_fixed_scene", [number])
@command(default_output=format_output("Turning on smart night light"))
def smart_night_light_on(self):
"""Smart Night Light On."""
return self.send("enable_bl", [1])
@command(default_output=format_output("Turning off smart night light"))
def smart_night_light_off(self):
"""Smart Night Light off."""
return self.send("enable_bl", [0])
@command(default_output=format_output("Turning on automatic color temperature"))
def automatic_color_temperature_on(self):
"""Automatic color temperature on."""
return self.send("enable_ac", [1])
@command(default_output=format_output("Turning off automatic color temperature"))
def automatic_color_temperature_off(self):
"""Automatic color temperature off."""
return self.send("enable_ac", [0])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/ceil_cli.py 0000644 0001750 0001750 00000010537 00000000000 016644 0 ustar 00tpr tpr 0000000 0000000 import logging
import sys
import click
import miio # noqa: E402
from miio.click_common import ExceptionHandlerGroup, validate_ip, validate_token
from miio.miioprotocol import MiIOProtocol
_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.Ceil)
def validate_percentage(ctx, param, value):
value = int(value)
if value < 1 or value > 100:
raise click.BadParameter("Should be a positive int between 1-100.")
return value
def validate_seconds(ctx, param, value):
value = int(value)
if value < 0 or value > 21600:
raise click.BadParameter("Should be a positive int between 1-21600.")
return value
def validate_scene(ctx, param, value):
value = int(value)
if value < 1 or value > 4:
raise click.BadParameter("Should be a positive int between 1-4.")
return value
@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup)
@click.option("--ip", envvar="DEVICE_IP", callback=validate_ip)
@click.option("--token", envvar="DEVICE_TOKEN", callback=validate_token)
@click.option("-d", "--debug", default=False, count=True)
@click.pass_context
def cli(ctx, ip: str, token: str, debug: int):
"""A tool to command Xiaomi Philips LED Ceiling Lamp."""
if debug:
logging.basicConfig(level=logging.DEBUG)
_LOGGER.info("Debug mode active")
else:
logging.basicConfig(level=logging.INFO)
# if we are scanning, we do not try to connect.
if ctx.invoked_subcommand == "discover":
return
if ip is None or token is None:
click.echo("You have to give ip and token!")
sys.exit(-1)
dev = miio.Ceil(ip, token, debug)
_LOGGER.debug("Connecting to %s with token %s", ip, token)
ctx.obj = dev
if ctx.invoked_subcommand is None:
ctx.invoke(status)
@cli.command()
def discover():
"""Search for plugs in the network."""
MiIOProtocol.discover()
@cli.command()
@pass_dev
def status(dev: miio.Ceil):
"""Returns the state information."""
res = dev.status()
if not res:
return # bail out
click.echo(click.style("Power: %s" % res.power, bold=True))
click.echo("Brightness: %s" % res.brightness)
click.echo("Color temperature: %s" % res.color_temperature)
click.echo("Scene: %s" % res.scene)
click.echo("Smart Night Light: %s" % res.smart_night_light)
click.echo("Auto CCT: %s" % res.automatic_color_temperature)
click.echo(
"Countdown of the delayed turn off: %s seconds" % res.delay_off_countdown
)
@cli.command()
@pass_dev
def on(dev: miio.Ceil):
"""Power on."""
click.echo("Power on: %s" % dev.on())
@cli.command()
@pass_dev
def off(dev: miio.Ceil):
"""Power off."""
click.echo("Power off: %s" % dev.off())
@cli.command()
@click.argument("level", callback=validate_percentage, required=True)
@pass_dev
def set_brightness(dev: miio.Ceil, level):
"""Set brightness level."""
click.echo("Brightness: %s" % dev.set_brightness(level))
@cli.command()
@click.argument("level", callback=validate_percentage, required=True)
@pass_dev
def set_color_temperature(dev: miio.Ceil, level):
"""Set CCT level."""
click.echo("Color temperature level: %s" % dev.set_color_temperature(level))
@cli.command()
@click.argument("seconds", callback=validate_seconds, required=True)
@pass_dev
def delay_off(dev: miio.Ceil, seconds):
"""Set delay off in seconds."""
click.echo("Delay off: %s" % dev.delay_off(seconds))
@cli.command()
@click.argument("scene", callback=validate_scene, required=True)
@pass_dev
def set_scene(dev: miio.Ceil, scene):
"""Set scene number."""
click.echo("Eyecare Scene: %s" % dev.set_scene(scene))
@cli.command()
@pass_dev
def smart_night_light_on(dev: miio.Ceil):
"""Smart Night Light on."""
click.echo("Smart Night Light On: %s" % dev.smart_night_light_on())
@cli.command()
@pass_dev
def smart_night_light_off(dev: miio.Ceil):
"""Smart Night Light off."""
click.echo("Smart Night Light Off: %s" % dev.smart_night_light_off())
@cli.command()
@pass_dev
def automatic_color_temperature_on(dev: miio.Ceil):
"""Auto CCT on."""
click.echo("Auto CCT On: %s" % dev.automatic_color_temperature_on())
@cli.command()
@pass_dev
def automatic_color_temperature_off(dev: miio.Ceil):
"""Auto CCT on."""
click.echo("Auto CCT Off: %s" % dev.automatic_color_temperature_off())
if __name__ == "__main__":
cli()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585421538.0
python-miio-0.5.0.1/miio/chuangmi_camera.py 0000644 0001750 0001750 00000017700 00000000000 020203 0 ustar 00tpr tpr 0000000 0000000 """Xiaomi Chuangmi camera (chuangmi.camera.ipc009) support."""
import logging
from typing import Any, Dict
from .click_common import command, format_output
from .device import Device
_LOGGER = logging.getLogger(__name__)
class CameraStatus:
"""Container for status reports from the Xiaomi Chuangmi Camera."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Request:
["power", "motion_record", "light", "full_color", "flip", "improve_program", "wdr",
"track", "sdcard_status", "watermark", "max_client", "night_mode", "mini_level"]
Response:
["on","on","on","on","off","on","on","off","0","off","0","0","1"]
"""
self.data = data
@property
def power(self) -> bool:
"""Camera power."""
return self.data["power"] == "on"
@property
def motion_record(self) -> bool:
"""Motion record status."""
return self.data["motion_record"] == "on"
@property
def light(self) -> bool:
"""Camera light status."""
return self.data["light"] == "on"
@property
def full_color(self) -> bool:
"""Full color with bad lighting conditions."""
return self.data["full_color"] == "on"
@property
def flip(self) -> bool:
"""Image 180 degrees flip status."""
return self.data["flip"] == "on"
@property
def improve_program(self) -> bool:
"""Customer experience improvement program status."""
return self.data["improve_program"] == "on"
@property
def wdr(self) -> bool:
"""Wide dynamic range status."""
return self.data["wdr"] == "on"
@property
def track(self) -> bool:
"""Tracking status."""
return self.data["track"] == "on"
@property
def watermark(self) -> bool:
"""Apply watermark to video."""
return self.data["watermark"] == "on"
@property
def sdcard_status(self) -> int:
"""SD card status."""
return self.data["sdcard_status"]
@property
def max_client(self) -> int:
"""Unknown."""
return self.data["max_client"]
@property
def night_mode(self) -> int:
"""Night mode."""
return self.data["night_mode"]
@property
def mini_level(self) -> int:
"""Unknown."""
return self.data["mini_level"]
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.motion_record,
self.light,
self.full_color,
self.flip,
self.improve_program,
self.wdr,
self.track,
self.sdcard_status,
self.watermark,
self.max_client,
self.night_mode,
self.mini_level,
)
)
return s
def __json__(self):
return self.data
class ChuangmiCamera(Device):
"""Main class representing the Xiaomi Chuangmi Camera."""
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Motion record: {result.motion_record}\n"
"Light: {result.light}\n"
"Full color: {result.full_color}\n"
"Flip: {result.flip}\n"
"Improve program: {result.improve_program}\n"
"Wdr: {result.wdr}\n"
"Track: {result.track}\n"
"SD card status: {result.sdcard_status}\n"
"Watermark: {result.watermark}\n"
"Max client: {result.max_client}\n"
"Night mode: {result.night_mode}\n"
"Mini level: {result.mini_level}\n"
"\n",
)
)
def status(self) -> CameraStatus:
"""Retrieve properties."""
properties = [
"power",
"motion_record",
"light",
"full_color",
"flip",
"improve_program",
"wdr",
"track",
"sdcard_status",
"watermark",
"max_client",
"night_mode",
"mini_level",
]
values = self.send("get_prop", properties)
return CameraStatus(dict(zip(properties, values)))
@command(default_output=format_output("Power on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Power off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(default_output=format_output("MotionRecord on"))
def motion_record_on(self):
"""Start recording when motion detected."""
return self.send("set_motion_record", ["on"])
@command(default_output=format_output("MotionRecord off"))
def motion_record_off(self):
"""Motion record off, always record video."""
return self.send("set_motion_record", ["off"])
@command(default_output=format_output("MotionRecord stop"))
def motion_record_stop(self):
"""Motion record off, video recording stopped."""
return self.send("set_motion_record", ["stop"])
@command(default_output=format_output("Light on"))
def light_on(self):
"""Light on."""
return self.send("set_light", ["on"])
@command(default_output=format_output("Light off"))
def light_off(self):
"""Light off."""
return self.send("set_light", ["off"])
@command(default_output=format_output("FullColor on"))
def full_color_on(self):
"""Full color on."""
return self.send("set_full_color", ["on"])
@command(default_output=format_output("FullColor off"))
def full_color_off(self):
"""Full color off."""
return self.send("set_full_color", ["off"])
@command(default_output=format_output("Flip on"))
def flip_on(self):
"""Flip image 180 degrees on."""
return self.send("set_flip", ["on"])
@command(default_output=format_output("Flip off"))
def flip_off(self):
"""Flip image 180 degrees off."""
return self.send("set_flip", ["off"])
@command(default_output=format_output("ImproveProgram on"))
def improve_program_on(self):
"""Improve program on."""
return self.send("set_improve_program", ["on"])
@command(default_output=format_output("ImproveProgram off"))
def improve_program_off(self):
"""Improve program off."""
return self.send("set_improve_program", ["off"])
@command(default_output=format_output("Watermark on"))
def watermark_on(self):
"""Watermark on."""
return self.send("set_watermark", ["on"])
@command(default_output=format_output("Watermark off"))
def watermark_off(self):
"""Watermark off."""
return self.send("set_watermark", ["off"])
@command(default_output=format_output("WideDynamicRange on"))
def wdr_on(self):
"""Wide dynamic range on."""
return self.send("set_wdr", ["on"])
@command(default_output=format_output("WideDynamicRange off"))
def wdr_off(self):
"""Wide dynamic range off."""
return self.send("set_wdr", ["off"])
@command(default_output=format_output("NightMode auto"))
def night_mode_auto(self):
"""Auto switch to night mode."""
return self.send("set_night_mode", [0])
@command(default_output=format_output("NightMode off"))
def night_mode_off(self):
"""Night mode off."""
return self.send("set_night_mode", [1])
@command(default_output=format_output("NightMode on"))
def night_mode_on(self):
"""Night mode always on."""
return self.send("set_night_mode", [2])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/chuangmi_ir.py 0000644 0001750 0001750 00000015160 00000000000 017363 0 ustar 00tpr tpr 0000000 0000000 import base64
import re
import click
from construct import (
Adapter,
Array,
BitsInteger,
BitStruct,
Computed,
Const,
Int16ub,
Int16ul,
Int32ul,
Rebuild,
Struct,
len_,
this,
)
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
class ChuangmiIrException(DeviceException):
pass
class ChuangmiIr(Device):
"""Main class representing Chuangmi IR Remote Controller."""
PRONTO_RE = re.compile(r"^([\da-f]{4}\s?){3,}([\da-f]{4})$", re.IGNORECASE)
@command(
click.argument("key", type=int),
default_output=format_output("Learning command into storage key {key}"),
)
def learn(self, key: int = 1):
"""Learn an infrared command.
:param int key: Storage slot, must be between 1 and 1000000"""
if key < 1 or key > 1000000:
raise ChuangmiIrException("Invalid storage slot.")
return self.send("miIO.ir_learn", {"key": str(key)})
@command(
click.argument("key", type=int),
default_output=format_output("Reading infrared command from storage key {key}"),
)
def read(self, key: int = 1):
"""Read a learned command.
Positive response (chuangmi.ir.v2):
{'key': '1', 'code': 'Z6WPAasBAAA3BQAA4AwJAEA....AAABAAEBAQAAAQAA=='}
Negative response (chuangmi.ir.v2):
{'error': {'code': -5002, 'message': 'no code for this key'}, 'id': 5}
Negative response (chuangmi.ir.v2):
{'error': {'code': -5003, 'message': 'learn timeout'}, 'id': 17}
:param int key: Slot to read from"""
if key < 1 or key > 1000000:
raise ChuangmiIrException("Invalid storage slot.")
return self.send("miIO.ir_read", {"key": str(key)})
def play_raw(self, command: str, frequency: int = 38400):
"""Play a captured command.
:param str command: Command to execute
:param int frequency: Execution frequency"""
return self.send("miIO.ir_play", {"freq": frequency, "code": command})
def play_pronto(self, pronto: str, repeats: int = 1):
"""Play a Pronto Hex encoded IR command.
Supports only raw Pronto format, starting with 0000.
:param str pronto: Pronto Hex string.
:param int repeats: Number of extra signal repeats."""
return self.play_raw(*self.pronto_to_raw(pronto, repeats))
@classmethod
def pronto_to_raw(cls, pronto: str, repeats: int = 1):
"""Play a Pronto Hex encoded IR command.
Supports only raw Pronto format, starting with 0000.
:param str pronto: Pronto Hex string.
:param int repeats: Number of extra signal repeats."""
if repeats < 0:
raise ChuangmiIrException("Invalid repeats value")
try:
pronto_data = Pronto.parse(bytearray.fromhex(pronto))
except Exception as ex:
raise ChuangmiIrException("Invalid Pronto command") from ex
if len(pronto_data.intro) == 0:
repeats += 1
times = set()
for pair in pronto_data.intro + pronto_data.repeat * (1 if repeats else 0):
times.add(pair.pulse)
times.add(pair.gap)
times = sorted(times)
times_map = {t: idx for idx, t in enumerate(times)}
edge_pairs = []
for pair in pronto_data.intro + pronto_data.repeat * repeats:
edge_pairs.append(
{"pulse": times_map[pair.pulse], "gap": times_map[pair.gap]}
)
signal_code = base64.b64encode(
ChuangmiIrSignal.build(
{
"times_index": times + [0] * (16 - len(times)),
"edge_pairs": edge_pairs,
}
)
).decode()
return signal_code, int(round(pronto_data.frequency))
@command(
click.argument("command", type=str),
default_output=format_output("Playing the supplied command"),
)
def play(self, command: str):
"""Plays a command in one of the supported formats."""
if ":" not in command:
if self.PRONTO_RE.match(command):
command_type = "pronto"
else:
command_type = "raw"
command_args = []
else:
command_type, command, *command_args = command.split(":")
if command_type == "raw":
play_method = self.play_raw
arg_types = [int]
elif command_type == "pronto":
play_method = self.play_pronto
arg_types = [int]
else:
raise ChuangmiIrException("Invalid command type")
if len(command_args) > len(arg_types):
raise ChuangmiIrException("Invalid command arguments count")
try:
command_args = [t(v) for v, t in zip(command_args, arg_types)]
except Exception as ex:
raise ChuangmiIrException("Invalid command arguments") from ex
return play_method(command, *command_args)
@command(
click.argument("indicator_led", type=bool),
default_output=format_output(
lambda indicator_led: "Turning on indicator LED"
if indicator_led
else "Turning off indicator LED"
),
)
def set_indicator_led(self, indicator_led: bool):
"""Set the indicator led on/off."""
if indicator_led:
return self.send("set_indicatorLamp", ["on"])
else:
return self.send("set_indicatorLamp", ["off"])
@command(default_output=format_output("Indicator LED status: {result}"))
def get_indicator_led(self):
"""Get the indicator led status."""
return self.send("get_indicatorLamp")
class ProntoPulseAdapter(Adapter):
def _decode(self, obj, context, path):
return int(obj * context._.modulation_period)
def _encode(self, obj, context, path):
raise RuntimeError("Not implemented")
ChuangmiIrSignal = Struct(
Const(0xA567, Int16ul),
"edge_count" / Rebuild(Int16ul, len_(this.edge_pairs) * 2 - 1),
"times_index" / Array(16, Int32ul),
"edge_pairs"
/ Array(
(this.edge_count + 1) // 2,
BitStruct("gap" / BitsInteger(4), "pulse" / BitsInteger(4)),
),
)
ProntoBurstPair = Struct(
"pulse" / ProntoPulseAdapter(Int16ub), "gap" / ProntoPulseAdapter(Int16ub)
)
Pronto = Struct(
Const(0, Int16ub),
"_ticks" / Int16ub,
"modulation_period" / Computed(this._ticks * 0.241246),
"frequency" / Computed(1000000 / this.modulation_period),
"intro_len" / Int16ub,
"repeat_len" / Int16ub,
"intro" / Array(this.intro_len, ProntoBurstPair),
"repeat" / Array(this.repeat_len, ProntoBurstPair),
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/chuangmi_plug.py 0000644 0001750 0001750 00000016470 00000000000 017725 0 ustar 00tpr tpr 0000000 0000000 import logging
from collections import defaultdict
from typing import Any, Dict, Optional
import click
from .click_common import command, format_output
from .device import Device
from .utils import deprecated
_LOGGER = logging.getLogger(__name__)
MODEL_CHUANGMI_PLUG_V3 = "chuangmi.plug.v3"
MODEL_CHUANGMI_PLUG_V1 = "chuangmi.plug.v1"
MODEL_CHUANGMI_PLUG_M1 = "chuangmi.plug.m1"
MODEL_CHUANGMI_PLUG_M3 = "chuangmi.plug.m3"
MODEL_CHUANGMI_PLUG_V2 = "chuangmi.plug.v2"
MODEL_CHUANGMI_PLUG_HMI205 = "chuangmi.plug.hmi205"
MODEL_CHUANGMI_PLUG_HMI206 = "chuangmi.plug.hmi206"
AVAILABLE_PROPERTIES = {
MODEL_CHUANGMI_PLUG_V1: ["on", "usb_on", "temperature"],
MODEL_CHUANGMI_PLUG_V3: ["on", "usb_on", "temperature", "wifi_led"],
MODEL_CHUANGMI_PLUG_M1: ["power", "temperature"],
MODEL_CHUANGMI_PLUG_M3: ["power", "temperature"],
MODEL_CHUANGMI_PLUG_V2: ["power", "temperature"],
MODEL_CHUANGMI_PLUG_HMI205: ["power", "temperature"],
MODEL_CHUANGMI_PLUG_HMI206: ["power", "temperature"],
}
class ChuangmiPlugStatus:
"""Container for status reports from the plug."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Chuangmi Plug V1 (chuangmi.plug.v1)
{ 'power': True, 'usb_on': True, 'temperature': 32 }
Response of a Chuangmi Plug V3 (chuangmi.plug.v3):
{ 'on': True, 'usb_on': True, 'temperature': 32, 'wifi_led': True }
"""
self.data = data
@property
def power(self) -> bool:
"""Current power state."""
if "on" in self.data:
return self.data["on"]
if "power" in self.data:
return self.data["power"] == "on"
@property
def is_on(self) -> bool:
"""True if device is on."""
return self.power
@property
def temperature(self) -> int:
return self.data["temperature"]
@property
def usb_power(self) -> Optional[bool]:
"""True if USB is on."""
if "usb_on" in self.data and self.data["usb_on"] is not None:
return self.data["usb_on"]
return None
@property
def load_power(self) -> Optional[float]:
"""Current power load, if available."""
if "load_power" in self.data and self.data["load_power"] is not None:
return float(self.data["load_power"])
return None
@property
def wifi_led(self) -> Optional[bool]:
"""True if the wifi led is turned on."""
if "wifi_led" in self.data and self.data["wifi_led"] is not None:
return self.data["wifi_led"] == "on"
return None
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.usb_power,
self.temperature,
self.load_power,
self.wifi_led,
)
)
return s
def __json__(self):
return self.data
class ChuangmiPlug(Device):
"""Main class representing the Chuangmi Plug."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_CHUANGMI_PLUG_M1,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_CHUANGMI_PLUG_M1
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"USB Power: {result.usb_power}\n"
"Temperature: {result.temperature} °C\n"
"Load power: {result.load_power}\n"
"WiFi LED: {result.wifi_led}\n",
)
)
def status(self) -> ChuangmiPlugStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model].copy()
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
if self.model == MODEL_CHUANGMI_PLUG_V3:
load_power = self.send("get_power") # Response: [300]
if len(load_power) == 1:
properties.append("load_power")
values.append(load_power[0] * 0.01)
return ChuangmiPlugStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
if self.model == MODEL_CHUANGMI_PLUG_V1:
return self.send("set_on")
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
if self.model == MODEL_CHUANGMI_PLUG_V1:
return self.send("set_off")
return self.send("set_power", ["off"])
@command(default_output=format_output("Powering USB on"))
def usb_on(self):
"""Power on."""
return self.send("set_usb_on")
@command(default_output=format_output("Powering USB off"))
def usb_off(self):
"""Power off."""
return self.send("set_usb_off")
@command(
click.argument("wifi_led", type=bool),
default_output=format_output(
lambda wifi_led: "Turning on WiFi LED"
if wifi_led
else "Turning off WiFi LED"
),
)
def set_wifi_led(self, wifi_led: bool):
"""Set the wifi led on/off."""
if wifi_led:
return self.send("set_wifi_led", ["on"])
else:
return self.send("set_wifi_led", ["off"])
@deprecated(
"This device class is deprecated. Please use the ChuangmiPlug "
"class in future and select a model by parameter 'model'."
)
class Plug(ChuangmiPlug):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(
ip, token, start_id, debug, lazy_discover, model=MODEL_CHUANGMI_PLUG_M1
)
@deprecated(
"This device class is deprecated. Please use the ChuangmiPlug "
"class in future and select a model by parameter 'model'."
)
class PlugV1(ChuangmiPlug):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(
ip, token, start_id, debug, lazy_discover, model=MODEL_CHUANGMI_PLUG_V1
)
@deprecated(
"This device class is deprecated. Please use the ChuangmiPlug "
"class in future and select a model by parameter 'model'."
)
class PlugV3(ChuangmiPlug):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(
ip, token, start_id, debug, lazy_discover, model=MODEL_CHUANGMI_PLUG_V3
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/cli.py 0000644 0001750 0001750 00000002055 00000000000 015644 0 ustar 00tpr tpr 0000000 0000000 import logging
import click
from miio.click_common import (
DeviceGroupMeta,
ExceptionHandlerGroup,
GlobalContextObject,
json_output,
)
_LOGGER = logging.getLogger(__name__)
@click.group(cls=ExceptionHandlerGroup)
@click.option("-d", "--debug", default=False, count=True)
@click.option(
"-o",
"--output",
type=click.Choice(["default", "json", "json_pretty"]),
default="default",
)
@click.pass_context
def cli(ctx, debug: int, output: str):
if debug:
logging.basicConfig(level=logging.DEBUG)
_LOGGER.info("Debug mode active")
else:
logging.basicConfig(level=logging.INFO)
if output in ("json", "json_pretty"):
output_func = json_output(pretty=output == "json_pretty")
else:
output_func = None
ctx.obj = GlobalContextObject(debug=debug, output=output_func)
for device_class in DeviceGroupMeta.device_classes:
cli.add_command(device_class.get_device_group())
def create_cli():
return cli(auto_envvar_prefix="MIIO")
if __name__ == "__main__":
create_cli()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/click_common.py 0000644 0001750 0001750 00000022125 00000000000 017532 0 ustar 00tpr tpr 0000000 0000000 """Click commons.
This file contains common functions for cli tools.
"""
import ast
import ipaddress
import json
import logging
import re
import sys
from functools import partial, wraps
from typing import Union
import click
import miio
from .exceptions import DeviceError
if sys.version_info < (3, 5):
print(
"To use this script you need python 3.5 or newer, got %s" % (sys.version_info,)
)
sys.exit(1)
_LOGGER = logging.getLogger(__name__)
def validate_ip(ctx, param, value):
if value is None:
return None
try:
ipaddress.ip_address(value)
return value
except ValueError as ex:
raise click.BadParameter("Invalid IP: %s" % ex)
def validate_token(ctx, param, value):
if value is None:
return None
token_len = len(value)
if token_len != 32:
raise click.BadParameter("Token length != 32 chars: %s" % token_len)
return value
class ExceptionHandlerGroup(click.Group):
"""Add a simple group for catching the miio-related exceptions.
This simplifies catching the exceptions from different click commands.
Idea from https://stackoverflow.com/a/44347763
"""
def __call__(self, *args, **kwargs):
try:
return self.main(*args, **kwargs)
except miio.DeviceException as ex:
_LOGGER.debug("Exception: %s", ex, exc_info=True)
click.echo(click.style("Error: %s" % ex, fg="red", bold=True))
class EnumType(click.Choice):
def __init__(self, enumcls, casesensitive=True):
choices = enumcls.__members__
if not casesensitive:
choices = (_.lower() for _ in choices)
self._enumcls = enumcls
self._casesensitive = casesensitive
super().__init__(list(sorted(set(choices))))
def convert(self, value, param, ctx):
if not self._casesensitive:
value = value.lower()
value = super().convert(value, param, ctx)
if not self._casesensitive:
return next(_ for _ in self._enumcls if _.name.lower() == value.lower())
else:
return next(_ for _ in self._enumcls if _.name == value)
def get_metavar(self, param):
word = self._enumcls.__name__
# Stolen from jpvanhal/inflection
word = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", word)
word = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", word)
word = word.replace("-", "_").lower().split("_")
if word[-1] == "enum":
word.pop()
return ("_".join(word)).upper()
class LiteralParamType(click.ParamType):
name = "literal"
def convert(self, value, param, ctx):
try:
return ast.literal_eval(value)
except ValueError:
self.fail("%s is not a valid literal" % value, param, ctx)
class GlobalContextObject:
def __init__(self, debug: int = 0, output: callable = None):
self.debug = debug
self.output = output
class DeviceGroupMeta(type):
device_classes = set()
def __new__(mcs, name, bases, namespace) -> type:
commands = {}
def _get_commands_for_namespace(namespace):
commands = {}
for key, val in namespace.items():
if not callable(val):
continue
device_group_command = getattr(val, "_device_group_command", None)
if device_group_command is None:
continue
commands[device_group_command.command_name] = device_group_command
return commands
# 1. Go through base classes for commands
for base in bases:
commands.update(getattr(base, "_device_group_commands", {}))
# 2. Add commands from the current class
commands.update(_get_commands_for_namespace(namespace))
namespace["_device_group_commands"] = commands
if "get_device_group" not in namespace:
def get_device_group(dcls):
return DeviceGroup(dcls)
namespace["get_device_group"] = classmethod(get_device_group)
cls = super().__new__(mcs, name, bases, namespace)
mcs.device_classes.add(cls)
return cls
class DeviceGroup(click.MultiCommand):
class Command:
def __init__(self, name, decorators, *, default_output=None, **kwargs):
self.name = name
self.decorators = list(decorators)
self.decorators.reverse()
self.default_output = default_output
self.kwargs = kwargs
def __call__(self, func):
self.func = func
func._device_group_command = self
self.kwargs.setdefault("help", self.func.__doc__)
return func
@property
def command_name(self):
return self.name or self.func.__name__.lower()
def wrap(self, ctx, func):
gco = ctx.find_object(GlobalContextObject)
if gco is not None and gco.output is not None:
output = gco.output
elif self.default_output:
output = self.default_output
else:
output = format_output("Running command {0}".format(self.command_name))
func = output(func)
for decorator in self.decorators:
func = decorator(func)
return click.command(self.command_name, **self.kwargs)(func)
def call(self, owner, *args, **kwargs):
method = getattr(owner, self.func.__name__)
return method(*args, **kwargs)
DEFAULT_PARAMS = [
click.Option(["--ip"], required=True, callback=validate_ip),
click.Option(["--token"], required=True, callback=validate_token),
]
def __init__(
self,
device_class,
name=None,
invoke_without_command=False,
no_args_is_help=None,
subcommand_metavar=None,
chain=False,
result_callback=None,
result_callback_pass_device=True,
**attrs
):
self.commands = getattr(device_class, "_device_group_commands", None)
if self.commands is None:
raise RuntimeError(
"Class {} doesn't use DeviceGroupMeta meta class."
" It can't be used with DeviceGroup."
)
self.device_class = device_class
self.device_pass = click.make_pass_decorator(device_class)
attrs.setdefault("params", self.DEFAULT_PARAMS)
attrs.setdefault("callback", click.pass_context(self.group_callback))
if result_callback_pass_device and callable(result_callback):
result_callback = self.device_pass(result_callback)
super().__init__(
name or device_class.__name__.lower(),
invoke_without_command,
no_args_is_help,
subcommand_metavar,
chain,
result_callback,
**attrs
)
def group_callback(self, ctx, *args, **kwargs):
gco = ctx.find_object(GlobalContextObject)
if gco:
kwargs["debug"] = gco.debug
ctx.obj = self.device_class(*args, **kwargs)
def command_callback(self, miio_command, miio_device, *args, **kwargs):
return miio_command.call(miio_device, *args, **kwargs)
def get_command(self, ctx, cmd_name):
if cmd_name not in self.commands:
ctx.fail("Unknown command (%s)" % cmd_name)
cmd = self.commands[cmd_name]
return self.commands[cmd_name].wrap(
ctx, self.device_pass(partial(self.command_callback, cmd))
)
def list_commands(self, ctx):
return sorted(self.commands.keys())
def command(*decorators, name=None, default_output=None, **kwargs):
return DeviceGroup.Command(
name, decorators, default_output=default_output, **kwargs
)
def format_output(
msg_fmt: Union[str, callable] = "",
result_msg_fmt: Union[str, callable] = "{result}",
):
def decorator(func):
@wraps(func)
def wrap(*args, **kwargs):
if msg_fmt:
if callable(msg_fmt):
msg = msg_fmt(**kwargs)
else:
msg = msg_fmt.format(**kwargs)
if msg:
click.echo(msg.strip())
kwargs["result"] = func(*args, **kwargs)
if result_msg_fmt:
if callable(result_msg_fmt):
result_msg = result_msg_fmt(**kwargs)
else:
result_msg = result_msg_fmt.format(**kwargs)
if result_msg:
click.echo(result_msg.strip())
return wrap
return decorator
def json_output(pretty=False):
indent = 2 if pretty else None
def decorator(func):
@wraps(func)
def wrap(*args, **kwargs):
try:
result = func(*args, **kwargs)
except DeviceError as ex:
click.echo(json.dumps(ex.args[0], indent=indent))
return
get_json_data_func = getattr(result, "__json__", None)
if get_json_data_func is not None:
result = get_json_data_func()
click.echo(json.dumps(result, indent=indent))
return wrap
return decorator
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/cooker.py 0000644 0001750 0001750 00000064134 00000000000 016365 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
import string
from collections import defaultdict
from datetime import time
from typing import List, Optional
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_PRESSURE1 = "chunmi.cooker.press1"
MODEL_PRESSURE2 = "chunmi.cooker.press2"
MODEL_NORMAL1 = "chunmi.cooker.normal1"
MODEL_NORMAL2 = "chunmi.cooker.normal2"
MODEL_NORMAL3 = "chunmi.cooker.normal3"
MODEL_NORMAL4 = "chunmi.cooker.normal4"
MODEL_NORMAL5 = "chunmi.cooker.normal5"
MODEL_PRESSURE = [MODEL_PRESSURE1, MODEL_PRESSURE2]
MODEL_NORMAL = [
MODEL_NORMAL1,
MODEL_NORMAL2,
MODEL_NORMAL3,
MODEL_NORMAL4,
MODEL_NORMAL5,
]
MODEL_NORMAL_GROUP1 = [MODEL_NORMAL2, MODEL_NORMAL5]
MODEL_NORMAL_GROUP2 = [MODEL_NORMAL3, MODEL_NORMAL4]
COOKING_STAGES = {
0: {
"name": "Quickly preheat",
"description": "Increase temperature in a controlled manner to soften rice gradually",
},
1: {
"name": "Water-absorbing",
"description": "Increase temperature, to flesh grains with water",
},
2: {"name": "Boiling", "description": "Last high heating, to cook rice evenly"},
3: {
"name": "Gelantinizing",
"description": "Steaming under high temperature, to bring sweetness to grains",
},
4: {"name": "Braising", "description": "Absorb water at moderate temperature"},
5: {
"name": "Boiling",
"description": "Operate at full load to boil rice",
# Keep heating at high temperature. Let rice to receive
},
7: {
"name": "Boiling",
"description": "Operate at full load to boil rice",
# Keep heating at high temperature. Let rice to receive
},
8: {
"name": "Warm up rice",
"description": "Temperature control adjustment and cyclic heating "
"achieve combination of taste, dolor and nutrition",
},
10: {
"name": "High temperature gelatinization",
"description": "High-temperature steam generates crystal clear rice g...",
},
16: {"name": "Cooking finished", "description": ""},
}
class CookerException(DeviceException):
pass
class OperationMode(enum.Enum):
# Observed
Running = "running"
Waiting = "waiting"
AutoKeepWarm = "autokeepwarm"
# Potential candidates
Cooking = "cooking"
Finish = "finish"
FinishA = "finisha"
KeepWarm = "keepwarm"
KeepTemp = "keep_temp"
Notice = "notice"
Offline = "offline"
Online = "online"
PreCook = "precook"
Resume = "resume"
ResumeP = "resumep"
Start = "start"
StartP = "startp"
Cancel = "Отмена"
class TemperatureHistory:
def __init__(self, data: str):
"""
Container of temperatures recorded every 10-15 seconds while cooking.
Example values:
Status waiting:
0
2 minutes:
161515161c242a3031302f2eaa2f2f2e2f
12 minutes:
161515161c242a3031302f2eaa2f2f2e2f2e302f2e2d302f2f2e2f2f2f2f343a3f3f3d3e3c3d3c3f3d3d3d3f3d3d3d3d3e3d3e3c3f3f3d3e3d3e3e3d3f3d3c3e3d3d3e3d3f3e3d3f3e3d3c
32 minutes:
161515161c242a3031302f2eaa2f2f2e2f2e302f2e2d302f2f2e2f2f2f2f343a3f3f3d3e3c3d3c3f3d3d3d3f3d3d3d3d3e3d3e3c3f3f3d3e3d3e3e3d3f3d3c3e3d3d3e3d3f3e3d3f3e3d3c3f3e3d3c3f3e3d3c3f3f3d3d3e3d3d3f3f3d3d3f3f3e3d3d3d3e3e3d3daa3f3f3f3f3f414446474a4e53575e5c5c5b59585755555353545454555554555555565656575757575858585859595b5b5c5c5c5c5d5daa5d5e5f5f606061
55 minutes:
161515161c242a3031302f2eaa2f2f2e2f2e302f2e2d302f2f2e2f2f2f2f343a3f3f3d3e3c3d3c3f3d3d3d3f3d3d3d3d3e3d3e3c3f3f3d3e3d3e3e3d3f3d3c3e3d3d3e3d3f3e3d3f3e3d3c3f3e3d3c3f3e3d3c3f3f3d3d3e3d3d3f3f3d3d3f3f3e3d3d3d3e3e3d3daa3f3f3f3f3f414446474a4e53575e5c5c5b59585755555353545454555554555555565656575757575858585859595b5b5c5c5c5c5d5daa5d5e5f5f60606161616162626263636363646464646464646464646464646464646464646364646464646464646464646464646464646464646464646464646464aa5a59585756555554545453535352525252525151515151
Data structure:
Octet 1 (16): First temperature measurement in hex (22 °C)
Octet 2 (15): Second temperature measurement in hex (21 °C)
Octet 3 (15): Third temperature measurement in hex (21 °C)
...
"""
if not len(data) % 2:
self.data = [int(data[i : i + 2], 16) for i in range(0, len(data), 2)]
else:
self.data = []
@property
def temperatures(self) -> List[int]:
return self.data
@property
def raw(self) -> str:
return "".join(["{:02x}".format(value) for value in self.data])
def __str__(self) -> str:
return str(self.data)
def __repr__(self) -> str:
s = "" % str(self.data)
return s
def __json__(self):
return self.data
class CookerCustomizations:
def __init__(self, custom: str):
"""
Container of different user customizations.
Example values:
ffffffffffff011effff010000001d1f,
ffffffffffff011effff010004026460,
ffffffffffff011effff01000a015559,
ffffffffffff011effff01000000535d
Data structure:
Octet 1 (ff): Jingzhu Appointment Hour in hex
Octet 2 (ff): Jingzhu Appointment Minute in hex
Octet 3 (ff): Kuaizhu Appointment Hour in hex
Octet 4 (ff): Kuaizhu Appointment Minute in hex
Octet 5 (ff): Zhuzhou Appointment Hour in hex
Octet 6 (ff): Zhuzhou Appointment Minute in hex
Octet 7 (01): Favorite Appointment Hour in hex (1 hour)
Octet 8 (1e): Favorite Appointment Minute in hex (30 minutes)
Octet 9 (ff): Favorite Cooking Hour in hex
Octet 10 (ff): Favorite Cooking Minute in hex
Octet 11-16 (01 00 00 00 1d 1f): Meaning unknown
"""
self.custom = [int(custom[i : i + 2], 16) for i in range(0, len(custom), 2)]
@property
def jingzhu_appointment(self) -> time:
return time(hour=self.custom[0], minute=self.custom[1])
@property
def kuaizhu_appointment(self) -> time:
return time(hour=self.custom[2], minute=self.custom[3])
@property
def zhuzhou_appointment(self) -> time:
return time(hour=self.custom[4], minute=self.custom[5])
@property
def zhuzhou_cooking(self) -> time:
return time(hour=self.custom[6], minute=self.custom[7])
@property
def favorite_appointment(self) -> time:
return time(hour=self.custom[8], minute=self.custom[9])
@property
def favorite_cooking(self) -> time:
return time(hour=self.custom[10], minute=self.custom[11])
def __str__(self) -> str:
return "".join(["{:02x}".format(value) for value in self.custom])
def __repr__(self) -> str:
s = (
""
% (
self.jingzhu_appointment,
self.kuaizhu_appointment,
self.zhuzhou_appointment,
self.zhuzhou_cooking,
self.favorite_appointment,
self.favorite_cooking,
)
)
return s
class CookingStage:
def __init__(self, stage: str):
"""
Container of cooking stages.
Example timeouts: 'null', 02000000ff, 03000000ff, 0a000000ff, 1000000000
Data structure:
Octet 1 (02): State in hex
Octet 2-3 (0000): Rice ID in hex
Octet 4 (00): Taste i n hex
Octet 5 (ff): Meaning unknown.
"""
self.stage = stage
@property
def state(self) -> int:
"""
10: Cooking finished
11: Cooking finished
12: Cooking finished
"""
return int(self.stage[0:2], 16)
@property
def rice_id(self) -> int:
return int(self.stage[2:6], 16)
@property
def taste(self) -> int:
return int(self.stage[6:8], 16)
@property
def taste_phase(self) -> int:
phase = int(self.taste / 33)
if phase > 2:
return 2
return phase
@property
def name(self) -> str:
try:
return COOKING_STAGES[self.state]["name"]
except KeyError:
return "Unknown stage"
@property
def description(self) -> str:
try:
return COOKING_STAGES[self.state]["description"]
except KeyError:
return ""
@property
def raw(self) -> str:
return self.stage
def __str__(self) -> str:
s = (
"name=%s, "
"description=%s, "
"state=%s, "
"rice_id=%s, "
"taste=%s, "
"taste_phase=%s, "
"raw=%s"
% (
self.name,
self.description,
self.state,
self.rice_id,
self.taste,
self.taste_phase,
self.raw,
)
)
return s
def __repr__(self) -> str:
s = (
""
% (
self.name,
self.description,
self.state,
self.rice_id,
self.taste,
self.taste_phase,
self.stage,
)
)
return s
class InteractionTimeouts:
def __init__(self, timeouts: str = None):
"""
Example timeouts: 05040f, 05060f
Data structure:
Octet 1 (05): LED off timeout in hex (5 seconds)
Octet 2 (04): Lid open timeout in hex (4 seconds)
Octet 3 (0f): Lid open warning timeout (15 seconds)
"""
if timeouts is None:
self.timeouts = [5, 4, 15]
else:
self.timeouts = [
int(timeouts[i : i + 2], 16) for i in range(0, len(timeouts), 2)
]
@property
def led_off(self) -> int:
return self.timeouts[0]
@property
def lid_open(self) -> int:
return self.timeouts[1]
@property
def lid_open_warning(self) -> int:
return self.timeouts[2]
@led_off.setter
def led_off(self, delay: int):
self.timeouts[0] = delay
@lid_open.setter
def lid_open(self, timeout: int):
self.timeouts[1] = timeout
@lid_open_warning.setter
def lid_open_warning(self, timeout: int):
self.timeouts[2] = timeout
def __str__(self) -> str:
return "".join(["{:02x}".format(value) for value in self.timeouts])
def __repr__(self) -> str:
s = (
""
% (self.led_off, self.lid_open, self.lid_open_warning)
)
return s
class CookerSettings:
def __init__(self, settings: str = None):
"""
Example settings: 1407, 0607, 0207
Data structure:
Octet 1 (14): Bitmask of setting flags
Bit 1: Pressure supported
Bit 2: LED on
Bit 3: Auto keep warm
Bit 4: Lid open warning
Bit 5: Lid open warning delayed
Bit 6-8: Unused
Octet 2 (07): Second bitmask of setting flags
Bit 1: Jingzhu auto keep warm
Bit 2: Kuaizhu auto keep warm
Bit 3: Zhuzhou auto keep warm
Bit 4: Favorite auto keep warm
Bit 5-8: Unused
"""
if settings is None:
self.settings = [0, 4]
else:
self.settings = [
int(settings[i : i + 2], 16) for i in range(0, len(settings), 2)
]
@property
def pressure_supported(self) -> bool:
return self.settings[0] & 1 != 0
@property
def led_on(self) -> bool:
return self.settings[0] & 2 != 0
@property
def auto_keep_warm(self) -> bool:
return self.settings[0] & 4 != 0
@property
def lid_open_warning(self) -> bool:
return self.settings[0] & 8 != 0
@property
def lid_open_warning_delayed(self) -> bool:
return self.settings[0] & 16 != 0
@property
def jingzhu_auto_keep_warm(self) -> bool:
return self.settings[1] & 1 != 0
@property
def kuaizhu_auto_keep_warm(self) -> bool:
return self.settings[1] & 2 != 0
@property
def zhuzhou_auto_keep_warm(self) -> bool:
return self.settings[1] & 4 != 0
@property
def favorite_auto_keep_warm(self) -> bool:
return self.settings[1] & 8 != 0
@pressure_supported.setter
def pressure_supported(self, supported: bool):
if supported:
self.settings[0] |= 1
else:
self.settings[0] &= 254
@led_on.setter
def led_on(self, on: bool):
if on:
self.settings[0] |= 2
else:
self.settings[0] &= 253
@auto_keep_warm.setter
def auto_keep_warm(self, keep_warm: bool):
if keep_warm:
self.settings[0] |= 4
else:
self.settings[0] &= 251
@lid_open_warning.setter
def lid_open_warning(self, alarm: bool):
if alarm:
self.settings[0] |= 8
else:
self.settings[0] &= 247
@lid_open_warning_delayed.setter
def lid_open_warning_delayed(self, alarm: bool):
if alarm:
self.settings[0] |= 16
else:
self.settings[0] &= 239
@jingzhu_auto_keep_warm.setter
def jingzhu_auto_keep_warm(self, auto_keep_warm: bool):
if auto_keep_warm:
self.settings[1] |= 1
else:
self.settings[1] &= 254
@kuaizhu_auto_keep_warm.setter
def kuaizhu_auto_keep_warm(self, auto_keep_warm: bool):
if auto_keep_warm:
self.settings[1] |= 2
else:
self.settings[1] &= 253
@zhuzhou_auto_keep_warm.setter
def zhuzhou_auto_keep_warm(self, auto_keep_warm: bool):
if auto_keep_warm:
self.settings[1] |= 4
else:
self.settings[1] &= 251
@favorite_auto_keep_warm.setter
def favorite_auto_keep_warm(self, auto_keep_warm: bool):
if auto_keep_warm:
self.settings[1] |= 8
else:
self.settings[1] &= 247
def __str__(self) -> str:
return "".join(["{:02x}".format(value) for value in self.settings])
def __repr__(self) -> str:
s = (
""
% (
self.pressure_supported,
self.led_on,
self.lid_open_warning,
self.lid_open_warning_delayed,
self.auto_keep_warm,
self.jingzhu_auto_keep_warm,
self.kuaizhu_auto_keep_warm,
self.zhuzhou_auto_keep_warm,
self.favorite_auto_keep_warm,
)
)
return s
class CookerStatus:
def __init__(self, data):
"""
Responses of a chunmi.cooker.normal2 (fw_ver: 1.2.8):
{ 'func': 'precook',
'menu': '0001',
'stage': '009ce63cff',
'temp': 21,
't_func': '769',
't_precook': '1180',
't_cook': 60,
'setting': '1407',
'delay': '05060f',
'version': '00030017',
'favorite': '0100',
'custom': '13281323ffff011effff010000001516'}
{ 'func': 'waiting',
'menu': '0001',
'stage': 'null',
'temp': 22,
't_func': 60,
't_precook': -1,
't_cook': 60,
'setting': '1407',
'delay': '05060f',
'version': '00030017',
'favorite': '0100',
'custom': '13281323ffff011effff010000001617'}
func , menu , stage , temp , t_func, t_precook, t_cook, setting, delay , version , favorite, custom
idle: ['waiting', '0001', 'null', '29', '60', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff010000001d1f']
quickly preheat: ['running', '0001', '00000000ff', '031e0b23', '60', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff010000001d1f']
absorb water at moderate temp: ['running', '0001', '02000000ff', '031e0b23', '54', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff010002013e23']
absorb water at moderate temp: ['running', '0001', '02000000ff', '031e0b23', '48', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff010002013f29']
operate at full load to boil rice: ['running', '0001', '03000000ff', '031e0b23', '39', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff010003055332']
operate at full load to boil rice: ['running', '0001', '04000000ff', '031e0b23', '35', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff010004026460']
operate at full load to boil rice: ['running', '0001', '06000000ff', '031e0b23', '29', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff010006015c64']
high temperature gelatinization: ['running', '0001', '07000000ff', '031e0b23', '22', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff010007015d64']
temperature gelatinization: ['running', '0001', '0a000000ff', '031e0b23', '2', '-1', '60', '0607', '05040f', '00030017', '0100', 'ffffffffffff011effff01000a015559']
meal is ready: ['autokeepwarm', '0001', '1000000000', '031e0b23031e', '1', '750', '60', '0207', '05040f', '00030017', '0100', 'ffffffffffff011effff01000000535d']
"""
self.data = data
@property
def mode(self) -> OperationMode:
"""Current operation mode."""
return OperationMode(self.data["func"])
@property
def menu(self) -> int:
"""Selected recipe id."""
return int(self.data["menu"], 16)
@property
def stage(self) -> Optional[CookingStage]:
"""Current stage if cooking."""
stage = self.data["stage"]
if len(stage) == 10:
return CookingStage(stage)
return None
@property
def temperature(self) -> Optional[int]:
"""
Current temperature, if idle.
Example values: *29*, 031e0b23, 031e0b23031e
"""
value = self.data["temp"]
if len(value) == 2 and value.isdigit():
return int(value)
return None
@property
def start_time(self) -> Optional[time]:
"""
Start time of cooking?
The property "temp" is used for different purposes.
Example values: 29, *031e0b23*, 031e0b23031e
"""
value = self.data["temp"]
if len(value) == 8:
return time(hour=int(value[4:6], 16), minute=int(value[6:8], 16))
return None
@property
def remaining(self) -> int:
"""Remaining minutes of the cooking process."""
return int(self.data["t_func"])
@property
def cooking_delayed(self) -> Optional[int]:
"""Wait n minutes before cooking / scheduled cooking."""
delay = int(self.data["t_precook"])
if delay >= 0:
return delay
return None
@property
def duration(self) -> int:
"""Duration of the cooking process."""
return int(self.data["t_cook"])
@property
def settings(self) -> CookerSettings:
"""Settings of the cooker."""
return CookerSettings(self.data["setting"])
@property
def interaction_timeouts(self) -> InteractionTimeouts:
"""Interaction timeouts."""
return InteractionTimeouts(self.data["delay"])
@property
def hardware_version(self) -> int:
"""Hardware version."""
return int(self.data["version"][0:4], 16)
@property
def firmware_version(self) -> int:
"""Firmware version."""
return int(self.data["version"][4:8], 16)
@property
def favorite(self) -> int:
"""Favored recipe id. Can be compared with the menu property."""
return int(self.data["favorite"], 16)
@property
def custom(self) -> Optional[CookerCustomizations]:
custom = self.data["custom"]
if len(custom) > 31:
return CookerCustomizations(custom)
return None
def __repr__(self) -> str:
s = (
""
% (
self.mode,
self.menu,
self.stage,
self.temperature,
self.start_time,
self.remaining,
self.cooking_delayed,
self.duration,
self.settings,
self.interaction_timeouts,
self.hardware_version,
self.firmware_version,
self.favorite,
self.custom,
)
)
return s
class Cooker(Device):
"""Main class representing the cooker."""
@command(
default_output=format_output(
"",
"Mode: {result.mode}\n"
"Menu: {result.menu}\n"
"Stage: {result.stage}\n"
"Temperature: {result.temperature}\n"
"Start time: {result.start_time}\n"
"Remaining: {result.remaining}\n"
"Cooking delayed: {result.cooking_delayed}\n"
"Duration: {result.duration}\n"
"Settings: {result.settings}\n"
"Interaction timeouts: {result.interaction_timeouts}\n"
"Hardware version: {result.hardware_version}\n"
"Firmware version: {result.firmware_version}\n"
"Favorite: {result.favorite}\n"
"Custom: {result.custom}\n",
)
)
def status(self) -> CookerStatus:
"""Retrieve properties."""
properties = [
"func",
"menu",
"stage",
"temp",
"t_func",
"t_precook",
"t_cook",
"setting",
"delay",
"version",
"favorite",
"custom",
]
"""
Some cookers doesn't support a list of properties here. Therefore "all" properties
are requested. If the property count or order changes the property list above must
be updated.
"""
values = self.send("get_prop", ["all"])
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return CookerStatus(defaultdict(lambda: None, zip(properties, values)))
@command(
click.argument("profile", type=str),
default_output=format_output("Cooking profile started"),
)
def start(self, profile: str):
"""Start cooking a profile."""
if not self._validate_profile(profile):
raise CookerException("Invalid cooking profile: %s" % profile)
self.send("set_start", [profile])
@command(default_output=format_output("Cooking stopped"))
def stop(self):
"""Stop cooking."""
self.send("set_func", ["end02"])
@command(default_output=format_output("Cooking stopped"))
def stop_outdated_firmware(self):
"""Stop cooking (obsolete)."""
self.send("set_func", ["end"])
@command(default_output=format_output("Setting no warnings"))
def set_no_warnings(self):
"""Disable warnings."""
self.send("set_func", ["nowarn"])
@command(default_output=format_output("Setting acknowledge"))
def set_acknowledge(self):
"""Enable warnings?"""
self.send("set_func", ["ack"])
# FIXME: Add unified CLI support
def set_interaction(self, settings: CookerSettings, timeouts: InteractionTimeouts):
"""Set interaction. Supported by all cookers except MODEL_PRESS1"""
self.send(
"set_interaction",
[
str(settings),
"{:x}".format(timeouts.led_off),
"{:x}".format(timeouts.lid_open),
"{:x}".format(timeouts.lid_open_warning),
],
)
@command(
click.argument("profile", type=str),
default_output=format_output("Setting menu to {profile}"),
)
def set_menu(self, profile: str):
"""Select one of the default(?) cooking profiles"""
if not self._validate_profile(profile):
raise CookerException("Invalid cooking profile: %s" % profile)
self.send("set_menu", [profile])
@command(default_output=format_output("", "Temperature history: {result}\n"))
def get_temperature_history(self) -> TemperatureHistory:
"""Retrieves a temperature history.
The temperature is only available while cooking.
Approx. six data points per minute.
"""
data = self.send("get_temp_history")
return TemperatureHistory(data[0])
@staticmethod
def _validate_profile(profile):
return all(c in string.hexdigits for c in profile) and len(profile) in [
228,
242,
]
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1585507846.5106072
python-miio-0.5.0.1/miio/data/ 0000755 0001750 0001750 00000000000 00000000000 015432 5 ustar 00tpr tpr 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1548870856.0
python-miio-0.5.0.1/miio/data/cooker_profiles.json 0000644 0001750 0001750 00000031607 00000000000 021521 0 ustar 00tpr tpr 0000000 0000000 {
"MODEL_PRESSURE": [
{
"title": "Jingzhu",
"description": "60 minutes cooking for tasty rice",
"profile": "0001E10100000000000080026E10082B126E1412698CAA555555550014280A6E0C02050506050505055A14040A0C0C0D00040505060A0F086E6E20000C0A5A28036468686A0004040500000000000000010202020204040506070708001212180C1E2D2D37000000000000000000000099A5"
},
{
"title": "Kuaizhu",
"description": "Quick 40 minutes cooking",
"profile": "0002E10028000000000080026E10082B056E1412698CAA55555555001428145A10070707070C0E0E105A14060A0C0C0E00090909091E14046E6E200010065A28035050505000040405000000000000030203030302040405070C0C0C00121218100F0F0F3200000000000000000000008914"
},
{
"title": "Zhuzhou",
"description": "Cooking on slow fire from 40 minutes to 4 hours",
"profile": "0003E2011E04000028008000145A46736E140F200000027382736E14002000001E695F736E140C200000017882736E1400200000F07D82735A2300200000000000000000000000000000000000000000000001507896030303035F624B085555555580191028036E0000000000000000DDA4"
},
{
"title": "Baowen",
"description": "Keeping warm at 73 degrees",
"profile": "00040C1800180000010000107891826E6E14002000001E464B6E6E140A2000000000000000000000000000000000000000000000F08282446E140020080800000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000005093"
},
{
"title": "Cake",
"description": "Baking for 40-60 minutes",
"profile": "010088003201000028000012000000000000000000000846822A6E14002018000F6E82736E140A201810000000000000000000003C8782716E1400200A100000000000000000000000000000000000000000000000000000000000003C0A000000008700000000000000000000000000424D"
},
{
"title": "Yoghurt",
"description": "6-12 hours for yogurt fermentation",
"profile": "01010908000C0006000000101E23736E6E1405200000000000000000000000000000000000000000000000000000000000000000F06E73246E140020000C0000000000000000000000000000000000000000020000000000000000000000000000004900000000290000000000000000424D"
},
{
"title": "Refan",
"description": "Cooking rice at 90 degrees",
"profile": "010264001e0023001900800000000000000000000000000000000000000000000f5582736e140a20180000000000000000000000148273735a1408201800000000000000000000000982735a6e140020100a000000000808080869694b0a000000008700000000000000000000000000ddf2"
},
{
"title": "Cooking",
"description": "Steaming at 100 degrees",
"profile": "010326001e0100000a00800000000000000000000000000000000000000000001e695f736e140f200000000000000000000000003c80736e5a0d081400000000000000000000000000000000000000000000015078960808080873694b0e545656568000000000000000000000000000fa31"
},
{
"title": "Sweet rice",
"description": "75 minutes cooking to preserve taste of the food",
"profile": "010461010F000000000080026E10082B126E1412698CAA55555555001428145A1005070708090909095A14060D0D0D0F001E1E1E1E0A14066E6E20000C0A5A2803505050500004040500000000000003020303030202020206070708001212180C1E2D2D370000000000000000000000424D"
},
{
"title": "Quick rice",
"description": "Cooking for 30 minutes",
"profile": "010561001e000000000080026e10082b006e141278a0be55555555001428145a1005070708090909095a140614140e0e00050505050a14076e6e20000c035a2803505050500004040500000000000003020303030202020212120c0c121212180c1e2d2d370000000000000000000000918b"
}
],
"MODEL_NORMAL_GROUP1": [
{
"title": "Jingzhu",
"description": "60 minutes cooking for tasty rice",
"profile": "0001E101000000000000800200A00069030103730000085A020000EB006B040103740000095A0400012D006E0501037400000A5A0401FFFF00700601047600000C5A0401052D0A0F3C0A1E91FF820E01FF05FF78826EFF10FF786E02690F0DFF826EFF691000FF826EFF69100069FF5AFF00000000000081E9"
},
{
"title": "Kuaizhu",
"description": "Quick 40 minutes cooking",
"profile": "0002E100280000000000800200D20069030103730000075A0200012D006B040103740000075A02000182006E050003740000095A0401FFFF0070060004760000095A040100280A063C0D1E91FF820E01FF05FF78826EFF10FF786E02690F0FFF826EFF69100082826EFF69100069FF5AFF00000000000015B6"
},
{
"title": "Zhuzhou",
"description": "Cooking on slow fire from 40 minutes to 4 hours",
"profile": "0003E2011E040000280080000190551C0601001E00000000000001B8551C0601002300000000000001E0561C0600002E000000000000FFFF571C0600003000000000000000280A0082001E914E730E01001E82FF736E0610FF756E02690A1E75826E0269100F75826E0269100069005A0000000000000000CB"
},
{
"title": "Baowen",
"description": "Keeping warm at 73 degrees",
"profile": "00040C180018000001000045000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A0082001E914E730801001E82FF736E0610FF756E02690A0F75826E0169101E75826E0169100069005A000000000000001BA2"
},
{
"title": "Cake",
"description": "Baking for 40-60 minutes",
"profile": "010088010001000028000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C00089178320600001E8278666E041000826E00690FFF96666E0469100082826E0069100069005A00000000000000B267"
},
{
"title": "Yoghurt",
"description": "6-12 hours for yogurt fermentation",
"profile": "01010908000C000600000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A0F270300915573000000008278736E001000756E0028050075826E002810FF75276E0228100069005A00000000000000B9BF"
},
{
"title": "Refan",
"description": "Cooking rice at 90 degrees",
"profile": "010264001E00230019010540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C000F91416E080000008278736E001000756E00690F0C75506E0669100082826E0869100069025004000000000000F5C6"
},
{
"title": "Cooking",
"description": "Steaming at 100 degrees",
"profile": "010366001E0100000A010540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C001E9155730E0000008278736E001000756E00690A0075826E006910FF756E6E0869100069005A00000000000000F462"
},
{
"title": "Sweet rice",
"description": "70 minutes cooking to preserve taste of the food",
"profile": "0104E1010A0000000000800200A00069030102780000085A020000EB006B040102780000085A0400012D006E0501027D0000065A0400FFFF00700601027D0000065A0400052D0A0F3C0A1E91FF820E01FF05FF78826EFF10FF786E02690F1EFF826EFF691400FF826EFF69100069FF5AFF00000000000042A7"
}
],
"MODEL_NORMAL_GROUP2": [
{
"title": "Jingzhu",
"description": "60 minutes cooking for tasty rice",
"profile": "0001E101000000000000800200A00069030103730000085A020000EB006B040103740000095A0400012D006E0501037400000A5A0401FFFF00700601047600000C5A0401052D0A0F3C0A1E91FF820E01FF05FF78826EFF10FF786E02690F0DFF826EFF691000FF826EFF69100069FF5AFF00000000000081E9"
},
{
"title": "Kuaizhu",
"description": "Quick 40 minutes cooking",
"profile": "0002E100280000000000800200D20069030103730000075A0200012D006B040103740000075A02000182006E050003740000095A0401FFFF0070060004760000095A040100280A063C0D1E91FF820E01FF05FF78826EFF10FF786E02690F0FFF826EFF69100082826EFF69100069FF5AFF00000000000015B6"
},
{
"title": "Zhuzhou",
"description": "Cooking on slow fire from 40 minutes to 4 hours",
"profile": "0003E2011E04000028008000019055140601001600000000000001B855140601001900000000000001E0561406000020000000000000FFFF57140600002200000000000000280A0082001E914E730E01001E82FF736E0610FF756E02690A1E75826E0269100F75826E0269100069005A000000000000001A2B"
},
{
"title": "Baowen",
"description": "Keeping warm at 73 degrees",
"profile": "00040C180018000001000045000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A0082001E914E730801001E82FF736E0610FF756E02690A0F75826E0169101E75826E0169100069005A000000000000001BA2"
},
{
"title": "Cake",
"description": "Baking for 40-60 minutes",
"profile": "010088010001000028000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C00089178320600001E8278666E041000826E00690FFF96666E0469100082826E0069100069005A00000000000000B267"
},
{
"title": "Yoghurt",
"description": "6-12 hours for yogurt fermentation",
"profile": "01010908000C000600000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A0F270300915573000000008278736E001000756E0028050075826E002810FF75276E0228100069005A00000000000000B9BF"
},
{
"title": "Refan",
"description": "Cooking rice at 90 degrees",
"profile": "010264001E00230019010540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C000F91416E080000008278736E001000756E00690F0C75506E0669100082826E0869100069025004000000000000F5C6"
},
{
"title": "Cooking",
"description": "Steaming at 100 degrees",
"profile": "0103E6001E0100000A0105400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C00089155730E0000008278736E001000756E00690A0075826E006910FF756E6E0869100069005A00000000000000000000009EB6"
},
{
"title": "Sweet rice",
"description": "70 minutes cooking to preserve taste of the food",
"profile": "0104E1010A0000000000800200A00069030102780000085A020000EB006B040102780000085A0400012D006E0501027D0000065A0400FFFF00700601027D0000065A0400052D0A0F3C0A1E91FF820E01FF05FF78826EFF10FF786E02690F1EFF826EFF691400FF826EFF69100069FF5AFF00000000000042A7"
}
],
"MODEL_NORMAL1": [
{
"title": "Jingzhu",
"description": "60 minutes cooking for tasty rice",
"profile": "0001E101000000000000800200A00069030103730000085A020000EB006B040103740000095A0400012D006E0501037400000A5A0401FFFF00700601047600000C5A0401052D0A0F3C0A1E91FF820E01FF05FF78826EFF10FF786E02690F0DFF826EFF691000FF826EFF69100069FF5AFF00000000000081E9"
},
{
"title": "Kuaizhu",
"description": "Quick 40 minutes cooking",
"profile": "0002E100280000000000800200D20069030103730000075A0200012D006B040103740000075A02000182006E050003740000095A0401FFFF0070060004760000095A040100280A063C0D1E91FF820E01FF05FF78826EFF10FF786E02690F0FFF826EFF69100082826EFF69100069FF5AFF00000000000015B6"
},
{
"title": "Zhuzhou",
"description": "Cooking on slow fire from 40 minutes to 4 hours",
"profile": "0003E2011E04000028008000019055140601001600000000000001B855140601001900000000000001E0561406000020000000000000FFFF57140600002200000000000000280A0082001E914E730E01001E82FF736E0610FF756E02690A1E75826E0269100F75826E0269100069005A000000000000001A2B"
},
{
"title": "Baowen",
"description": "Keeping warm at 73 degrees",
"profile": "00040C180018000001000045000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A0082001E914E730801001E82FF736E0610FF756E02690A0F75826E0169101E75826E0169100069005A000000000000001BA2"
},
{
"title": "Cake",
"description": "Baking for 40-60 minutes",
"profile": "010088010001000028000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C00089178320600001E8278666E041000826E00690FFF96666E0469100082826E0069100069005A00000000000000B267"
},
{
"title": "Yoghurt",
"description": "6-12 hours for yogurt fermentation",
"profile": "01010908000C000600000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A0F270300915573000000008278736E001000756E0028050075826E002810FF75276E0228100069005A00000000000000B9BF"
},
{
"title": "Refan",
"description": "Cooking rice at 90 degrees",
"profile": "010264001E00230019010540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C000F91416E080000008278736E001000756E00690F0C75506E0669100082826E0869100069025004000000000000F5C6"
},
{
"title": "Cooking",
"description": "Steaming at 100 degrees",
"profile": "010366001E0100000A010540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280A003C001E9155730E0000008278736E001000756E00690A0075826E006910FF756E6E0869100069005A00000000000000F462"
},
{
"title": "Sweet rice",
"description": "70 minutes cooking to preserve taste of the food",
"profile": "0104E1010A0000000000800200A00069030102780000085A020000EB006B040102780000085A0400012D006E0501027D0000065A0400FFFF00700601027D0000065A0400052D0A0F3C0A1E91FF820E01FF05FF78826EFF10FF786E02690F1EFF826EFF691400FF826EFF69100069FF5AFF00000000000042A7"
}
]
}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/device.py 0000644 0001750 0001750 00000013270 00000000000 016335 0 ustar 00tpr tpr 0000000 0000000 import logging
from enum import Enum
from typing import Any, Optional # noqa: F401
import click
from .click_common import DeviceGroupMeta, LiteralParamType, command, format_output
from .miioprotocol import MiIOProtocol
_LOGGER = logging.getLogger(__name__)
class UpdateState(Enum):
Downloading = "downloading"
Installing = "installing"
Failed = "failed"
Idle = "idle"
class DeviceInfo:
"""Container of miIO device information.
Hardware properties such as device model, MAC address, memory information,
and hardware and software information is contained here."""
def __init__(self, data):
"""
Response of a Xiaomi Smart WiFi Plug
{'ap': {'bssid': 'FF:FF:FF:FF:FF:FF', 'rssi': -68, 'ssid': 'network'},
'cfg_time': 0,
'fw_ver': '1.2.4_16',
'hw_ver': 'MW300',
'life': 24,
'mac': '28:FF:FF:FF:FF:FF',
'mmfree': 30312,
'model': 'chuangmi.plug.m1',
'netif': {'gw': '192.168.xxx.x',
'localIp': '192.168.xxx.x',
'mask': '255.255.255.0'},
'ot': 'otu',
'ott_stat': [0, 0, 0, 0],
'otu_stat': [320, 267, 3, 0, 3, 742],
'token': '2b00042f7481c7b056c4b410d28f33cf',
'wifi_fw_ver': 'SD878x-14.76.36.p84-702.1.0-WM'}
"""
self.data = data
def __repr__(self):
return "%s v%s (%s) @ %s - token: %s" % (
self.data["model"],
self.data["fw_ver"],
self.data["mac"],
self.network_interface["localIp"],
self.data["token"],
)
def __json__(self):
return self.data
@property
def network_interface(self):
"""Information about network configuration."""
return self.data["netif"]
@property
def accesspoint(self):
"""Information about connected wlan accesspoint."""
return self.data["ap"]
@property
def model(self) -> Optional[str]:
"""Model string if available."""
if self.data["model"] is not None:
return self.data["model"]
return None
@property
def firmware_version(self) -> Optional[str]:
"""Firmware version if available."""
if self.data["fw_ver"] is not None:
return self.data["fw_ver"]
return None
@property
def hardware_version(self) -> Optional[str]:
"""Hardware version if available."""
if self.data["hw_ver"] is not None:
return self.data["hw_ver"]
return None
@property
def mac_address(self) -> Optional[str]:
"""MAC address if available."""
if self.data["mac"] is not None:
return self.data["mac"]
return None
@property
def raw(self):
"""Raw data as returned by the device."""
return self.data
class Device(metaclass=DeviceGroupMeta):
"""Base class for all device implementations.
This is the main class providing the basic protocol handling for devices using
the ``miIO`` protocol.
This class should not be initialized directly but a device-specific class inheriting
it should be used instead of it."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
self.ip = ip
self.token = token
self._protocol = MiIOProtocol(ip, token, start_id, debug, lazy_discover)
def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
return self._protocol.send(command, parameters, retry_count)
def send_handshake(self):
return self._protocol.send_handshake()
@command(
click.argument("command", type=str, required=True),
click.argument("parameters", type=LiteralParamType(), required=False),
)
def raw_command(self, command, parameters):
"""Send a raw command to the device.
This is mostly useful when trying out commands which are not
implemented by a given device instance.
:param str command: Command to send
:param dict parameters: Parameters to send"""
return self._protocol.send(command, parameters)
@command(
default_output=format_output(
"",
"Model: {result.model}\n"
"Hardware version: {result.hardware_version}\n"
"Firmware version: {result.firmware_version}\n"
"Network: {result.network_interface}\n"
"AP: {result.accesspoint}\n",
)
)
def info(self) -> DeviceInfo:
"""Get miIO protocol information from the device.
This includes information about connected wlan network,
and hardware and software versions."""
return DeviceInfo(self._protocol.send("miIO.info"))
def update(self, url: str, md5: str):
"""Start an OTA update."""
payload = {
"mode": "normal",
"install": "1",
"app_url": url,
"file_md5": md5,
"proc": "dnld install",
}
return self._protocol.send("miIO.ota", payload)[0] == "ok"
def update_progress(self) -> int:
"""Return current update progress [0-100]."""
return self._protocol.send("miIO.get_ota_progress")[0]
def update_state(self):
"""Return current update state."""
return UpdateState(self._protocol.send("miIO.get_ota_state")[0])
def configure_wifi(self, ssid, password, uid=0, extra_params=None):
"""Configure the wifi settings."""
if extra_params is None:
extra_params = {}
params = {"ssid": ssid, "passwd": password, "uid": uid, **extra_params}
return self._protocol.send("miIO.config_router", params)[0]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/discovery.py 0000644 0001750 0001750 00000022705 00000000000 017110 0 ustar 00tpr tpr 0000000 0000000 import codecs
import inspect
import ipaddress
import logging
from functools import partial
from typing import Callable, Dict, Optional, Union # noqa: F401
import zeroconf
from . import (
AirConditioningCompanion,
AirFresh,
AirFreshT2017,
AirHumidifier,
AirHumidifierJsq,
AirHumidifierMjjsq,
AirPurifier,
AirPurifierMiot,
AirQualityMonitor,
AqaraCamera,
Ceil,
ChuangmiCamera,
ChuangmiIr,
ChuangmiPlug,
Cooker,
Device,
Fan,
Heater,
PhilipsBulb,
PhilipsEyecare,
PhilipsMoonlight,
PhilipsRwread,
PhilipsWhiteBulb,
PowerStrip,
Toiletlid,
Vacuum,
ViomiVacuum,
WaterPurifier,
WifiRepeater,
WifiSpeaker,
Yeelight,
)
from .airconditioningcompanion import (
MODEL_ACPARTNER_V1,
MODEL_ACPARTNER_V2,
MODEL_ACPARTNER_V3,
)
from .airhumidifier import (
MODEL_HUMIDIFIER_CA1,
MODEL_HUMIDIFIER_CB1,
MODEL_HUMIDIFIER_V1,
)
from .airhumidifier_mjjsq import MODEL_HUMIDIFIER_MJJSQ
from .airqualitymonitor import (
MODEL_AIRQUALITYMONITOR_B1,
MODEL_AIRQUALITYMONITOR_S1,
MODEL_AIRQUALITYMONITOR_V1,
)
from .alarmclock import AlarmClock
from .chuangmi_plug import (
MODEL_CHUANGMI_PLUG_HMI205,
MODEL_CHUANGMI_PLUG_HMI206,
MODEL_CHUANGMI_PLUG_M1,
MODEL_CHUANGMI_PLUG_M3,
MODEL_CHUANGMI_PLUG_V1,
MODEL_CHUANGMI_PLUG_V2,
MODEL_CHUANGMI_PLUG_V3,
)
from .fan import (
MODEL_FAN_P5,
MODEL_FAN_SA1,
MODEL_FAN_V2,
MODEL_FAN_V3,
MODEL_FAN_ZA1,
MODEL_FAN_ZA3,
MODEL_FAN_ZA4,
)
from .heater import MODEL_HEATER_MA1, MODEL_HEATER_ZA1
from .powerstrip import MODEL_POWER_STRIP_V1, MODEL_POWER_STRIP_V2
from .toiletlid import MODEL_TOILETLID_V1
_LOGGER = logging.getLogger(__name__)
DEVICE_MAP = {
"rockrobo-vacuum-v1": Vacuum,
"roborock-vacuum-s5": Vacuum,
"roborock-vacuum-m1s": Vacuum,
"chuangmi-plug-m1": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_M1),
"chuangmi-plug-m3": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_M3),
"chuangmi-plug-v1": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_V1),
"chuangmi-plug-v2": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_V2),
"chuangmi-plug-v3": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_V3),
"chuangmi-plug-hmi205": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_HMI205),
"chuangmi-plug-hmi206": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_HMI206),
"chuangmi-plug_": partial(ChuangmiPlug, model=MODEL_CHUANGMI_PLUG_V1),
"qmi-powerstrip-v1": partial(PowerStrip, model=MODEL_POWER_STRIP_V1),
"zimi-powerstrip-v2": partial(PowerStrip, model=MODEL_POWER_STRIP_V2),
"zimi-clock-myk01": AlarmClock,
"zhimi-airpurifier-m1": AirPurifier, # mini model
"zhimi-airpurifier-m2": AirPurifier, # mini model 2
"zhimi-airpurifier-ma1": AirPurifier, # ms model
"zhimi-airpurifier-ma2": AirPurifier, # ms model 2
"zhimi-airpurifier-sa1": AirPurifier, # super model
"zhimi-airpurifier-sa2": AirPurifier, # super model 2
"zhimi-airpurifier-v1": AirPurifier, # v1
"zhimi-airpurifier-v2": AirPurifier, # v2
"zhimi-airpurifier-v3": AirPurifier, # v3
"zhimi-airpurifier-v5": AirPurifier, # v5
"zhimi-airpurifier-v6": AirPurifier, # v6
"zhimi-airpurifier-v7": AirPurifier, # v7
"zhimi-airpurifier-mc1": AirPurifier, # mc1
"zhimi-airpurifier-mb3": AirPurifierMiot, # mb3 (3/3H)
"zhimi-airpurifier-ma4": AirPurifierMiot, # ma4 (3)
"chuangmi.camera.ipc009": ChuangmiCamera,
"chuangmi-ir-v2": ChuangmiIr,
"chuangmi-remote-h102a03_": ChuangmiIr,
"zhimi-humidifier-v1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_V1),
"zhimi-humidifier-ca1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CA1),
"zhimi-humidifier-cb1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CB1),
"shuii-humidifier-jsq001": partial(AirHumidifierJsq, model=MODEL_HUMIDIFIER_MJJSQ),
"deerma-humidifier-mjjsq": partial(
AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_MJJSQ
),
"yunmi-waterpuri-v2": WaterPurifier,
"philips-light-bulb": PhilipsBulb, # cannot be discovered via mdns
"philips-light-hbulb": PhilipsWhiteBulb, # cannot be discovered via mdns
"philips-light-candle": PhilipsBulb, # cannot be discovered via mdns
"philips-light-candle2": PhilipsBulb, # cannot be discovered via mdns
"philips-light-ceiling": Ceil,
"philips-light-zyceiling": Ceil,
"philips-light-sread1": PhilipsEyecare, # name needs to be checked
"philips-light-moonlight": PhilipsMoonlight, # name needs to be checked
"philips-light-rwread": PhilipsRwread, # name needs to be checked
"xiaomi-wifispeaker-v1": WifiSpeaker, # name needs to be checked
"xiaomi-repeater-v1": WifiRepeater, # name needs to be checked
"xiaomi-repeater-v3": WifiRepeater, # name needs to be checked
"chunmi-cooker-press1": Cooker,
"chunmi-cooker-press2": Cooker,
"chunmi-cooker-normal1": Cooker,
"chunmi-cooker-normal2": Cooker,
"chunmi-cooker-normal3": Cooker,
"chunmi-cooker-normal4": Cooker,
"chunmi-cooker-normal5": Cooker,
"lumi-acpartner-v1": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V1),
"lumi-acpartner-v2": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V2),
"lumi-acpartner-v3": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V3),
"lumi-camera-aq2": AqaraCamera,
"yeelink-light-": Yeelight,
"zhimi-fan-v2": partial(Fan, model=MODEL_FAN_V2),
"zhimi-fan-v3": partial(Fan, model=MODEL_FAN_V3),
"zhimi-fan-sa1": partial(Fan, model=MODEL_FAN_SA1),
"zhimi-fan-za1": partial(Fan, model=MODEL_FAN_ZA1),
"zhimi-fan-za3": partial(Fan, model=MODEL_FAN_ZA3),
"zhimi-fan-za4": partial(Fan, model=MODEL_FAN_ZA4),
"dmaker-fan-p5": partial(Fan, model=MODEL_FAN_P5),
"tinymu-toiletlid-v1": partial(Toiletlid, model=MODEL_TOILETLID_V1),
"zhimi-airfresh-va2": AirFresh,
"dmaker-airfresh-t2017": AirFreshT2017,
"zhimi-airmonitor-v1": partial(AirQualityMonitor, model=MODEL_AIRQUALITYMONITOR_V1),
"cgllc-airmonitor-b1": partial(AirQualityMonitor, model=MODEL_AIRQUALITYMONITOR_B1),
"cgllc-airmonitor-s1": partial(AirQualityMonitor, model=MODEL_AIRQUALITYMONITOR_S1),
"lumi-gateway-": lambda x: other_package_info(
x, "https://github.com/Danielhiversen/PyXiaomiGateway"
),
"viomi-vacuum-v7": ViomiVacuum,
"zhimi.heater.za1": partial(Heater, model=MODEL_HEATER_ZA1),
"zhimi.elecheater.ma1": partial(Heater, model=MODEL_HEATER_MA1),
} # type: Dict[str, Union[Callable, Device]]
def pretty_token(token):
"""Return a pretty string presentation for a token."""
return codecs.encode(token, "hex").decode()
def other_package_info(info, desc):
"""Return information about another package supporting the device."""
return "%s @ %s, check %s" % (info.name, ipaddress.ip_address(info.address), desc)
def create_device(name: str, addr: str, device_cls: partial) -> Device:
"""Return a device object for a zeroconf entry."""
_LOGGER.debug(
"Found a supported '%s', using '%s' class", name, device_cls.func.__name__
)
dev = device_cls(ip=addr)
m = dev.send_handshake()
dev.token = m.checksum
_LOGGER.info(
"Found a supported '%s' at %s - token: %s",
device_cls.func.__name__,
addr,
pretty_token(dev.token),
)
return dev
class Listener:
"""mDNS listener creating Device objects based on detected devices."""
def __init__(self):
self.found_devices = {} # type: Dict[str, Device]
def check_and_create_device(self, info, addr) -> Optional[Device]:
"""Create a corresponding :class:`Device` implementation
for a given info and address.."""
name = info.name
for identifier, v in DEVICE_MAP.items():
if name.startswith(identifier):
if inspect.isclass(v):
return create_device(name, addr, partial(v))
elif type(v) is partial and inspect.isclass(v.func):
return create_device(name, addr, v)
elif callable(v):
dev = Device(ip=addr)
_LOGGER.info(
"%s: token: %s",
v(info),
pretty_token(dev.send_handshake().checksum),
)
return None
_LOGGER.warning(
"Found unsupported device %s at %s, " "please report to developers",
name,
addr,
)
return None
def add_service(self, zeroconf, type, name):
info = zeroconf.get_service_info(type, name)
addr = str(ipaddress.ip_address(info.address))
if addr not in self.found_devices:
dev = self.check_and_create_device(info, addr)
self.found_devices[addr] = dev
class Discovery:
"""mDNS discoverer for miIO based devices (_miio._udp.local).
Calling :func:`discover_mdns` will cause this to subscribe for updates
on ``_miio._udp.local`` until any key is pressed, after which a dict
of detected devices is returned."""
@staticmethod
def discover_mdns() -> Dict[str, Device]:
"""Discover devices with mdns until """
_LOGGER.info("Discovering devices with mDNS, press any key to quit...")
listener = Listener()
browser = zeroconf.ServiceBrowser(
zeroconf.Zeroconf(), "_miio._udp.local.", listener
)
input() # to keep execution running until a key is pressed
browser.cancel()
return listener.found_devices
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585489963.0
python-miio-0.5.0.1/miio/exceptions.py 0000644 0001750 0001750 00000000736 00000000000 017262 0 ustar 00tpr tpr 0000000 0000000 class DeviceException(Exception):
"""Exception wrapping any communication errors with the device."""
pass
class DeviceError(DeviceException):
"""Exception communicating an error delivered by the target device."""
def __init__(self, error):
self.code = error.get("code")
self.message = error.get("message")
class RecoverableError(DeviceError):
"""Exception communicating an recoverable error delivered by the target device."""
pass
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/extract_tokens.py 0000644 0001750 0001750 00000016653 00000000000 020143 0 ustar 00tpr tpr 0000000 0000000 import json
import logging
import sqlite3
import tempfile
import xml.etree.ElementTree as ET
from pprint import pformat as pf
from typing import Iterator
import attr
import click
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
logging.basicConfig(level=logging.INFO)
_LOGGER = logging.getLogger(__name__)
@attr.s
class DeviceConfig:
"""A presentation of a device including its name, model, ip etc."""
name = attr.ib()
mac = attr.ib()
ip = attr.ib()
token = attr.ib()
model = attr.ib()
everything = attr.ib(default=None)
def read_android_yeelight(db) -> Iterator[DeviceConfig]:
"""Read tokens from Yeelight's android backup."""
_LOGGER.info("Reading tokens from Yeelight Android DB")
xml = ET.parse(db)
devicelist = xml.find(".//set[@name='deviceList']")
if not devicelist:
_LOGGER.warning("Unable to find deviceList")
return []
for dev_elem in list(devicelist):
dev = json.loads(dev_elem.text)
ip = dev["localip"]
mac = dev["mac"]
model = dev["model"]
name = dev["name"]
token = dev["token"]
config = DeviceConfig(
name=name, ip=ip, mac=mac, model=model, token=token, everything=dev
)
yield config
class BackupDatabaseReader:
"""Main class for reading backup files.
The main usage is following:
.. code-block:: python
r = BackupDatabaseReader()
devices = r.read_tokens("/tmp/database.sqlite")
for dev in devices:
print("Got %s with token %s" % (dev.ip, dev.token)
"""
def __init__(self, dump_raw=False):
self.dump_raw = dump_raw
@staticmethod
def dump_raw(dev):
"""Dump whole database."""
raw = {k: dev[k] for k in dev.keys()}
_LOGGER.info(pf(raw))
@staticmethod
def decrypt_ztoken(ztoken):
"""Decrypt the given ztoken, used by apple."""
if ztoken is None or len(ztoken) <= 32:
return str(ztoken)
keystring = "00000000000000000000000000000000"
key = bytes.fromhex(keystring)
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
decryptor = cipher.decryptor()
token = decryptor.update(bytes.fromhex(ztoken[:64])) + decryptor.finalize()
return token.decode()
def read_apple(self) -> Iterator[DeviceConfig]:
"""Read Apple-specific database file."""
_LOGGER.info("Reading tokens from Apple DB")
c = self.conn.execute("SELECT * FROM ZDEVICE WHERE ZTOKEN IS NOT '';")
for dev in c.fetchall():
if self.dump_raw:
BackupDatabaseReader.dump_raw(dev)
ip = dev["ZLOCALIP"]
mac = dev["ZMAC"]
model = dev["ZMODEL"]
name = dev["ZNAME"]
token = BackupDatabaseReader.decrypt_ztoken(dev["ZTOKEN"])
config = DeviceConfig(
name=name, mac=mac, ip=ip, model=model, token=token, everything=dev
)
yield config
def read_android(self) -> Iterator[DeviceConfig]:
"""Read Android-specific database file."""
_LOGGER.info("Reading tokens from Android DB")
c = self.conn.execute("SELECT * FROM devicerecord WHERE token IS NOT '';")
for dev in c.fetchall():
if self.dump_raw:
BackupDatabaseReader.dump_raw(dev)
ip = dev["localIP"]
mac = dev["mac"]
model = dev["model"]
name = dev["name"]
token = dev["token"]
config = DeviceConfig(
name=name, ip=ip, mac=mac, model=model, token=token, everything=dev
)
yield config
def read_tokens(self, db) -> Iterator[DeviceConfig]:
"""Read device information out from a given database file.
:param str db: Database file"""
self.db = db
_LOGGER.info("Reading database from %s" % db)
self.conn = sqlite3.connect(db)
self.conn.row_factory = sqlite3.Row
with self.conn:
is_android = (
self.conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='devicerecord';"
).fetchone()
is not None
)
is_apple = (
self.conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='ZDEVICE'"
).fetchone()
is not None
)
if is_android:
yield from self.read_android()
elif is_apple:
yield from self.read_apple()
else:
_LOGGER.error("Error, unknown database type!")
@click.command()
@click.argument("backup")
@click.option(
"--write-to-disk",
type=click.File("wb"),
help="writes sqlite3 db to a file for debugging",
)
@click.option(
"--password", type=str, help="password if the android database is encrypted"
)
@click.option(
"--dump-all", is_flag=True, default=False, help="dump devices without ip addresses"
)
@click.option("--dump-raw", is_flag=True, help="dumps raw rows")
def main(backup, write_to_disk, password, dump_all, dump_raw):
"""Reads device information out from an sqlite3 DB.
If the given file is an Android backup (.ab), the database
will be extracted automatically.
If the given file is an iOS backup, the tokens will be
extracted (and decrypted if needed) automatically.
"""
def read_miio_database(tar):
DBFILE = "apps/com.xiaomi.smarthome/db/miio2.db"
try:
db = tar.extractfile(DBFILE)
except KeyError as ex:
click.echo("Unable to find miio database file %s: %s" % (DBFILE, ex))
return []
if write_to_disk:
file = write_to_disk
else:
file = tempfile.NamedTemporaryFile()
with file as fp:
click.echo("Saving database to %s" % fp.name)
fp.write(db.read())
return list(reader.read_tokens(fp.name))
def read_yeelight_database(tar):
DBFILE = "apps/com.yeelight.cherry/sp/miot.xml"
_LOGGER.info("Trying to read %s", DBFILE)
try:
db = tar.extractfile(DBFILE)
except KeyError as ex:
click.echo("Unable to find yeelight database file %s: %s" % (DBFILE, ex))
return []
return list(read_android_yeelight(db))
devices = []
reader = BackupDatabaseReader(dump_raw)
if backup.endswith(".ab"):
try:
from android_backup import AndroidBackup
except ModuleNotFoundError:
click.echo(
"You need to install android_backup to extract "
"tokens from Android backup files."
)
return
with AndroidBackup(backup, stream=False) as f:
tar = f.read_data(password)
devices.extend(read_miio_database(tar))
devices.extend(read_yeelight_database(tar))
else:
devices = list(reader.read_tokens(backup))
for dev in devices:
if dev.ip or dump_all:
click.echo(
"%s\n"
"\tModel: %s\n"
"\tIP address: %s\n"
"\tToken: %s\n"
"\tMAC: %s" % (dev.name, dev.model, dev.ip, dev.token, dev.mac)
)
if dump_raw:
click.echo(dev)
if __name__ == "__main__":
main()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585421538.0
python-miio-0.5.0.1/miio/fan.py 0000644 0001750 0001750 00000057020 00000000000 015643 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from typing import Any, Dict, Optional
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_FAN_V2 = "zhimi.fan.v2"
MODEL_FAN_V3 = "zhimi.fan.v3"
MODEL_FAN_SA1 = "zhimi.fan.sa1"
MODEL_FAN_ZA1 = "zhimi.fan.za1"
MODEL_FAN_ZA3 = "zhimi.fan.za3"
MODEL_FAN_ZA4 = "zhimi.fan.za4"
MODEL_FAN_P5 = "dmaker.fan.p5"
AVAILABLE_PROPERTIES_COMMON = [
"angle",
"speed",
"poweroff_time",
"power",
"ac_power",
"angle_enable",
"speed_level",
"natural_level",
"child_lock",
"buzzer",
"led_b",
"use_time",
]
AVAILABLE_PROPERTIES_COMMON_V2_V3 = [
"temp_dec",
"humidity",
"battery",
"bat_charge",
"button_pressed",
] + AVAILABLE_PROPERTIES_COMMON
AVAILABLE_PROPERTIES_P5 = [
"power",
"mode",
"speed",
"roll_enable",
"roll_angle",
"time_off",
"light",
"beep_sound",
"child_lock",
]
AVAILABLE_PROPERTIES = {
MODEL_FAN_V2: ["led", "bat_state"] + AVAILABLE_PROPERTIES_COMMON_V2_V3,
MODEL_FAN_V3: AVAILABLE_PROPERTIES_COMMON_V2_V3,
MODEL_FAN_SA1: AVAILABLE_PROPERTIES_COMMON,
MODEL_FAN_ZA1: AVAILABLE_PROPERTIES_COMMON,
MODEL_FAN_ZA3: AVAILABLE_PROPERTIES_COMMON,
MODEL_FAN_ZA4: AVAILABLE_PROPERTIES_COMMON,
MODEL_FAN_P5: AVAILABLE_PROPERTIES_P5,
}
class FanException(DeviceException):
pass
class OperationMode(enum.Enum):
Normal = "normal"
Nature = "nature"
class LedBrightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
class MoveDirection(enum.Enum):
Left = "left"
Right = "right"
class FanStatus:
"""Container for status reports from the Xiaomi Mi Smart Pedestal Fan."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Fan (zhimi.fan.v3):
{'temp_dec': 232, 'humidity': 46, 'angle': 118, 'speed': 298,
'poweroff_time': 0, 'power': 'on', 'ac_power': 'off', 'battery': 98,
'angle_enable': 'off', 'speed_level': 1, 'natural_level': 0,
'child_lock': 'off', 'buzzer': 'on', 'led_b': 1, 'led': None,
'natural_enable': None, 'use_time': 0, 'bat_charge': 'complete',
'bat_state': None, 'button_pressed':'speed'}
Response of a Fan (zhimi.fan.sa1):
{'angle': 120, 'speed': 277, 'poweroff_time': 0, 'power': 'on',
'ac_power': 'on', 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 2,
'child_lock': 'off', 'buzzer': 0, 'led_b': 0, 'use_time': 2318}
Response of a Fan (zhimi.fan.sa4):
{'angle': 120, 'speed': 327, 'poweroff_time': 0, 'power': 'on',
'ac_power': 'on', 'angle_enable': 'off', 'speed_level': 1, 'natural_level': 0,
'child_lock': 'off', 'buzzer': 2, 'led_b': 0, 'use_time': 85}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""True if device is currently on."""
return self.power == "on"
@property
def humidity(self) -> Optional[int]:
"""Current humidity."""
if "humidity" in self.data and self.data["humidity"] is not None:
return self.data["humidity"]
return None
@property
def temperature(self) -> Optional[float]:
"""Current temperature, if available."""
if "temp_dec" in self.data and self.data["temp_dec"] is not None:
return self.data["temp_dec"] / 10.0
return None
@property
def led(self) -> Optional[bool]:
"""True if LED is turned on, if available."""
if "led" in self.data and self.data["led"] is not None:
return self.data["led"] == "on"
return None
@property
def led_brightness(self) -> Optional[LedBrightness]:
"""LED brightness, if available."""
if self.data["led_b"] is not None:
return LedBrightness(self.data["led_b"])
return None
@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["buzzer"] in ["on", 1, 2]
@property
def child_lock(self) -> bool:
"""True if child lock is on."""
return self.data["child_lock"] == "on"
@property
def natural_speed(self) -> Optional[int]:
"""Speed level in natural mode."""
if "natural_level" in self.data and self.data["natural_level"] is not None:
return self.data["natural_level"]
@property
def direct_speed(self) -> Optional[int]:
"""Speed level in direct mode."""
if "speed_level" in self.data and self.data["speed_level"] is not None:
return self.data["speed_level"]
@property
def oscillate(self) -> bool:
"""True if oscillation is enabled."""
return self.data["angle_enable"] == "on"
@property
def battery(self) -> Optional[int]:
"""Current battery level."""
if "battery" in self.data and self.data["battery"] is not None:
return self.data["battery"]
@property
def battery_charge(self) -> Optional[str]:
"""State of the battery charger, if available."""
if "bat_charge" in self.data and self.data["bat_charge"] is not None:
return self.data["bat_charge"]
return None
@property
def battery_state(self) -> Optional[str]:
"""State of the battery, if available."""
if "bat_state" in self.data and self.data["bat_state"] is not None:
return self.data["bat_state"]
return None
@property
def ac_power(self) -> bool:
"""True if powered by AC."""
return self.data["ac_power"] == "on"
@property
def delay_off_countdown(self) -> int:
"""Countdown until turning off in seconds."""
return self.data["poweroff_time"]
@property
def speed(self) -> int:
"""Speed of the motor."""
return self.data["speed"]
@property
def angle(self) -> int:
"""Current angle."""
return self.data["angle"]
@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
def button_pressed(self) -> Optional[str]:
"""Last pressed button."""
if "button_pressed" in self.data and self.data["button_pressed"] is not None:
return self.data["button_pressed"]
return None
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.temperature,
self.humidity,
self.led,
self.led_brightness,
self.buzzer,
self.child_lock,
self.natural_speed,
self.direct_speed,
self.speed,
self.oscillate,
self.angle,
self.ac_power,
self.battery,
self.battery_charge,
self.battery_state,
self.use_time,
self.delay_off_countdown,
self.button_pressed,
)
)
return s
def __json__(self):
return self.data
class FanStatusP5:
"""Container for status reports from the Xiaomi Mi Smart Pedestal Fan DMaker P5."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Fan (dmaker.fan.p5):
{'power': False, 'mode': 'normal', 'speed': 35, 'roll_enable': False,
'roll_angle': 140, 'time_off': 0, 'light': True, 'beep_sound': False,
'child_lock': False}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return "on" if self.data["power"] else "off"
@property
def is_on(self) -> bool:
"""True if device is currently on."""
return self.data["power"]
@property
def mode(self) -> OperationMode:
"""Operation mode."""
return OperationMode(self.data["mode"])
@property
def speed(self) -> int:
"""Speed of the motor."""
return self.data["speed"]
@property
def oscillate(self) -> bool:
"""True if oscillation is enabled."""
return self.data["roll_enable"]
@property
def angle(self) -> int:
"""Oscillation angle."""
return self.data["roll_angle"]
@property
def delay_off_countdown(self) -> int:
"""Countdown until turning off in seconds."""
return self.data["time_off"]
@property
def led(self) -> bool:
"""True if LED is turned on, if available."""
return self.data["light"]
@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["beep_sound"]
@property
def child_lock(self) -> bool:
"""True if child lock is on."""
return self.data["child_lock"]
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.mode,
self.speed,
self.oscillate,
self.angle,
self.led,
self.buzzer,
self.child_lock,
self.delay_off_countdown,
)
)
return s
def __json__(self):
return self.data
class Fan(Device):
"""Main class representing the Xiaomi Mi Smart Pedestal Fan."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_FAN_V3,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_FAN_V3
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Battery: {result.battery} %\n"
"AC power: {result.ac_power}\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"LED: {result.led}\n"
"LED brightness: {result.led_brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Speed: {result.speed}\n"
"Natural speed: {result.natural_speed}\n"
"Direct speed: {result.direct_speed}\n"
"Oscillate: {result.oscillate}\n"
"Power-off time: {result.delay_off_countdown}\n"
"Angle: {result.angle}\n",
)
)
def status(self) -> FanStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props_per_request = 15
# The SA1, ZA1, ZA3 and ZA4 is limited to a single property per request
if self.model in [MODEL_FAN_SA1, MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4]:
_props_per_request = 1
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:_props_per_request]))
_props[:] = _props[_props_per_request:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return FanStatus(dict(zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("speed", type=int),
default_output=format_output("Setting speed of the natural mode to {speed}"),
)
def set_natural_speed(self, speed: int):
"""Set natural level."""
if speed < 0 or speed > 100:
raise FanException("Invalid speed: %s" % speed)
return self.send("set_natural_level", [speed])
@command(
click.argument("speed", type=int),
default_output=format_output("Setting speed of the direct mode to {speed}"),
)
def set_direct_speed(self, speed: int):
"""Set speed of the direct mode."""
if speed < 0 or speed > 100:
raise FanException("Invalid speed: %s" % speed)
return self.send("set_speed_level", [speed])
@command(
click.argument("direction", type=EnumType(MoveDirection, False)),
default_output=format_output("Rotating the fan to the {direction}"),
)
def set_rotate(self, direction: MoveDirection):
"""Rotate the fan by -5/+5 degrees left/right."""
return self.send("set_move", [direction.value])
@command(
click.argument("angle", type=int),
default_output=format_output("Setting angle to {angle}"),
)
def set_angle(self, angle: int):
"""Set the oscillation angle."""
if angle < 0 or angle > 120:
raise FanException("Invalid angle: %s" % angle)
return self.send("set_angle", [angle])
@command(
click.argument("oscillate", type=bool),
default_output=format_output(
lambda oscillate: "Turning on oscillate"
if oscillate
else "Turning off oscillate"
),
)
def set_oscillate(self, oscillate: bool):
"""Set oscillate on/off."""
if oscillate:
return self.send("set_angle_enable", ["on"])
else:
return self.send("set_angle_enable", ["off"])
@command(
click.argument("brightness", type=EnumType(LedBrightness, False)),
default_output=format_output("Setting LED brightness to {brightness}"),
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
return self.send("set_led_b", [brightness.value])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off. Not supported by model SA1."""
if led:
return self.send("set_led", ["on"])
else:
return self.send("set_led", ["off"])
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if self.model in [MODEL_FAN_SA1, MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4]:
if buzzer:
return self.send("set_buzzer", [2])
else:
return self.send("set_buzzer", [0])
if buzzer:
return self.send("set_buzzer", ["on"])
else:
return self.send("set_buzzer", ["off"])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])
@command(
click.argument("seconds", type=int),
default_output=format_output("Setting delayed turn off to {seconds} seconds"),
)
def delay_off(self, seconds: int):
"""Set delay off seconds."""
if seconds < 1:
raise FanException("Invalid value for a delayed turn off: %s" % seconds)
return self.send("set_poweroff_time", [seconds])
class FanV2(Fan):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_V2)
class FanSA1(Fan):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_SA1)
class FanZA1(Fan):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_ZA1)
class FanZA3(Fan):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_ZA3)
class FanZA4(Fan):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_ZA4)
class FanP5(Device):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_FAN_P5,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_FAN_P5
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Operation mode: {result.mode}\n"
"Speed: {result.speed}\n"
"Oscillate: {result.oscillate}\n"
"Angle: {result.angle}\n"
"LED: {result.led}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Power-off time: {result.delay_off_countdown}\n",
)
)
def status(self) -> FanStatusP5:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props_per_request = 15
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:_props_per_request]))
_props[:] = _props[_props_per_request:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return FanStatusP5(dict(zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("s_power", [True])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("s_power", [False])
@command(
click.argument("mode", type=EnumType(OperationMode, False)),
default_output=format_output("Setting mode to '{mode.value}'"),
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.send("s_mode", [mode.value])
@command(
click.argument("speed", type=int),
default_output=format_output("Setting speed to {speed}"),
)
def set_speed(self, speed: int):
"""Set speed."""
if speed < 0 or speed > 100:
raise FanException("Invalid speed: %s" % speed)
return self.send("s_speed", [speed])
@command(
click.argument("angle", type=int),
default_output=format_output("Setting angle to {angle}"),
)
def set_angle(self, angle: int):
"""Set the oscillation angle."""
if angle not in [30, 60, 90, 120, 140]:
raise FanException(
"Unsupported angle. Supported values: 30, 60, 90, 120, 140"
)
return self.send("s_angle", [angle])
@command(
click.argument("oscillate", type=bool),
default_output=format_output(
lambda oscillate: "Turning on oscillate"
if oscillate
else "Turning off oscillate"
),
)
def set_oscillate(self, oscillate: bool):
"""Set oscillate on/off."""
if oscillate:
return self.send("s_roll", [True])
else:
return self.send("s_roll", [False])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on LED" if led else "Turning off LED"
),
)
def set_led(self, led: bool):
"""Turn led on/off."""
if led:
return self.send("s_light", [True])
else:
return self.send("s_light", [False])
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if buzzer:
return self.send("s_sound", [True])
else:
return self.send("s_sound", [False])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("s_lock", [True])
else:
return self.send("s_lock", [False])
@command(
click.argument("minutes", type=int),
default_output=format_output("Setting delayed turn off to {minutes} minutes"),
)
def delay_off(self, minutes: int):
"""Set delay off minutes."""
if minutes < 1:
raise FanException("Invalid value for a delayed turn off: %s" % minutes)
return self.send("s_t_off", [minutes])
@command(
click.argument("direction", type=EnumType(MoveDirection, False)),
default_output=format_output("Rotating the fan to the {direction}"),
)
def set_rotate(self, direction: MoveDirection):
"""Rotate the fan by -5/+5 degrees left/right."""
return self.send("m_roll", [direction.value])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500788.0
python-miio-0.5.0.1/miio/gateway.py 0000644 0001750 0001750 00000042133 00000000000 016537 0 ustar 00tpr tpr 0000000 0000000 import logging
from datetime import datetime
from enum import IntEnum
from typing import Optional
import click
from .click_common import command, format_output
from .device import Device
from .utils import brightness_and_color_to_int, int_to_brightness, int_to_rgb
_LOGGER = logging.getLogger(__name__)
color_map = {
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
"white": (255, 255, 255),
"yellow": (255, 255, 0),
"orange": (255, 165, 0),
"aqua": (0, 255, 255),
"olive": (128, 128, 0),
"purple": (128, 0, 128),
}
class DeviceType(IntEnum):
Gateway = 0
Switch = 1
Motion = 2
Magnet = 3
SwitchTwoChannels = 7
Cube = 8
SwitchOneChannel = 9
SensorHT = 10
Plug = 11
AqaraHT = 19
SwitchLiveOneChannel = 20
SwitchLiveTwoChannels = 21
AqaraSwitch = 51
AqaraMotion = 52
AqaraMagnet = 53
class Gateway(Device):
"""Main class representing the Xiaomi Gateway.
Use the given property getters to access specific functionalities such
as `alarm` (for alarm controls) or `light` (for lights).
Commands whose functionality or parameters are unknown, feel free to implement!
* toggle_device
* toggle_plug
* remove_all_bind
* list_bind [0]
* welcome
* set_curtain_level
* get_corridor_on_time
* set_corridor_light ["off"]
* get_corridor_light -> "on"
* set_default_sound
* set_doorbell_push, get_doorbell_push ["off"]
* set_doorbell_volume [100], get_doorbell_volume
* set_gateway_volume, get_gateway_volume
* set_clock_volume
* set_clock
* get_sys_data
* update_neighbor_token [{"did":x, "token":x, "ip":x}]
## property getters
* ctrl_device_prop
* get_device_prop_exp [[sid, list, of, properties]]
## scene
* get_lumi_bind ["scene", ] for rooms/devices"""
def __init__(self, ip: str = None, token: str = None) -> None:
super().__init__(ip, token)
self._alarm = GatewayAlarm(self)
self._radio = GatewayRadio(self)
self._zigbee = GatewayZigbee(self)
self._light = GatewayLight(self)
@property
def alarm(self) -> "GatewayAlarm":
"""Return alarm control interface."""
# example: gateway.alarm.on()
return self._alarm
@property
def radio(self) -> "GatewayRadio":
"""Return radio control interface."""
return self._radio
@property
def zigbee(self) -> "GatewayZigbee":
"""Return zigbee control interface."""
return self._zigbee
@property
def light(self) -> "GatewayLight":
"""Return light control interface."""
return self._light
@command()
def devices(self):
"""Return list of devices."""
# from https://github.com/aholstenson/miio/issues/26
devices_raw = self.send("get_device_prop", ["lumi.0", "device_list"])
devices = [
SubDevice(self, *devices_raw[x : x + 5]) # noqa: E203
for x in range(0, len(devices_raw), 5)
]
return devices
@command(click.argument("sid"), click.argument("property"))
def get_device_prop(self, sid, property):
"""Get the value of a property for given sid."""
return self.send("get_device_prop", [sid, property])
@command(click.argument("sid"), click.argument("properties", nargs=-1))
def get_device_prop_exp(self, sid, properties):
"""Get the value of a bunch of properties for given sid."""
return self.send("get_device_prop_exp", [[sid] + list(properties)])
@command(click.argument("sid"), click.argument("property"), click.argument("value"))
def set_device_prop(self, sid, property, value):
"""Set the device property."""
return self.send("set_device_prop", {"sid": sid, property: value})
@command()
def clock(self):
"""Alarm clock"""
# payload of clock volume ("get_clock_volume") already in get_clock response
return self.send("get_clock")
# Developer key
@command()
def get_developer_key(self):
"""Return the developer API key."""
return self.send("get_lumi_dpf_aes_key")[0]
@command(click.argument("key"))
def set_developer_key(self, key):
"""Set the developer API key."""
if len(key) != 16:
click.echo("Key must be of length 16, was %s" % len(key))
return self.send("set_lumi_dpf_aes_key", [key])
@command()
def timezone(self):
"""Get current timezone."""
return self.send("get_device_prop", ["lumi.0", "tzone_sec"])
@command()
def get_illumination(self):
"""Get illumination. In lux?"""
return self.send("get_illumination")[0]
class GatewayAlarm(Device):
"""Class representing the Xiaomi Gateway Alarm."""
def __init__(self, parent) -> None:
self._device = parent
@command(default_output=format_output("[alarm_status]"))
def status(self) -> str:
"""Return the alarm status from the device."""
# Response: 'on', 'off', 'oning'
return self._device.send("get_arming").pop()
@command(default_output=format_output("Turning alarm on"))
def on(self):
"""Turn alarm on."""
return self._device.send("set_arming", ["on"])
@command(default_output=format_output("Turning alarm off"))
def off(self):
"""Turn alarm off."""
return self._device.send("set_arming", ["off"])
@command()
def arming_time(self) -> int:
"""Return time in seconds the alarm stays 'oning' before transitioning to 'on'"""
# Response: 5, 15, 30, 60
return self._device.send("get_arm_wait_time").pop()
@command(click.argument("seconds"))
def set_arming_time(self, seconds):
"""Set time the alarm stays at 'oning' before transitioning to 'on'"""
return self._device.send("set_arm_wait_time", [seconds])
@command()
def triggering_time(self) -> int:
"""Return the time in seconds the alarm is going off when triggered"""
# Response: 30, 60, etc.
return self._device.send("get_device_prop", ["lumi.0", "alarm_time_len"]).pop()
@command(click.argument("seconds"))
def set_triggering_time(self, seconds):
"""Set the time in seconds the alarm is going off when triggered"""
return self._device.send(
"set_device_prop", {"sid": "lumi.0", "alarm_time_len": seconds}
)
@command()
def triggering_light(self) -> int:
"""Return the time the gateway light blinks when the alarm is triggerd"""
# Response: 0=do not blink, 1=always blink, x>1=blink for x seconds
return self._device.send("get_device_prop", ["lumi.0", "en_alarm_light"]).pop()
@command(click.argument("seconds"))
def set_triggering_light(self, seconds):
"""Set the time the gateway light blinks when the alarm is triggerd"""
# values: 0=do not blink, 1=always blink, x>1=blink for x seconds
return self._device.send(
"set_device_prop", {"sid": "lumi.0", "en_alarm_light": seconds}
)
@command()
def triggering_volume(self) -> int:
"""Return the volume level at which alarms go off [0-100]"""
return self._device.send("get_alarming_volume").pop()
@command(click.argument("volume"))
def set_triggering_volume(self, volume):
"""Set the volume level at which alarms go off [0-100]"""
return self._device.send("set_alarming_volume", [volume])
@command()
def last_status_change_time(self):
"""Return the last time the alarm changed status, type datetime.datetime"""
return datetime.fromtimestamp(self._device.send("get_arming_time").pop())
class GatewayZigbee(Device):
"""Zigbee controls."""
def __init__(self, parent) -> None:
self._device = parent
@command()
def get_zigbee_version(self):
"""timeouts on device"""
return self._device.send("get_zigbee_device_version")
@command()
def get_zigbee_channel(self):
"""Return currently used zigbee channel."""
return self._device.send("get_zigbee_channel")[0]
@command(click.argument("channel"))
def set_zigbee_channel(self, channel):
"""Set zigbee channel."""
return self._device.send("set_zigbee_channel", [channel])
@command(click.argument("timeout", type=int))
def zigbee_pair(self, timeout):
"""Start pairing, use 0 to disable"""
return self._device.send("start_zigbee_join", [timeout])
def send_to_zigbee(self):
"""How does this differ from writing? Unknown."""
raise NotImplementedError()
return self._device.send("send_to_zigbee")
def read_zigbee_eep(self):
"""Read eeprom?"""
raise NotImplementedError()
return self._device.send("read_zig_eep", [0]) # 'ok'
def read_zigbee_attribute(self):
"""Read zigbee data?"""
raise NotImplementedError()
return self._device.send("read_zigbee_attribute", [0x0000, 0x0080])
def write_zigbee_attribute(self):
"""Unknown parameters."""
raise NotImplementedError()
return self._device.send("write_zigbee_attribute")
@command()
def zigbee_unpair_all(self):
"""Unpair all devices."""
return self._device.send("remove_all_device")
def zigbee_unpair(self, sid):
"""Unpair a device."""
# get a device obj an call dev.unpair()
raise NotImplementedError()
class GatewayRadio(Device):
"""Radio controls for the gateway."""
def __init__(self, parent) -> None:
self._device = parent
@command()
def get_radio_info(self):
"""Radio play info."""
return self._device.send("get_prop_fm")
@command(click.argument("volume"))
def set_radio_volume(self, volume):
"""Set radio volume"""
return self._device.send("set_fm_volume", [volume])
def play_music_new(self):
"""Unknown."""
# {'from': '4', 'id': 9514, 'method': 'set_default_music', 'params': [2, '21']}
# {'from': '4', 'id': 9515, 'method': 'play_music_new', 'params': ['21', 0]}
raise NotImplementedError()
def play_specify_fm(self):
"""play specific stream?"""
raise NotImplementedError()
# {"from": "4", "id": 65055, "method": "play_specify_fm",
# "params": {"id": 764, "type": 0, "url": "http://live.xmcdn.com/live/764/64.m3u8"}}
return self._device.send("play_specify_fm")
def play_fm(self):
"""radio on/off?"""
raise NotImplementedError()
# play_fm","params":["off"]}
return self._device.send("play_fm")
def volume_ctrl_fm(self):
"""Unknown."""
raise NotImplementedError()
return self._device.send("volume_ctrl_fm")
def get_channels(self):
"""Unknown."""
raise NotImplementedError()
# "method": "get_channels", "params": {"start": 0}}
return self._device.send("get_channels")
def add_channels(self):
"""Unknown."""
raise NotImplementedError()
return self._device.send("add_channels")
def remove_channels(self):
"""Unknown."""
raise NotImplementedError()
return self._device.send("remove_channels")
def get_default_music(self):
"""seems to timeout (w/o internet)"""
# params [0,1,2]
raise NotImplementedError()
return self._device.send("get_default_music")
@command()
def get_music_info(self):
"""Unknown."""
info = self._device.send("get_music_info")
click.echo("info: %s" % info)
free_space = self._device.send("get_music_free_space")
click.echo("free space: %s" % free_space)
@command()
def get_mute(self):
"""mute of what?"""
return self._device.send("get_mute")
def download_music(self):
"""Unknown"""
raise NotImplementedError()
return self._device.send("download_music")
def delete_music(self):
"""delete music"""
raise NotImplementedError()
return self._device.send("delete_music")
def download_user_music(self):
"""Unknown."""
raise NotImplementedError()
return self._device.send("download_user_music")
def get_download_progress(self):
"""progress for music downloads or updates?"""
# returns [':0']
raise NotImplementedError()
return self._device.send("get_download_progress")
@command()
def set_sound_playing(self):
"""stop playing?"""
return self._device.send("set_sound_playing", ["off"])
def set_default_music(self):
raise NotImplementedError()
# method":"set_default_music","params":[0,"2"]}
class GatewayLight(Device):
"""Light controls for the gateway."""
def __init__(self, parent) -> None:
self._device = parent
@command()
def get_night_light_rgb(self):
"""Unknown."""
# Returns 0 when light is off?"""
# looks like this is the same as get_rgb
# id': 65064, 'method': 'set_night_light_rgb', 'params': [419407616]}
# {'method': 'props', 'params': {'light': 'on', 'from.light': '4,,,'}, 'id': 88457} ?!
return self.send("get_night_light_rgb")
@command(click.argument("color_name", type=str))
def set_night_light_color(self, color_name):
"""Set night light color using color name (red, green, etc)."""
if color_name not in color_map.keys():
raise Exception(
"Cannot find {color} in {colors}".format(
color=color_name, colors=color_map.keys()
)
)
current_brightness = int_to_brightness(self.send("get_night_light_rgb")[0])
brightness_and_color = brightness_and_color_to_int(
current_brightness, color_map[color_name]
)
return self.send("set_night_light_rgb", [brightness_and_color])
@command(click.argument("color_name", type=str))
def set_color(self, color_name):
"""Set gateway lamp color using color name (red, green, etc)."""
if color_name not in color_map.keys():
raise Exception(
"Cannot find {color} in {colors}".format(
color=color_name, colors=color_map.keys()
)
)
current_brightness = int_to_brightness(self.send("get_rgb")[0])
brightness_and_color = brightness_and_color_to_int(
current_brightness, color_map[color_name]
)
return self.send("set_rgb", [brightness_and_color])
@command(click.argument("brightness", type=int))
def set_brightness(self, brightness):
"""Set gateway lamp brightness (0-100)."""
if 100 < brightness < 0:
raise Exception("Brightness must be between 0 and 100")
current_color = int_to_rgb(self.send("get_rgb")[0])
brightness_and_color = brightness_and_color_to_int(brightness, current_color)
return self.send("set_rgb", [brightness_and_color])
@command(click.argument("brightness", type=int))
def set_night_light_brightness(self, brightness):
"""Set night light brightness (0-100)."""
if 100 < brightness < 0:
raise Exception("Brightness must be between 0 and 100")
current_color = int_to_rgb(self.send("get_night_light_rgb")[0])
brightness_and_color = brightness_and_color_to_int(brightness, current_color)
print(brightness, current_color)
return self.send("set_night_light_rgb", [brightness_and_color])
@command(
click.argument("color_name", type=str), click.argument("brightness", type=int)
)
def set_light(self, color_name, brightness):
"""Set color (using color name) and brightness (0-100)."""
if 100 < brightness < 0:
raise Exception("Brightness must be between 0 and 100")
if color_name not in color_map.keys():
raise Exception(
"Cannot find {color} in {colors}".format(
color=color_name, colors=color_map.keys()
)
)
brightness_and_color = brightness_and_color_to_int(
brightness, color_map[color_name]
)
return self.send("set_rgb", [brightness_and_color])
class SubDevice:
def __init__(self, gw, sid, type_, _, __, ___):
self.gw = gw
self.sid = sid
self.type = DeviceType(type_)
def unpair(self):
return self.gw.send("remove_device", [self.sid])
def battery(self):
return self.gw.send("get_battery", [self.sid])[0]
def get_firmware_version(self) -> Optional[int]:
"""Returns firmware version"""
try:
return self.gw.send("get_device_prop", [self.sid, "fw_ver"])[0]
except Exception as ex:
_LOGGER.debug(
"Got an exception while fetching fw_ver: %s", ex, exc_info=True
)
return None
def __repr__(self):
return "" % (
self.type,
self.sid,
self.get_firmware_version(),
self.battery(),
)
class SensorHT(SubDevice):
accessor = "get_prop_sensor_ht"
properties = ["temperature", "humidity"]
class Plug(SubDevice):
accessor = "get_prop_plug"
properties = ["power", "neutral_0"]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585421538.0
python-miio-0.5.0.1/miio/heater.py 0000644 0001750 0001750 00000021226 00000000000 016346 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from typing import Any, Dict, Optional
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_HEATER_ZA1 = "zhimi.heater.za1"
MODEL_HEATER_MA1 = "zhimi.elecheater.ma1"
AVAILABLE_PROPERTIES_COMMON = [
"power",
"target_temperature",
"brightness",
"buzzer",
"child_lock",
"temperature",
"use_time",
]
AVAILABLE_PROPERTIES_ZA1 = ["poweroff_time", "relative_humidity"]
AVAILABLE_PROPERTIES_MA1 = ["poweroff_level", "poweroff_value"]
SUPPORTED_MODELS = {
MODEL_HEATER_ZA1: {
"available_properties": AVAILABLE_PROPERTIES_COMMON + AVAILABLE_PROPERTIES_ZA1,
"temperature_range": (16, 32),
"delay_off_range": (0, 9 * 3600),
},
MODEL_HEATER_MA1: {
"available_properties": AVAILABLE_PROPERTIES_COMMON + AVAILABLE_PROPERTIES_MA1,
"temperature_range": (20, 32),
"delay_off_range": (0, 5 * 3600),
},
}
class HeaterException(DeviceException):
pass
class Brightness(enum.Enum):
Bright = 0
Dim = 1
Off = 2
class HeaterStatus:
"""Container for status reports from the Smartmi Zhimi Heater."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Heater (zhimi.heater.za1):
{'power': 'off', 'target_temperature': 24, 'brightness': 1,
'buzzer': 'on', 'child_lock': 'off', 'temperature': 22.3,
'use_time': 43117, 'poweroff_time': 0, 'relative_humidity': 34}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""True if device is currently on."""
return self.power == "on"
@property
def humidity(self) -> Optional[int]:
"""Current humidity."""
if (
"relative_humidity" in self.data
and self.data["relative_humidity"] is not None
):
return self.data["relative_humidity"]
return None
@property
def temperature(self) -> float:
"""Current temperature."""
return self.data["temperature"]
@property
def target_temperature(self) -> int:
"""Target temperature."""
return self.data["target_temperature"]
@property
def brightness(self) -> Brightness:
"""Display brightness."""
return Brightness(self.data["brightness"])
@property
def buzzer(self) -> bool:
"""True if buzzer is turned on."""
return self.data["buzzer"] in ["on", 1, 2]
@property
def child_lock(self) -> bool:
"""True if child lock is on."""
return self.data["child_lock"] == "on"
@property
def use_time(self) -> int:
"""How long the device has been active in seconds."""
return self.data["use_time"]
@property
def delay_off_countdown(self) -> Optional[int]:
"""Countdown until turning off in seconds."""
if "poweroff_time" in self.data and self.data["poweroff_time"] is not None:
return self.data["poweroff_time"]
if "poweroff_level" in self.data and self.data["poweroff_level"] is not None:
return self.data["poweroff_level"]
return None
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.target_temperature,
self.temperature,
self.humidity,
self.brightness,
self.buzzer,
self.child_lock,
self.use_time,
self.delay_off_countdown,
)
)
return s
def __json__(self):
return self.data
class Heater(Device):
"""Main class representing the Smartmi Zhimi Heater."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_HEATER_ZA1,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in SUPPORTED_MODELS.keys():
self.model = model
else:
self.model = MODEL_HEATER_ZA1
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Target temperature: {result.target_temperature} °C\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"Display brightness: {result.brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Power-off time: {result.delay_off_countdown}\n",
)
)
def status(self) -> HeaterStatus:
"""Retrieve properties."""
properties = SUPPORTED_MODELS[self.model]["available_properties"]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props_per_request = 15
# The MA1, ZA1 is limited to a single property per request
if self.model in [MODEL_HEATER_MA1, MODEL_HEATER_ZA1]:
_props_per_request = 1
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:_props_per_request]))
_props[:] = _props[_props_per_request:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return HeaterStatus(dict(zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("temperature", type=int),
default_output=format_output("Setting target temperature to {temperature}"),
)
def set_target_temperature(self, temperature: int):
"""Set target temperature."""
min_temp, max_temp = SUPPORTED_MODELS[self.model]["temperature_range"]
if not min_temp <= temperature <= max_temp:
raise HeaterException("Invalid target temperature: %s" % temperature)
return self.send("set_target_temperature", [temperature])
@command(
click.argument("brightness", type=EnumType(Brightness, False)),
default_output=format_output("Setting display brightness to {brightness}"),
)
def set_brightness(self, brightness: Brightness):
"""Set display brightness."""
return self.send("set_brightness", [brightness.value])
@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer"
),
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if buzzer:
return self.send("set_buzzer", ["on"])
else:
return self.send("set_buzzer", ["off"])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])
@command(
click.argument("seconds", type=int),
default_output=format_output("Setting delayed turn off to {seconds} seconds"),
)
def delay_off(self, seconds: int):
"""Set delay off seconds."""
min_delay, max_delay = SUPPORTED_MODELS[self.model]["delay_off_range"]
if not min_delay <= seconds <= max_delay:
raise HeaterException("Invalid delay time: %s" % seconds)
if self.model == MODEL_HEATER_ZA1:
return self.send("set_poweroff_time", [seconds])
elif self.model == MODEL_HEATER_MA1:
return self.send("set_poweroff_level", [seconds // 3600])
return None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585489963.0
python-miio-0.5.0.1/miio/miioprotocol.py 0000644 0001750 0001750 00000020274 00000000000 017617 0 ustar 00tpr tpr 0000000 0000000 """miIO protocol implementation
This module contains the implementation of routines to send handshakes, send
commands and discover devices (MiIOProtocol).
"""
import binascii
import codecs
import datetime
import logging
import socket
from typing import Any, List
import construct
from .exceptions import DeviceError, DeviceException, RecoverableError
from .protocol import Message
_LOGGER = logging.getLogger(__name__)
class MiIOProtocol:
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
"""
Create a :class:`Device` instance.
:param ip: IP address or a hostname for the device
:param token: Token used for encryption
:param start_id: Running message id sent to the device
:param debug: Wanted debug level
"""
self.ip = ip
self.port = 54321
if token is None:
token = 32 * "0"
if token is not None:
self.token = bytes.fromhex(token)
self.debug = debug
self.lazy_discover = lazy_discover
self._timeout = 5
self._discovered = False
self._device_ts = None # type: datetime.datetime
self.__id = start_id
self._device_id = None
def send_handshake(self) -> Message:
"""Send a handshake to the device,
which can be used to the device type and serial.
The handshake must also be done regularly to enable communication
with the device.
:rtype: Message
:raises DeviceException: if the device could not be discovered."""
m = MiIOProtocol.discover(self.ip)
if m is not None:
self._device_id = m.header.value.device_id
self._device_ts = m.header.value.ts
self._discovered = True
if self.debug > 1:
_LOGGER.debug(m)
_LOGGER.debug(
"Discovered %s with ts: %s, token: %s",
binascii.hexlify(self._device_id).decode(),
self._device_ts,
codecs.encode(m.checksum, "hex"),
)
else:
_LOGGER.error("Unable to discover a device at address %s", self.ip)
raise DeviceException("Unable to discover the device %s" % self.ip)
return m
@staticmethod
def discover(addr: str = None) -> Any:
"""Scan for devices in the network.
This method is used to discover supported devices by sending a
handshake message to the broadcast address on port 54321.
If the target IP address is given, the handshake will be send as
an unicast packet.
:param str addr: Target IP address"""
timeout = 5
is_broadcast = addr is None
seen_addrs = [] # type: List[str]
if is_broadcast:
addr = ""
is_broadcast = True
_LOGGER.info("Sending discovery to %s with timeout of %ss..", addr, timeout)
# magic, length 32
helobytes = bytes.fromhex(
"21310020ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.settimeout(timeout)
s.sendto(helobytes, (addr, 54321))
while True:
try:
data, addr = s.recvfrom(1024)
m = Message.parse(data) # type: Message
_LOGGER.debug("Got a response: %s", m)
if not is_broadcast:
return m
if addr[0] not in seen_addrs:
_LOGGER.info(
" IP %s (ID: %s) - token: %s",
addr[0],
binascii.hexlify(m.header.value.device_id).decode(),
codecs.encode(m.checksum, "hex"),
)
seen_addrs.append(addr[0])
except socket.timeout:
if is_broadcast:
_LOGGER.info("Discovery done")
return # ignore timeouts on discover
except Exception as ex:
_LOGGER.warning("error while reading discover results: %s", ex)
break
def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
"""Build and send the given command.
Note that this will implicitly call :func:`send_handshake` to do a handshake,
and will re-try in case of errors while incrementing the `_id` by 100.
:param str command: Command to send
:param dict parameters: Parameters to send, or an empty list FIXME
:param retry_count: How many times to retry in case of failure
:raises DeviceException: if an error has occurred during communication."""
if not self.lazy_discover or not self._discovered:
self.send_handshake()
cmd = {"id": self._id, "method": command}
if parameters is not None:
cmd["params"] = parameters
else:
cmd["params"] = []
send_ts = self._device_ts + datetime.timedelta(seconds=1)
header = {
"length": 0,
"unknown": 0x00000000,
"device_id": self._device_id,
"ts": send_ts,
}
msg = {"data": {"value": cmd}, "header": {"value": header}, "checksum": 0}
m = Message.build(msg, token=self.token)
_LOGGER.debug("%s:%s >>: %s", self.ip, self.port, cmd)
if self.debug > 1:
_LOGGER.debug(
"send (timeout %s): %s",
self._timeout,
Message.parse(m, token=self.token),
)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(self._timeout)
try:
s.sendto(m, (self.ip, self.port))
except OSError as ex:
_LOGGER.error("failed to send msg: %s", ex)
raise DeviceException from ex
try:
data, addr = s.recvfrom(1024)
m = Message.parse(data, token=self.token)
self._device_ts = m.header.value.ts
if self.debug > 1:
_LOGGER.debug("recv from %s: %s", addr[0], m)
self.__id = m.data.value["id"]
_LOGGER.debug(
"%s:%s (ts: %s, id: %s) << %s",
self.ip,
self.port,
m.header.value.ts,
m.data.value["id"],
m.data.value,
)
if "error" in m.data.value:
error = m.data.value["error"]
if "code" in error and error["code"] == -30001:
raise RecoverableError(error)
raise DeviceError(error)
try:
return m.data.value["result"]
except KeyError:
return m.data.value
except construct.core.ChecksumError as ex:
raise DeviceException(
"Got checksum error which indicates use "
"of an invalid token. "
"Please check your token!"
) from ex
except OSError as ex:
if retry_count > 0:
_LOGGER.debug(
"Retrying with incremented id, retries left: %s", retry_count
)
self.__id += 100
self._discovered = False
return self.send(command, parameters, retry_count - 1)
_LOGGER.error("Got error when receiving: %s", ex)
raise DeviceException("No response from the device") from ex
except RecoverableError as ex:
if retry_count > 0:
_LOGGER.debug(
"Retrying to send failed command, retries left: %s", retry_count
)
return self.send(command, parameters, retry_count - 1)
_LOGGER.error("Got error when receiving: %s", ex)
raise DeviceException("Unable to recover failed command") from ex
@property
def _id(self) -> int:
"""Increment and return the sequence id."""
self.__id += 1
if self.__id >= 9999:
self.__id = 1
return self.__id
@property
def raw_id(self):
return self.__id
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584295509.0
python-miio-0.5.0.1/miio/miot_device.py 0000644 0001750 0001750 00000003204 00000000000 017361 0 ustar 00tpr tpr 0000000 0000000 import logging
from .device import Device
_LOGGER = logging.getLogger(__name__)
class MiotDevice(Device):
"""Main class representing a MIoT device."""
def __init__(
self,
mapping: dict,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
self.mapping = mapping
super().__init__(ip, token, start_id, debug, lazy_discover)
def get_properties(self) -> list:
"""Retrieve raw properties based on mapping."""
# We send property key in "did" because it's sent back via response and we can identify the property.
properties = [{"did": k, **v} for k, v in self.mapping.items()]
# A single request is limited to 16 properties. Therefore the
# properties are divided into multiple requests
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_properties", _props[:15]))
_props[:] = _props[15:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return values
def set_property(self, property_key: str, value):
"""Sets property value."""
return self.send(
"set_properties",
[{"did": property_key, **self.mapping[property_key], "value": value}],
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507037.0
python-miio-0.5.0.1/miio/philips_bulb.py 0000644 0001750 0001750 00000015101 00000000000 017545 0 ustar 00tpr tpr 0000000 0000000 import logging
from collections import defaultdict
from typing import Any, Dict, Optional
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_PHILIPS_LIGHT_BULB = "philips.light.bulb"
MODEL_PHILIPS_LIGHT_HBULB = "philips.light.hbulb"
AVAILABLE_PROPERTIES_COMMON = [
"power",
"dv",
]
AVAILABLE_PROPERTIES = {
MODEL_PHILIPS_LIGHT_HBULB: AVAILABLE_PROPERTIES_COMMON + ["bri"],
MODEL_PHILIPS_LIGHT_BULB: AVAILABLE_PROPERTIES_COMMON + ["bright", "cct", "snm"],
}
class PhilipsBulbException(DeviceException):
pass
class PhilipsBulbStatus:
"""Container for status reports from Xiaomi Philips LED Ceiling Lamp"""
def __init__(self, data: Dict[str, Any]) -> None:
# {'power': 'on', 'bright': 85, 'cct': 9, 'snm': 0, 'dv': 0}
self.data = data
@property
def power(self) -> str:
return self.data["power"]
@property
def is_on(self) -> bool:
return self.power == "on"
@property
def brightness(self) -> Optional[int]:
if "bright" in self.data:
return self.data["bright"]
if "bri" in self.data:
return self.data["bri"]
return None
@property
def color_temperature(self) -> Optional[int]:
if "cct" in self.data:
return self.data["cct"]
return None
@property
def scene(self) -> Optional[int]:
if "snm" in self.data:
return self.data["snm"]
return None
@property
def delay_off_countdown(self) -> int:
return self.data["dv"]
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.brightness,
self.delay_off_countdown,
self.color_temperature,
self.scene,
)
)
return s
def __json__(self):
return self.data
class PhilipsWhiteBulb(Device):
"""Main class representing Xiaomi Philips White LED Ball Lamp."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_PHILIPS_LIGHT_HBULB,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_PHILIPS_LIGHT_HBULB
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Brightness: {result.brightness}\n"
"Delayed turn off: {result.delay_off_countdown}\n"
"Color temperature: {result.color_temperature}\n"
"Scene: {result.scene}\n",
)
)
def status(self) -> PhilipsBulbStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return PhilipsBulbStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("level", type=int),
default_output=format_output("Setting brightness to {level}"),
)
def set_brightness(self, level: int):
"""Set brightness level."""
if level < 1 or level > 100:
raise PhilipsBulbException("Invalid brightness: %s" % level)
return self.send("set_bright", [level])
@command(
click.argument("seconds", type=int),
default_output=format_output("Setting delayed turn off to {seconds} seconds"),
)
def delay_off(self, seconds: int):
"""Set delay off seconds."""
if seconds < 1:
raise PhilipsBulbException(
"Invalid value for a delayed turn off: %s" % seconds
)
return self.send("delay_off", [seconds])
class PhilipsBulb(PhilipsWhiteBulb):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_PHILIPS_LIGHT_BULB,
) -> None:
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_PHILIPS_LIGHT_BULB
super().__init__(ip, token, start_id, debug, lazy_discover, self.model)
@command(
click.argument("level", type=int),
default_output=format_output("Setting color temperature to {level}"),
)
def set_color_temperature(self, level: int):
"""Set Correlated Color Temperature."""
if level < 1 or level > 100:
raise PhilipsBulbException("Invalid color temperature: %s" % level)
return self.send("set_cct", [level])
@command(
click.argument("brightness", type=int),
click.argument("cct", type=int),
default_output=format_output(
"Setting brightness to {brightness} and color temperature to {cct}"
),
)
def set_brightness_and_color_temperature(self, brightness: int, cct: int):
"""Set brightness level and the correlated color temperature."""
if brightness < 1 or brightness > 100:
raise PhilipsBulbException("Invalid brightness: %s" % brightness)
if cct < 1 or cct > 100:
raise PhilipsBulbException("Invalid color temperature: %s" % cct)
return self.send("set_bricct", [brightness, cct])
@command(
click.argument("number", type=int),
default_output=format_output("Setting fixed scene to {number}"),
)
def set_scene(self, number: int):
"""Set scene number."""
if number < 1 or number > 4:
raise PhilipsBulbException("Invalid fixed scene number: %s" % number)
return self.send("apply_fixed_scene", [number])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/philips_eyecare.py 0000644 0001750 0001750 00000017252 00000000000 020247 0 ustar 00tpr tpr 0000000 0000000 import logging
from collections import defaultdict
from typing import Any, Dict
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
class PhilipsEyecareException(DeviceException):
pass
class PhilipsEyecareStatus:
"""Container for status reports from Xiaomi Philips Eyecare Smart Lamp 2"""
def __init__(self, data: Dict[str, Any]) -> None:
# ['power': 'off', 'bright': 5, 'notifystatus': 'off',
# 'ambstatus': 'off', 'ambvalue': 41, 'eyecare': 'on',
# 'scene_num': 3, 'bls': 'on', 'dvalue': 0]
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""True if the device is turned on."""
return self.power == "on"
@property
def brightness(self) -> int:
"""Current brightness of the primary light."""
return self.data["bright"]
@property
def reminder(self) -> bool:
"""Indicates the eye fatigue notification is enabled or not."""
return self.data["notifystatus"] == "on"
@property
def ambient(self) -> bool:
"""True if the ambient light (second light source) is on."""
return self.data["ambstatus"] == "on"
@property
def ambient_brightness(self) -> int:
"""Brightness of the ambient light."""
return self.data["ambvalue"]
@property
def eyecare(self) -> bool:
"""True if the eyecare mode is on."""
return self.data["eyecare"] == "on"
@property
def scene(self) -> int:
"""Current fixed scene."""
return self.data["scene_num"]
@property
def smart_night_light(self) -> bool:
"""True if the smart night light mode is on."""
return self.data["bls"] == "on"
@property
def delay_off_countdown(self) -> int:
"""Countdown until turning off in minutes."""
return self.data["dvalue"]
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.brightness,
self.ambient,
self.ambient_brightness,
self.eyecare,
self.scene,
self.reminder,
self.smart_night_light,
self.delay_off_countdown,
)
)
return s
def __json__(self):
return self.data
class PhilipsEyecare(Device):
"""Main class representing Xiaomi Philips Eyecare Smart Lamp 2."""
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Brightness: {result.brightness}\n"
"Ambient light: {result.ambient}\n"
"Ambient light brightness: {result.ambient_brightness}\n"
"Eyecare mode: {result.eyecare}\n"
"Scene: {result.scence}\n"
"Eye fatigue reminder: {result.reminder}\n"
"Smart night light: {result.smart_night_light}\n"
"Delayed turn off: {result.delay_off_countdown}\n",
)
)
def status(self) -> PhilipsEyecareStatus:
"""Retrieve properties."""
properties = [
"power",
"bright",
"notifystatus",
"ambstatus",
"ambvalue",
"eyecare",
"scene_num",
"bls",
"dvalue",
]
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return PhilipsEyecareStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(default_output=format_output("Turning on eyecare mode"))
def eyecare_on(self):
"""Turn the eyecare mode on."""
return self.send("set_eyecare", ["on"])
@command(default_output=format_output("Turning off eyecare mode"))
def eyecare_off(self):
"""Turn the eyecare mode off."""
return self.send("set_eyecare", ["off"])
@command(
click.argument("level", type=int),
default_output=format_output("Setting brightness to {level}"),
)
def set_brightness(self, level: int):
"""Set brightness level of the primary light."""
if level < 1 or level > 100:
raise PhilipsEyecareException("Invalid brightness: %s" % level)
return self.send("set_bright", [level])
@command(
click.argument("number", type=int),
default_output=format_output("Setting fixed scene to {number}"),
)
def set_scene(self, number: int):
"""Set one of the fixed eyecare user scenes."""
if number < 1 or number > 4:
raise PhilipsEyecareException("Invalid fixed scene number: %s" % number)
return self.send("set_user_scene", [number])
@command(
click.argument("minutes", type=int),
default_output=format_output("Setting delayed turn off to {minutes} minutes"),
)
def delay_off(self, minutes: int):
"""Set delay off minutes."""
if minutes < 0:
raise PhilipsEyecareException(
"Invalid value for a delayed turn off: %s" % minutes
)
return self.send("delay_off", [minutes])
@command(default_output=format_output("Turning on smart night light"))
def smart_night_light_on(self):
"""Turn the smart night light mode on."""
return self.send("enable_bl", ["on"])
@command(default_output=format_output("Turning off smart night light"))
def smart_night_light_off(self):
"""Turn the smart night light mode off."""
return self.send("enable_bl", ["off"])
@command(default_output=format_output("Turning on eye fatigue reminder"))
def reminder_on(self):
"""Enable the eye fatigue reminder / notification."""
return self.send("set_notifyuser", ["on"])
@command(default_output=format_output("Turning off eye fatigue reminder"))
def reminder_off(self):
"""Disable the eye fatigue reminder / notification."""
return self.send("set_notifyuser", ["off"])
@command(default_output=format_output("Turning on ambient light"))
def ambient_on(self):
"""Turn the ambient light on."""
return self.send("enable_amb", ["on"])
@command(default_output=format_output("Turning off ambient light"))
def ambient_off(self):
"""Turn the ambient light off."""
return self.send("enable_amb", ["off"])
@command(
click.argument("level", type=int),
default_output=format_output("Setting brightness to {level}"),
)
def set_ambient_brightness(self, level: int):
"""Set the brightness of the ambient light."""
if level < 1 or level > 100:
raise PhilipsEyecareException("Invalid ambient brightness: %s" % level)
return self.send("set_amb_bright", [level])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584291922.0
python-miio-0.5.0.1/miio/philips_eyecare_cli.py 0000644 0001750 0001750 00000012167 00000000000 021076 0 ustar 00tpr tpr 0000000 0000000 import logging
import sys
import click
import miio # noqa: E402
from miio.click_common import ExceptionHandlerGroup, validate_ip, validate_token
from miio.miioprotocol import MiIOProtocol
_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.PhilipsEyecare)
def validate_brightness(ctx, param, value):
value = int(value)
if value < 1 or value > 100:
raise click.BadParameter("Should be a positive int between 1-100.")
return value
def validate_minutes(ctx, param, value):
value = int(value)
if value < 0 or value > 60:
raise click.BadParameter("Should be a positive int between 1-60.")
return value
def validate_scene(ctx, param, value):
value = int(value)
if value < 1 or value > 3:
raise click.BadParameter("Should be a positive int between 1-3.")
return value
@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup)
@click.option("--ip", envvar="DEVICE_IP", callback=validate_ip)
@click.option("--token", envvar="DEVICE_TOKEN", callback=validate_token)
@click.option("-d", "--debug", default=False, count=True)
@click.pass_context
def cli(ctx, ip: str, token: str, debug: int):
"""A tool to command Xiaomi Philips Eyecare Smart Lamp 2."""
if debug:
logging.basicConfig(level=logging.DEBUG)
_LOGGER.info("Debug mode active")
else:
logging.basicConfig(level=logging.INFO)
# if we are scanning, we do not try to connect.
if ctx.invoked_subcommand == "discover":
return
if ip is None or token is None:
click.echo("You have to give ip and token!")
sys.exit(-1)
dev = miio.PhilipsEyecare(ip, token, debug)
_LOGGER.debug("Connecting to %s with token %s", ip, token)
ctx.obj = dev
if ctx.invoked_subcommand is None:
ctx.invoke(status)
@cli.command()
def discover():
"""Search for plugs in the network."""
MiIOProtocol.discover()
@cli.command()
@pass_dev
def status(dev: miio.PhilipsEyecare):
"""Returns the state information."""
res = dev.status()
if not res:
return # bail out
click.echo(click.style("Power: %s" % res.power, bold=True))
click.echo("Brightness: %s" % res.brightness)
click.echo("Eye Fatigue Reminder: %s" % res.reminder)
click.echo("Ambient Light: %s" % res.ambient)
click.echo("Ambient Light Brightness: %s" % res.ambient_brightness)
click.echo("Eyecare Mode: %s" % res.eyecare)
click.echo("Eyecare Scene: %s" % res.scene)
click.echo("Night Light: %s " % res.smart_night_light)
click.echo(
"Countdown of the delayed turn off: %s minutes" % res.delay_off_countdown
)
@cli.command()
@pass_dev
def on(dev: miio.PhilipsEyecare):
"""Power on."""
click.echo("Power on: %s" % dev.on())
@cli.command()
@pass_dev
def off(dev: miio.PhilipsEyecare):
"""Power off."""
click.echo("Power off: %s" % dev.off())
@cli.command()
@pass_dev
def eyecare_on(dev: miio.PhilipsEyecare):
"""Turn eyecare on."""
click.echo("Eyecare on: %s" % dev.eyecare_on())
@cli.command()
@pass_dev
def eyecare_off(dev: miio.PhilipsEyecare):
"""Turn eyecare off."""
click.echo("Eyecare off: %s" % dev.eyecare_off())
@cli.command()
@click.argument("level", callback=validate_brightness, required=True)
@pass_dev
def set_brightness(dev: miio.PhilipsEyecare, level):
"""Set brightness level."""
click.echo("Brightness: %s" % dev.set_brightness(level))
@cli.command()
@click.argument("scene", callback=validate_scene, required=True)
@pass_dev
def set_scene(dev: miio.PhilipsEyecare, scene):
"""Set eyecare scene number."""
click.echo("Eyecare Scene: %s" % dev.set_scene(scene))
@cli.command()
@click.argument("minutes", callback=validate_minutes, required=True)
@pass_dev
def delay_off(dev: miio.PhilipsEyecare, minutes):
"""Set delay off in minutes."""
click.echo("Delay off: %s" % dev.delay_off(minutes))
@cli.command()
@pass_dev
def bl_on(dev: miio.PhilipsEyecare):
"""Night Light on."""
click.echo("Night Light On: %s" % dev.smart_night_light_on())
@cli.command()
@pass_dev
def bl_off(dev: miio.PhilipsEyecare):
"""Night Light off."""
click.echo("Night Light off: %s" % dev.smart_night_light_off())
@cli.command()
@pass_dev
def notify_on(dev: miio.PhilipsEyecare):
"""Eye Fatigue Reminder On."""
click.echo("Eye Fatigue Reminder On: %s" % dev.reminder_on())
@cli.command()
@pass_dev
def notify_off(dev: miio.PhilipsEyecare):
"""Eye Fatigue Reminder off."""
click.echo("Eye Fatigue Reminder Off: %s" % dev.reminder_off())
@cli.command()
@pass_dev
def ambient_on(dev: miio.PhilipsEyecare):
"""Ambient Light on."""
click.echo("Ambient Light On: %s" % dev.ambient_on())
@cli.command()
@pass_dev
def ambient_off(dev: miio.PhilipsEyecare):
"""Ambient Light off."""
click.echo("Ambient Light Off: %s" % dev.ambient_off())
@cli.command()
@click.argument("level", callback=validate_brightness, required=True)
@pass_dev
def set_ambient_brightness(dev: miio.PhilipsEyecare, level):
"""Set Ambient Light brightness level."""
click.echo("Ambient Light Brightness: %s" % dev.set_ambient_brightness(level))
if __name__ == "__main__":
cli()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/philips_moonlight.py 0000644 0001750 0001750 00000017403 00000000000 020630 0 ustar 00tpr tpr 0000000 0000000 import logging
from collections import defaultdict
from typing import Any, Dict, Tuple
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
from .utils import int_to_rgb
_LOGGER = logging.getLogger(__name__)
class PhilipsMoonlightException(DeviceException):
pass
class PhilipsMoonlightStatus:
"""Container for status reports from Xiaomi Philips Zhirui Bedside Lamp."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Moonlight (philips.light.moonlight):
{'pow': 'off', 'sta': 0, 'bri': 1, 'rgb': 16741971, 'cct': 1, 'snm': 0, 'spr': 0,
'spt': 15, 'wke': 0, 'bl': 1, 'ms': 1, 'mb': 1, 'wkp': [0, 24, 0]}
"""
self.data = data
@property
def power(self) -> str:
return self.data["pow"]
@property
def is_on(self) -> bool:
return self.power == "on"
@property
def brightness(self) -> int:
return self.data["bri"]
@property
def color_temperature(self) -> int:
return self.data["cct"]
@property
def rgb(self) -> Tuple[int, int, int]:
"""Return color in RGB."""
return int_to_rgb(int(self.data["rgb"]))
@property
def scene(self) -> int:
return self.data["snm"]
@property
def sleep_assistant(self) -> int:
"""
Example values:
0: Unknown
1: Unknown
2: Sleep assistant enabled
3: Awake
"""
return self.data["sta"]
@property
def sleep_off_time(self) -> int:
return self.data["spr"]
@property
def total_assistant_sleep_time(self) -> int:
return self.data["spt"]
@property
def brand_sleep(self) -> bool:
# sp_sleep_open?
return self.data["ms"] == 1
@property
def brand(self) -> bool:
# sp_xm_bracelet?
return self.data["mb"] == 1
@property
def wake_up_time(self) -> [int, int, int]:
# Example: [weekdays?, hour, minute]
return self.data["wkp"]
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.brightness,
self.color_temperature,
self.rgb,
self.scene,
)
)
return s
def __json__(self):
return self.data
class PhilipsMoonlight(Device):
"""Main class representing Xiaomi Philips Zhirui Bedside Lamp.
Not yet implemented features/methods:
add_mb # Add miband
get_band_period # Bracelet work time
get_mb_rssi # Miband RSSI
get_mb_mac # Miband MAC address
enable_mibs
set_band_period
miIO.bleStartSearchBand
miIO.bleGetNearbyBandList
enable_sub_voice # Sub voice control?
enable_voice # Voice control
skip_breath
set_sleep_time
set_wakeup_time
en_sleep
en_wakeup
go_night # Night light / read mode
get_wakeup_time
enable_bl # Night light
"""
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Brightness: {result.brightness}\n"
"Color temperature: {result.color_temperature}\n"
"RGB: {result.rgb}\n"
"Scene: {result.scene}\n",
)
)
def status(self) -> PhilipsMoonlightStatus:
"""Retrieve properties."""
properties = [
"pow",
"sta",
"bri",
"rgb",
"cct",
"snm",
"spr",
"spt",
"wke",
"bl",
"ms",
"mb",
"wkp",
]
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return PhilipsMoonlightStatus(
defaultdict(lambda: None, zip(properties, values))
)
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("rgb", default=[255] * 3, type=click.Tuple([int, int, int])),
default_output=format_output("Setting color to {rgb}"),
)
def set_rgb(self, rgb: Tuple[int, int, int]):
"""Set color in RGB."""
for color in rgb:
if color < 0 or color > 255:
raise PhilipsMoonlightException("Invalid color: %s" % color)
return self.send("set_rgb", [*rgb])
@command(
click.argument("level", type=int),
default_output=format_output("Setting brightness to {level}"),
)
def set_brightness(self, level: int):
"""Set brightness level."""
if level < 1 or level > 100:
raise PhilipsMoonlightException("Invalid brightness: %s" % level)
return self.send("set_bright", [level])
@command(
click.argument("level", type=int),
default_output=format_output("Setting color temperature to {level}"),
)
def set_color_temperature(self, level: int):
"""Set Correlated Color Temperature."""
if level < 1 or level > 100:
raise PhilipsMoonlightException("Invalid color temperature: %s" % level)
return self.send("set_cct", [level])
@command(
click.argument("brightness", type=int),
click.argument("cct", type=int),
default_output=format_output(
"Setting brightness to {brightness} and color temperature to {cct}"
),
)
def set_brightness_and_color_temperature(self, brightness: int, cct: int):
"""Set brightness level and the correlated color temperature."""
if brightness < 1 or brightness > 100:
raise PhilipsMoonlightException("Invalid brightness: %s" % brightness)
if cct < 1 or cct > 100:
raise PhilipsMoonlightException("Invalid color temperature: %s" % cct)
return self.send("set_bricct", [brightness, cct])
@command(
click.argument("brightness", type=int),
click.argument("rgb", default=[255] * 3, type=click.Tuple([int, int, int])),
default_output=format_output(
"Setting brightness to {brightness} and color to {rgb}"
),
)
def set_brightness_and_rgb(self, brightness: int, rgb: Tuple[int, int, int]):
"""Set brightness level and the color."""
if brightness < 1 or brightness > 100:
raise PhilipsMoonlightException("Invalid brightness: %s" % brightness)
for color in rgb:
if color < 0 or color > 255:
raise PhilipsMoonlightException("Invalid color: %s" % color)
return self.send("set_brirgb", [*rgb, brightness])
@command(
click.argument("number", type=int),
default_output=format_output("Setting fixed scene to {number}"),
)
def set_scene(self, number: int):
"""Set scene number."""
if number < 1 or number > 6:
raise PhilipsMoonlightException("Invalid fixed scene number: %s" % number)
if number == 6:
return self.send("go_night")
return self.send("apply_fixed_scene", [number])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507037.0
python-miio-0.5.0.1/miio/philips_rwread.py 0000644 0001750 0001750 00000015563 00000000000 020121 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from collections import defaultdict
from typing import Any, Dict
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_PHILIPS_LIGHT_RWREAD = "philips.light.rwread"
AVAILABLE_PROPERTIES = {
MODEL_PHILIPS_LIGHT_RWREAD: ["power", "bright", "dv", "snm", "flm", "chl", "flmv"],
}
class PhilipsRwreadException(DeviceException):
pass
class MotionDetectionSensitivity(enum.Enum):
Low = 1
Medium = 2
High = 3
class PhilipsRwreadStatus:
"""Container for status reports from Xiaomi Philips RW Read"""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a RW Read (philips.light.rwread):
{'power': 'on', 'bright': 53, 'dv': 0, 'snm': 1,
'flm': 0, 'chl': 0, 'flmv': 0}
"""
self.data = data
@property
def power(self) -> str:
"""Power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""True if the device is turned on."""
return self.power == "on"
@property
def brightness(self) -> int:
"""Current brightness."""
return self.data["bright"]
@property
def delay_off_countdown(self) -> int:
"""Countdown until turning off in seconds."""
return self.data["dv"]
@property
def scene(self) -> int:
"""Current fixed scene."""
return self.data["snm"]
@property
def motion_detection(self) -> bool:
"""True if motion detection is enabled."""
return self.data["flm"] == 1
@property
def motion_detection_sensitivity(self) -> MotionDetectionSensitivity:
"""The sensitivity of the motion detection."""
return MotionDetectionSensitivity(self.data["flmv"])
@property
def child_lock(self) -> bool:
"""True if child lock is enabled."""
return self.data["chl"] == 1
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.brightness,
self.delay_off_countdown,
self.scene,
self.motion_detection,
self.motion_detection_sensitivity,
self.child_lock,
)
)
return s
def __json__(self):
return self.data
class PhilipsRwread(Device):
"""Main class representing Xiaomi Philips RW Read."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_PHILIPS_LIGHT_RWREAD,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_PHILIPS_LIGHT_RWREAD
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Brightness: {result.brightness}\n"
"Delayed turn off: {result.delay_off_countdown}\n"
"Scene: {result.scene}\n"
"Motion detection: {result.motion_detection}\n"
"Motion detection sensitivity: {result.motion_detection_sensitivity}\n"
"Child lock: {result.child_lock}\n",
)
)
def status(self) -> PhilipsRwreadStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return PhilipsRwreadStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("level", type=int),
default_output=format_output("Setting brightness to {level}"),
)
def set_brightness(self, level: int):
"""Set brightness level of the primary light."""
if level < 1 or level > 100:
raise PhilipsRwreadException("Invalid brightness: %s" % level)
return self.send("set_bright", [level])
@command(
click.argument("number", type=int),
default_output=format_output("Setting fixed scene to {number}"),
)
def set_scene(self, number: int):
"""Set one of the fixed eyecare user scenes."""
if number < 1 or number > 4:
raise PhilipsRwreadException("Invalid fixed scene number: %s" % number)
return self.send("apply_fixed_scene", [number])
@command(
click.argument("seconds", type=int),
default_output=format_output("Setting delayed turn off to {seconds} seconds"),
)
def delay_off(self, seconds: int):
"""Set delay off in seconds."""
if seconds < 0:
raise PhilipsRwreadException(
"Invalid value for a delayed turn off: %s" % seconds
)
return self.send("delay_off", [seconds])
@command(
click.argument("motion_detection", type=bool),
default_output=format_output(
lambda motion_detection: "Turning on motion detection"
if motion_detection
else "Turning off motion detection"
),
)
def set_motion_detection(self, motion_detection: bool):
"""Set motion detection on/off."""
return self.send("enable_flm", [int(motion_detection)])
@command(
click.argument("sensitivity", type=EnumType(MotionDetectionSensitivity, False)),
default_output=format_output(
"Setting motion detection sensitivity to {sensitivity}"
),
)
def set_motion_detection_sensitivity(self, sensitivity: MotionDetectionSensitivity):
"""Set motion detection sensitivity."""
return self.send("set_flmvalue", [sensitivity.value])
@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
return self.send("enable_chl", [int(lock)])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/plug_cli.py 0000644 0001750 0001750 00000004506 00000000000 016676 0 ustar 00tpr tpr 0000000 0000000 import ast
import logging
import sys
from typing import Any # noqa: F401
import click
import miio # noqa: E402
from miio.click_common import ExceptionHandlerGroup, validate_ip, validate_token
from miio.miioprotocol import MiIOProtocol
_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.ChuangmiPlug)
@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup)
@click.option("--ip", envvar="DEVICE_IP", callback=validate_ip)
@click.option("--token", envvar="DEVICE_TOKEN", callback=validate_token)
@click.option("-d", "--debug", default=False, count=True)
@click.pass_context
def cli(ctx, ip: str, token: str, debug: int):
"""A tool to command Xiaomi Smart Plug."""
if debug:
logging.basicConfig(level=logging.DEBUG)
_LOGGER.info("Debug mode active")
else:
logging.basicConfig(level=logging.INFO)
# if we are scanning, we do not try to connect.
if ctx.invoked_subcommand == "discover":
return
if ip is None or token is None:
click.echo("You have to give ip and token!")
sys.exit(-1)
dev = miio.ChuangmiPlug(ip, token, debug)
_LOGGER.debug("Connecting to %s with token %s", ip, token)
ctx.obj = dev
if ctx.invoked_subcommand is None:
ctx.invoke(status)
@cli.command()
def discover():
"""Search for plugs in the network."""
MiIOProtocol.discover()
@cli.command()
@pass_dev
def status(dev: miio.ChuangmiPlug):
"""Returns the state information."""
res = dev.status()
if not res:
return # bail out
click.echo(click.style("Power: %s" % res.power, bold=True))
click.echo("Temperature: %s" % res.temperature)
@cli.command()
@pass_dev
def on(dev: miio.ChuangmiPlug):
"""Power on."""
click.echo("Power on: %s" % dev.on())
@cli.command()
@pass_dev
def off(dev: miio.ChuangmiPlug):
"""Power off."""
click.echo("Power off: %s" % dev.off())
@cli.command()
@click.argument("cmd", required=True)
@click.argument("parameters", required=False)
@pass_dev
def raw_command(dev: miio.ChuangmiPlug, cmd, parameters):
"""Run a raw command."""
params = [] # type: Any
if parameters:
params = ast.literal_eval(parameters)
click.echo("Sending cmd %s with params %s" % (cmd, params))
click.echo(dev.raw_command(cmd, params))
if __name__ == "__main__":
cli()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/powerstrip.py 0000644 0001750 0001750 00000017576 00000000000 017331 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from collections import defaultdict
from typing import Any, Dict, Optional
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
MODEL_POWER_STRIP_V1 = "qmi.powerstrip.v1"
MODEL_POWER_STRIP_V2 = "zimi.powerstrip.v2"
AVAILABLE_PROPERTIES = {
MODEL_POWER_STRIP_V1: [
"power",
"temperature",
"current",
"mode",
"power_consume_rate",
"voltage",
"power_factor",
"elec_leakage",
],
MODEL_POWER_STRIP_V2: [
"power",
"temperature",
"current",
"mode",
"power_consume_rate",
"wifi_led",
"power_price",
],
}
class PowerStripException(DeviceException):
pass
class PowerMode(enum.Enum):
Eco = "green"
Normal = "normal"
class PowerStripStatus:
"""Container for status reports from the power strip."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Supported device models: qmi.powerstrip.v1, zimi.powerstrip.v2
Response of a Power Strip 2 (zimi.powerstrip.v2):
{'power','on', 'temperature': 48.7, 'current': 0.05, 'mode': None,
'power_consume_rate': 4.09, 'wifi_led': 'on', 'power_price': 49}
"""
self.data = data
@property
def power(self) -> str:
"""Current power state."""
return self.data["power"]
@property
def is_on(self) -> bool:
"""True if the device is turned on."""
return self.power == "on"
@property
def temperature(self) -> float:
"""Current temperature."""
return self.data["temperature"]
@property
def current(self) -> Optional[float]:
"""Current, if available. Meaning and voltage reference unknown."""
if self.data["current"] is not None:
return self.data["current"]
return None
@property
def load_power(self) -> Optional[float]:
"""Current power load, if available."""
if self.data["power_consume_rate"] is not None:
return self.data["power_consume_rate"]
return None
@property
def mode(self) -> Optional[PowerMode]:
"""Current operation mode, can be either green or normal."""
if self.data["mode"] is not None:
return PowerMode(self.data["mode"])
return None
@property
def wifi_led(self) -> Optional[bool]:
"""True if the wifi led is turned on."""
if "wifi_led" in self.data and self.data["wifi_led"] is not None:
return self.data["wifi_led"] == "on"
return None
@property
def power_price(self) -> Optional[int]:
"""The stored power price, if available."""
if "power_price" in self.data and self.data["power_price"] is not None:
return self.data["power_price"]
return None
@property
def leakage_current(self) -> Optional[int]:
"""The leakage current, if available."""
if "elec_leakage" in self.data and self.data["elec_leakage"] is not None:
return self.data["elec_leakage"]
return None
@property
def voltage(self) -> Optional[float]:
"""The voltage, if available."""
if "voltage" in self.data and self.data["voltage"] is not None:
return self.data["voltage"] / 100.0
return None
@property
def power_factor(self) -> Optional[float]:
"""The power factor, if available."""
if "power_factor" in self.data and self.data["power_factor"] is not None:
return self.data["power_factor"]
return None
def __repr__(self) -> str:
s = (
""
% (
self.power,
self.temperature,
self.voltage,
self.current,
self.load_power,
self.power_factor,
self.power_price,
self.leakage_current,
self.mode,
self.wifi_led,
)
)
return s
def __json__(self):
return self.data
class PowerStrip(Device):
"""Main class representing the smart power strip."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_POWER_STRIP_V1,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_POWER_STRIP_V1
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Temperature: {result.temperature} °C\n"
"Voltage: {result.voltage} V\n"
"Current: {result.current} A\n"
"Load power: {result.load_power} W\n"
"Power factor: {result.power_factor}\n"
"Power price: {result.power_price}\n"
"Leakage current: {result.leakage_current} A\n"
"Mode: {result.mode}\n"
"WiFi LED: {result.wifi_led}\n",
)
)
def status(self) -> PowerStripStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return PowerStripStatus(defaultdict(lambda: None, zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
@command(
click.argument("mode", type=EnumType(PowerMode, False)),
default_output=format_output("Setting mode to {mode}"),
)
def set_power_mode(self, mode: PowerMode):
"""Set the power mode."""
# green, normal
return self.send("set_power_mode", [mode.value])
@command(
click.argument("led", type=bool),
default_output=format_output(
lambda led: "Turning on WiFi LED" if led else "Turning off WiFi LED"
),
)
def set_wifi_led(self, led: bool):
"""Set the wifi led on/off."""
if led:
return self.send("set_wifi_led", ["on"])
else:
return self.send("set_wifi_led", ["off"])
@command(
click.argument("price", type=int),
default_output=format_output("Setting power price to {price}"),
)
def set_power_price(self, price: int):
"""Set the power price."""
if price < 0 or price > 999:
raise PowerStripException("Invalid power price: %s" % price)
return self.send("set_power_price", [price])
@command(
click.argument("power", type=bool),
default_output=format_output(
lambda led: "Turning on real-time power measurement"
if led
else "Turning off real-time power measurement"
),
)
def set_realtime_power(self, power: bool):
"""Set the realtime power on/off."""
if power:
return self.send("set_rt_power", [1])
else:
return self.send("set_rt_power", [0])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/protocol.py 0000644 0001750 0001750 00000016240 00000000000 016737 0 ustar 00tpr tpr 0000000 0000000 """miIO protocol implementation
This module contains the implementation of the routines to encrypt and decrypt
miIO payloads with a device-specific token.
The payloads to be encrypted (to be passed to a device) are expected to be
JSON objects, the same applies for decryption where they are converted
automatically to JSON objects.
If the decryption fails, raw bytes as returned by the device are returned.
An usage example can be seen in the source of :func:`miio.Device.send`.
If the decryption fails, raw bytes as returned by the device are returned.
"""
import calendar
import datetime
import hashlib
import json
import logging
from typing import Any, Dict, Tuple
from construct import (
Adapter,
Bytes,
Checksum,
Const,
Default,
GreedyBytes,
Hex,
IfThenElse,
Int16ub,
Int32ub,
Pointer,
RawCopy,
Rebuild,
Struct,
)
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
_LOGGER = logging.getLogger(__name__)
class Utils:
""" This class is adapted from the original xpn.py code by gst666 """
@staticmethod
def verify_token(token: bytes):
"""Checks if the given token is of correct type and length."""
if not isinstance(token, bytes):
raise TypeError("Token must be bytes")
if len(token) != 16:
raise ValueError("Wrong token length")
@staticmethod
def md5(data: bytes) -> bytes:
"""Calculates a md5 hashsum for the given bytes object."""
checksum = hashlib.md5()
checksum.update(data)
return checksum.digest()
@staticmethod
def key_iv(token: bytes) -> Tuple[bytes, bytes]:
"""Generate an IV used for encryption based on given token."""
key = Utils.md5(token)
iv = Utils.md5(key + token)
return key, iv
@staticmethod
def encrypt(plaintext: bytes, token: bytes) -> bytes:
"""Encrypt plaintext with a given token.
:param bytes plaintext: Plaintext (json) to encrypt
:param bytes token: Token to use
:return: Encrypted bytes"""
if not isinstance(plaintext, bytes):
raise TypeError("plaintext requires bytes")
Utils.verify_token(token)
key, iv = Utils.key_iv(token)
padder = padding.PKCS7(128).padder()
padded_plaintext = padder.update(plaintext) + padder.finalize()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
return encryptor.update(padded_plaintext) + encryptor.finalize()
@staticmethod
def decrypt(ciphertext: bytes, token: bytes) -> bytes:
"""Decrypt ciphertext with a given token.
:param bytes ciphertext: Ciphertext to decrypt
:param bytes token: Token to use
:return: Decrypted bytes object"""
if not isinstance(ciphertext, bytes):
raise TypeError("ciphertext requires bytes")
Utils.verify_token(token)
key, iv = Utils.key_iv(token)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
unpadded_plaintext = unpadder.update(padded_plaintext)
unpadded_plaintext += unpadder.finalize()
return unpadded_plaintext
@staticmethod
def checksum_field_bytes(ctx: Dict[str, Any]) -> bytearray:
"""Gather bytes for checksum calculation"""
x = bytearray(ctx["header"].data)
x += ctx["_"]["token"]
if "data" in ctx:
x += ctx["data"].data
# print("DATA: %s" % ctx["data"])
return x
@staticmethod
def get_length(x) -> int:
"""Return total packet length."""
datalen = x._.data.length # type: int
return datalen + 32
@staticmethod
def is_hello(x) -> bool:
"""Return if packet is a hello packet."""
# not very nice, but we know that hellos are 32b of length
if "length" in x:
val = x["length"]
else:
val = x.header.value["length"]
return bool(val == 32)
class TimeAdapter(Adapter):
"""Adapter for timestamp conversion."""
def _encode(self, obj, context, path):
return calendar.timegm(obj.timetuple())
def _decode(self, obj, context, path):
return datetime.datetime.utcfromtimestamp(obj)
class EncryptionAdapter(Adapter):
"""Adapter to handle communication encryption."""
def _encode(self, obj, context, path):
"""Encrypt the given payload with the token stored in the context.
:param obj: JSON object to encrypt"""
# pp(context)
return Utils.encrypt(
json.dumps(obj).encode("utf-8") + b"\x00", context["_"]["token"]
)
def _decode(self, obj, context, path):
"""Decrypts the given payload with the token stored in the context.
:return str: JSON object"""
try:
# pp(context)
decrypted = Utils.decrypt(obj, context["_"]["token"])
decrypted = decrypted.rstrip(b"\x00")
except Exception:
_LOGGER.debug("Unable to decrypt, returning raw bytes: %s", obj)
return obj
# list of adaption functions for malformed json payload (quirks)
decrypted_quirks = [
# try without modifications first
lambda decrypted_bytes: decrypted_bytes,
# powerstrip returns malformed JSON if the device is not
# connected to the cloud, so we try to fix it here carefully.
lambda decrypted_bytes: decrypted_bytes.replace(
b',,"otu_stat"', b',"otu_stat"'
),
# xiaomi cloud returns malformed json when answering _sync.batch_gen_room_up_url
# command so try to sanitize it
lambda decrypted_bytes: decrypted_bytes[: decrypted_bytes.rfind(b"\x00")]
if b"\x00" in decrypted_bytes
else decrypted_bytes,
]
for i, quirk in enumerate(decrypted_quirks):
decoded = quirk(decrypted).decode("utf-8")
try:
return json.loads(decoded)
except Exception as ex:
# log the error when decrypted bytes couldn't be loaded
# after trying all quirk adaptions
if i == len(decrypted_quirks) - 1:
_LOGGER.error("unable to parse json '%s': %s", decoded, ex)
return None
Message = Struct(
# for building we need data before anything else.
"data" / Pointer(32, RawCopy(EncryptionAdapter(GreedyBytes))),
"header"
/ RawCopy(
Struct(
Const(0x2131, Int16ub),
"length" / Rebuild(Int16ub, Utils.get_length),
"unknown" / Default(Int32ub, 0x00000000),
"device_id" / Hex(Bytes(4)),
"ts" / TimeAdapter(Default(Int32ub, datetime.datetime.utcnow())),
)
),
"checksum"
/ IfThenElse(
Utils.is_hello,
Bytes(16),
Checksum(Bytes(16), Utils.md5, Utils.checksum_field_bytes),
),
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/pwzn_relay.py 0000644 0001750 0001750 00000011577 00000000000 017300 0 ustar 00tpr tpr 0000000 0000000 import logging
from collections import defaultdict
from typing import Any, Dict
import click
from .click_common import command, format_output
from .device import Device
_LOGGER = logging.getLogger(__name__)
MODEL_PWZN_RELAY_APPLE = "pwzn.relay.apple"
MODEL_PWZN_RELAY_BANANA = "pwzn.relay.banana"
AVAILABLE_PROPERTIES = {
MODEL_PWZN_RELAY_APPLE: [
"relay_status",
"on_count",
"name0",
"name1",
"name2",
"name3",
"name4",
"name5",
"name6",
"name7",
"name8",
"name9",
"name10",
"name11",
"name12",
"name13",
"name14",
"name15",
],
MODEL_PWZN_RELAY_BANANA: [
"relay_status",
"on_count",
"name0",
"name1",
"name2",
"name3",
"name4",
"name5",
"name6",
"name7",
"name8",
"name9",
"name10",
"name11",
"name12",
"name13",
"name14",
"name15",
],
}
class PwznRelayStatus:
"""Container for status reports from the plug."""
def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a PWZN Relay Apple (pwzn.relay.apple)
{ 'relay_status': 9, 'on_count': 2, 'name0': 'channel1', 'name1': '',
'name2': '', 'name3': '', 'name4': '', 'name5': '', 'name6': '',
'name7': '', 'name8': '', 'name9': '', 'name10': '', 'name11': '',
'name12': '', 'name13': '', 'name14': '', 'name15': '' }
"""
self.data = data
@property
def relay_state(self) -> int:
"""Current relay state."""
if "relay_status" in self.data:
return self.data["relay_status"]
@property
def relay_names(self) -> Dict[int, str]:
def _extract_index_from_key(name) -> int:
"""extract the index from the variable"""
return int(name[4:])
return {
_extract_index_from_key(name): value
for name, value in self.data.items()
if name.startswith("name")
}
@property
def on_count(self) -> int:
"""Number of on relay."""
if "on_count" in self.data:
return self.data["on_count"]
def __repr__(self) -> str:
s = (
"" % (self.relay_state, self.relay_names, self.on_count)
)
return s
def __json__(self):
return self.data
class PwznRelay(Device):
"""Main class representing the PWZN Relay."""
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_PWZN_RELAY_APPLE,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_PWZN_RELAY_APPLE
@command(default_output=format_output("", "on_count: {result.on_count}\n"))
def status(self) -> PwznRelayStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model].copy()
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return PwznRelayStatus(defaultdict(lambda: None, zip(properties, values)))
@command(
click.argument("number", type=int),
default_output=format_output("Turn on relay {number}"),
)
def relay_on(self, number: int = 0):
"""Relay X on."""
if self.send("power_on", [number]) == [0]:
return ["ok"]
@command(
click.argument("number", type=int),
default_output=format_output("Turn off relay {number}"),
)
def relay_off(self, number: int = 0):
"""Relay X off."""
if self.send("power_off", [number]) == [0]:
return ["ok"]
@command(default_output=format_output("Turn on all relay"))
def all_relay_on(self):
"""Relay all on."""
return self.send("power_all", [1])
@command(default_output=format_output("Turn off all relay"))
def all_relay_off(self):
"""Relay all off."""
return self.send("power_all", [0])
@command(
click.argument("number", type=int),
click.argument("name", type=str),
default_output=format_output("Set relay {number} name to {name}"),
)
def set_name(self, number: int = 0, name: str = ""):
"""Set relay X name."""
return self.send("set_name", [number, name])
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1585507846.5139406
python-miio-0.5.0.1/miio/tests/ 0000755 0001750 0001750 00000000000 00000000000 015663 5 ustar 00tpr tpr 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1507444682.0
python-miio-0.5.0.1/miio/tests/__init__.py 0000644 0001750 0001750 00000000000 00000000000 017762 0 ustar 00tpr tpr 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585489963.0
python-miio-0.5.0.1/miio/tests/dummies.py 0000644 0001750 0001750 00000005057 00000000000 017707 0 ustar 00tpr tpr 0000000 0000000 class DummyMiIOProtocol:
"""
DummyProtocol allows you mock MiIOProtocol.
"""
def __init__(self, dummy_device):
# TODO: Ideally, return_values should be passed in here. Passing in dummy_device (which must have
# return_values) is a temporary workaround to minimize diff size.
self.dummy_device = dummy_device
def send(self, command: str, parameters=None, retry_count=3):
"""Overridden send() to return values from `self.return_values`."""
return self.dummy_device.return_values[command](parameters)
class DummyDevice:
"""DummyDevice base class, you should inherit from this and call
`super().__init__(args, kwargs)` to save the original state.
This class provides helpers to test simple devices, for more complex
ones you will want to extend the `return_values` accordingly.
The basic idea is that the overloaded send() will read a wanted response
based on the call from `return_values`.
For changing values :func:`_set_state` will use :func:`pop()` to extract
the first parameter and set the state accordingly.
For a very simple device the following is enough, see :class:`TestPlug`
for complete code.
.. code-block::
self.return_values = {
"get_prop": self._get_state,
"power": lambda x: self._set_state("power", x)
}
"""
def __init__(self, *args, **kwargs):
self.start_state = self.state.copy()
self._protocol = DummyMiIOProtocol(self)
def _reset_state(self):
"""Revert back to the original state."""
self.state = self.start_state.copy()
def _set_state(self, var, value):
"""Set a state of a variable,
the value is expected to be an array with length of 1."""
# print("setting %s = %s" % (var, value))
self.state[var] = value.pop(0)
def _get_state(self, props):
"""Return wanted properties"""
return [self.state[x] for x in props if x in self.state]
class DummyMiotDevice(DummyDevice):
"""Main class representing a MIoT device."""
def __init__(self, *args, **kwargs):
# {prop["did"]: prop["value"] for prop in self.miot_client.get_properties()}
self.state = [{"did": k, "value": v, "code": 0} for k, v in self.state.items()]
super().__init__(*args, **kwargs)
def get_properties(self):
return self.state
def set_property(self, property_key: str, value):
for prop in self.state:
if prop["did"] == property_key:
prop["value"] = value
return None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1548870856.0
python-miio-0.5.0.1/miio/tests/test_airconditioningcompanion.json 0000644 0001750 0001750 00000013172 00000000000 024705 0 ustar 00tpr tpr 0000000 0000000 {
"test_send_ir_code_ok": [
{
"in": [
"010504870000714501",
"FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517"
],
"out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517"
},
{
"in": [
"010504870000714501",
"FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517",
1
],
"out": "FE04870000714594701FFF7AFF06004227490025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517"
},
{
"in": [
"010504870000714501",
"FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517",
134
],
"out": "FE04870000714594701FFFFFFF06004227CE0025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517"
}
],
"test_send_ir_code_exception": [
{
"in": [
"010504870000714501",
"FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517",
-1
],
"out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517"
},
{
"in": [
"010504870000714501",
"FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517",
135
],
"out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517"
},
{
"in": [
"Y",
"FE00000000000000000000000006004222680025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517",
0
],
"out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517"
},
{
"in": [
"010504870000714501",
"Z",
0
],
"out": "FE04870000714594701FFF79FF06004227480025002D008500AC015A138843010201020102010201010102010202010201010102020201020102010202010101010101010101010102010101010101020101010102010102010101020201020517"
}
],
"test_send_configuration_ok": [
{
"in": ["010000000001072700", {"__enum__": "Power.Off"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}],
"out": "010001072701011101004000205002112000D04000207002000000A0"
},
{
"in": ["010000000001072700", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}],
"out": "010001072712001611001906205002102000C0190620700200000090"
},
{
"in": ["010000000001072700", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.High"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}],
"out": "010001072712201611001906205002102000C0190620700200000090"
},
{
"in": ["010000000001072700", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.Off"}, {"__enum__": "Led.Off"}],
"out": "010001072712011611001906205002102000C0190620700200000090"
},
{
"in": ["010000000001072700", {"__enum__": "Power.Off"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.On"}],
"out": "010001072701011101004000205002112000D04000207002000000A0"
},
{
"in": ["010000000001072700", {"__enum__": "Power.Off"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.Off"}, {"__enum__": "Led.Off"}],
"out": "010001072701011101004000205002112000D04000207002000000A0"
},
{
"in": ["010000000001072700", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 23, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}],
"out": "010001072712001711001907205002102000D01907207002000000A0"
},
{
"in": ["010000000001072700", {"__enum__": "Power.Off"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}],
"out": "010001072701011101004000205002112000D04000207002000000A0"
},
{
"in": ["010507950000257301", {"__enum__": "Power.On"}, {"__enum__": "OperationMode.Auto"}, 22, {"__enum__": "FanSpeed.Low"}, {"__enum__": "SwingMode.On"}, {"__enum__": "Led.Off"}],
"out": "0100002573120016A1"
}
]
} ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_airconditioningcompanion.py 0000644 0001750 0001750 00000023026 00000000000 024363 0 ustar 00tpr tpr 0000000 0000000 import json
import os
import string
from unittest import TestCase
import pytest
from miio import AirConditioningCompanion, AirConditioningCompanionV3
from miio.airconditioningcompanion import (
MODEL_ACPARTNER_V3,
STORAGE_SLOT_ID,
AirConditioningCompanionException,
AirConditioningCompanionStatus,
FanSpeed,
Led,
OperationMode,
Power,
SwingMode,
)
from miio.tests.dummies import DummyDevice
STATE_ON = ["on"]
STATE_OFF = ["off"]
PUBLIC_ENUMS = {
"OperationMode": OperationMode,
"FanSpeed": FanSpeed,
"Power": Power,
"SwingMode": SwingMode,
"Led": Led,
}
def as_enum(d):
if "__enum__" in d:
name, member = d["__enum__"].split(".")
return getattr(PUBLIC_ENUMS[name], member)
else:
return d
with open(
os.path.join(os.path.dirname(__file__), "test_airconditioningcompanion.json")
) as inp:
test_data = json.load(inp, object_hook=as_enum)
class EnumEncoder(json.JSONEncoder):
def default(self, obj):
if type(obj) in PUBLIC_ENUMS.values():
return {"__enum__": str(obj)}
return json.JSONEncoder.default(self, obj)
class DummyAirConditioningCompanion(DummyDevice, AirConditioningCompanion):
def __init__(self, *args, **kwargs):
self.state = ["010500978022222102", "01020119A280222221", "2"]
self.last_ir_played = None
self.return_values = {
"get_model_and_state": self._get_state,
"start_ir_learn": lambda x: True,
"end_ir_learn": lambda x: True,
"get_ir_learn_result": lambda x: True,
"send_ir_code": lambda x: self._send_input_validation(x),
"send_cmd": lambda x: self._send_input_validation(x),
"set_power": lambda x: self._set_power(x),
}
self.start_state = self.state.copy()
super().__init__(args, kwargs)
def _reset_state(self):
"""Revert back to the original state."""
self.state = self.start_state.copy()
def _get_state(self, props):
"""Return the requested data"""
return self.state
def _set_power(self, value: str):
"""Set the requested power state"""
if value == STATE_ON:
self.state[1] = self.state[1][:2] + "1" + self.state[1][3:]
if value == STATE_OFF:
self.state[1] = self.state[1][:2] + "0" + self.state[1][3:]
@staticmethod
def _hex_input_validation(payload):
return all(c in string.hexdigits for c in payload[0])
def _send_input_validation(self, payload):
if self._hex_input_validation(payload[0]):
self.last_ir_played = payload[0]
return True
return False
def get_last_ir_played(self):
return self.last_ir_played
@pytest.fixture(scope="class")
def airconditioningcompanion(request):
request.cls.device = DummyAirConditioningCompanion()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airconditioningcompanion")
class TestAirConditioningCompanion(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(
AirConditioningCompanionStatus(
dict(model_and_state=self.device.start_state)
)
)
assert self.is_on() is False
assert self.state().power_socket is None
assert self.state().load_power == 2
assert self.state().air_condition_model == bytes.fromhex("010500978022222102")
assert self.state().model_format == 1
assert self.state().device_type == 5
assert self.state().air_condition_brand == int("0097", 16)
assert self.state().air_condition_remote == int("80222221", 16)
assert self.state().state_format == 2
assert self.state().air_condition_configuration == "020119A2"
assert self.state().target_temperature == 25
assert self.state().swing_mode == SwingMode.Off
assert self.state().fan_speed == FanSpeed.Low
assert self.state().mode == OperationMode.Auto
assert self.state().led is False
def test_status_without_target_temperature(self):
self.device._reset_state()
self.device.state[1] = None
assert self.state().target_temperature is None
def test_status_without_swing_mode(self):
self.device._reset_state()
self.device.state[1] = None
assert self.state().swing_mode is None
def test_status_without_mode(self):
self.device._reset_state()
self.device.state[1] = None
assert self.state().mode is None
def test_status_without_fan_speed(self):
self.device._reset_state()
self.device.state[1] = None
assert self.state().fan_speed is None
def test_learn(self):
assert self.device.learn(STORAGE_SLOT_ID) is True
assert self.device.learn() is True
def test_learn_result(self):
assert self.device.learn_result() is True
def test_learn_stop(self):
assert self.device.learn_stop(STORAGE_SLOT_ID) is True
assert self.device.learn_stop() is True
def test_send_ir_code(self):
for args in test_data["test_send_ir_code_ok"]:
with self.subTest():
self.device._reset_state()
self.assertTrue(self.device.send_ir_code(*args["in"]))
self.assertSequenceEqual(self.device.get_last_ir_played(), args["out"])
for args in test_data["test_send_ir_code_exception"]:
with pytest.raises(AirConditioningCompanionException):
self.device.send_ir_code(*args["in"])
def test_send_command(self):
assert self.device.send_command("0000000") is True
def test_send_configuration(self):
for args in test_data["test_send_configuration_ok"]:
with self.subTest():
self.device._reset_state()
self.assertTrue(self.device.send_configuration(*args["in"]))
self.assertSequenceEqual(self.device.get_last_ir_played(), args["out"])
class DummyAirConditioningCompanionV3(DummyDevice, AirConditioningCompanionV3):
def __init__(self, *args, **kwargs):
self.state = ["010507950000257301", "011001160100002573", "807"]
self.device_prop = {"lumi.0": {"plug_state": ["on"]}}
self.model = MODEL_ACPARTNER_V3
self.last_ir_played = None
self.return_values = {
"get_model_and_state": self._get_state,
"get_device_prop": self._get_device_prop,
"toggle_plug": self._toggle_plug,
}
self.start_state = self.state.copy()
self.start_device_prop = self.device_prop.copy()
super().__init__(args, kwargs)
def _reset_state(self):
"""Revert back to the original state."""
self.state = self.start_state.copy()
def _get_state(self, props):
"""Return the requested data"""
return self.state
def _get_device_prop(self, props):
"""Return the requested data"""
return self.device_prop[props[0]][props[1]]
def _toggle_plug(self, props):
"""Toggle the lumi.0 plug state"""
self.device_prop["lumi.0"]["plug_state"] = [props.pop()]
@pytest.fixture(scope="class")
def airconditioningcompanionv3(request):
request.cls.device = DummyAirConditioningCompanionV3()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airconditioningcompanionv3")
class TestAirConditioningCompanionV3(TestCase):
def state(self):
return self.device.status()
def is_on(self):
return self.device.status().is_on
def test_socket_on(self):
self.device.socket_off() # ensure off
assert self.state().power_socket == "off"
self.device.socket_on()
assert self.state().power_socket == "on"
def test_socket_off(self):
self.device.socket_on() # ensure on
assert self.state().power_socket == "on"
self.device.socket_off()
assert self.state().power_socket == "off"
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(
AirConditioningCompanionStatus(
dict(
model_and_state=self.device.start_state,
power_socket=self.device.start_device_prop["lumi.0"]["plug_state"][
0
],
)
)
)
assert self.is_on() is True
assert self.state().power_socket == "on"
assert self.state().load_power == 807
assert self.state().air_condition_model == bytes.fromhex("010507950000257301")
assert self.state().model_format == 1
assert self.state().device_type == 5
assert self.state().air_condition_brand == int("0795", 16)
assert self.state().air_condition_remote == int("00002573", 16)
assert self.state().state_format == 1
assert self.state().air_condition_configuration == "10011601"
assert self.state().target_temperature == 22
assert self.state().swing_mode == SwingMode.Off
assert self.state().fan_speed == FanSpeed.Low
assert self.state().mode == OperationMode.Heat
assert self.state().led is True
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_airdehumidifier.py 0000644 0001750 0001750 00000014536 00000000000 022437 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import AirDehumidifier
from miio.airdehumidifier import (
MODEL_DEHUMIDIFIER_V1,
AirDehumidifierException,
AirDehumidifierStatus,
FanSpeed,
OperationMode,
)
from miio.device import DeviceInfo
from .dummies import DummyDevice
class DummyAirDehumidifierV1(DummyDevice, AirDehumidifier):
def __init__(self, *args, **kwargs):
self.model = MODEL_DEHUMIDIFIER_V1
self.dummy_device_info = {
"life": 348202,
"uid": 1759530000,
"model": "nwt.derh.wdh318efw1",
"token": "68ffffffffffffffffffffffffffffff",
"fw_ver": "2.0.5",
"mcu_fw_ver": "0018",
"miio_ver": "0.0.5",
"hw_ver": "esp32",
"mmfree": 65476,
"mac": "78:11:FF:FF:FF:FF",
"wifi_fw_ver": "v3.1.4-56-g8ffb04960",
"netif": {
"gw": "192.168.0.1",
"localIp": "192.168.0.25",
"mask": "255.255.255.0",
},
}
self.device_info = None
self.state = {
"on_off": "on",
"mode": "auto",
"fan_st": 2,
"buzzer": "off",
"led": "on",
"child_lock": "off",
"humidity": 48,
"temp": 34,
"compressor_status": "off",
"fan_speed": 0,
"tank_full": "off",
"defrost_status": "off",
"alarm": "ok",
"auto": 50,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("on_off", x),
"set_mode": lambda x: self._set_state("mode", x),
"set_led": lambda x: self._set_state("led", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_fan_speed": lambda x: self._set_state("fan_st", x),
"set_auto": lambda x: self._set_state("auto", x),
"miIO.info": self._get_device_info,
}
super().__init__(args, kwargs)
def _get_device_info(self, _):
"""Return dummy device info."""
return self.dummy_device_info
@pytest.fixture(scope="class")
def airdehumidifierv1(request):
request.cls.device = DummyAirDehumidifierV1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airdehumidifierv1")
class TestAirDehumidifierV1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
device_info = DeviceInfo(self.device.dummy_device_info)
assert repr(self.state()) == repr(
AirDehumidifierStatus(self.device.start_state, device_info)
)
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temp"]
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().led == (self.device.start_state["led"] == "on")
assert self.state().buzzer == (self.device.start_state["buzzer"] == "on")
assert self.state().child_lock == (
self.device.start_state["child_lock"] == "on"
)
assert self.state().target_humidity == self.device.start_state["auto"]
assert self.state().fan_speed == FanSpeed(self.device.start_state["fan_speed"])
assert self.state().tank_full == (self.device.start_state["tank_full"] == "on")
assert self.state().compressor_status == (
self.device.start_state["compressor_status"] == "on"
)
assert self.state().defrost_status == (
self.device.start_state["defrost_status"] == "on"
)
assert self.state().fan_st == self.device.start_state["fan_st"]
assert self.state().alarm == self.device.start_state["alarm"]
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.On)
assert mode() == OperationMode.On
self.device.set_mode(OperationMode.Auto)
assert mode() == OperationMode.Auto
self.device.set_mode(OperationMode.DryCloth)
assert mode() == OperationMode.DryCloth
def test_set_led(self):
def led():
return self.device.status().led
self.device.set_led(True)
assert led() is True
self.device.set_led(False)
assert led() is False
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_status_without_temperature(self):
self.device._reset_state()
self.device.state["temp"] = None
assert self.state().temperature is None
def test_set_target_humidity(self):
def target_humidity():
return self.device.status().target_humidity
self.device.set_target_humidity(40)
assert target_humidity() == 40
self.device.set_target_humidity(50)
assert target_humidity() == 50
self.device.set_target_humidity(60)
assert target_humidity() == 60
with pytest.raises(AirDehumidifierException):
self.device.set_target_humidity(-1)
with pytest.raises(AirDehumidifierException):
self.device.set_target_humidity(30)
with pytest.raises(AirDehumidifierException):
self.device.set_target_humidity(70)
with pytest.raises(AirDehumidifierException):
self.device.set_target_humidity(110)
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584295509.0
python-miio-0.5.0.1/miio/tests/test_airfilter_util.py 0000644 0001750 0001750 00000003004 00000000000 022307 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio.airfilter_util import FilterType, FilterTypeUtil
@pytest.fixture(scope="class")
def airfilter_util(request):
request.cls.filter_type_util = FilterTypeUtil()
@pytest.mark.usefixtures("airfilter_util")
class TestAirFilterUtil(TestCase):
def test_determine_filter_type__recognises_unknown_filter(self):
assert (
self.filter_type_util.determine_filter_type("0:0:0:0:0:0:0", None)
is FilterType.Unknown
)
def test_determine_filter_type__recognises_antibacterial_filter(self):
assert (
self.filter_type_util.determine_filter_type(
"80:64:d1:ba:4f:5f:4", "12:34:41:30"
)
is FilterType.AntiBacterial
)
def test_determine_filter_type__recognises_antiformaldehyde_filter(self):
assert (
self.filter_type_util.determine_filter_type(
"80:64:d1:ba:4f:5f:4", "12:34:00:31"
)
is FilterType.AntiFormaldehyde
)
def test_determine_filter_type__falls_back_to_regular_filter(self):
regular_filters = [
"12:34:56:78",
"12:34:56:31",
"12:34:56:31:11:11",
"CO:FF:FF:EE",
None,
]
for product_id in regular_filters:
assert (
self.filter_type_util.determine_filter_type(
"80:64:d1:ba:4f:5f:4", product_id
)
is FilterType.Regular
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_airfresh.py 0000644 0001750 0001750 00000014700 00000000000 021101 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import AirFresh
from miio.airfresh import (
AirFreshException,
AirFreshStatus,
LedBrightness,
OperationMode,
)
from .dummies import DummyDevice
class DummyAirFresh(DummyDevice, AirFresh):
def __init__(self, *args, **kwargs):
self.state = {
"power": "on",
"temp_dec": 186,
"aqi": 10,
"average_aqi": 8,
"humidity": 62,
"co2": 350,
"buzzer": "off",
"child_lock": "off",
"led_level": 2,
"mode": "auto",
"motor1_speed": 354,
"use_time": 2457000,
"ntcT": None,
"app_extra": 1,
"f1_hour_used": 682,
"filter_life": 80,
"f_hour": 3500,
"favorite_level": None,
"led": "on",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_mode": lambda x: self._set_state("mode", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_led": lambda x: self._set_state("led", x),
"set_led_level": lambda x: self._set_state("led_level", x),
"reset_filter1": lambda x: (
self._set_state("f1_hour_used", [0]),
self._set_state("filter_life", [100]),
),
"set_app_extra": lambda x: self._set_state("app_extra", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def airfresh(request):
request.cls.device = DummyAirFresh()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airfresh")
class TestAirFresh(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(AirFreshStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().aqi == self.device.start_state["aqi"]
assert self.state().average_aqi == self.device.start_state["average_aqi"]
assert self.state().temperature == self.device.start_state["temp_dec"] / 10.0
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().co2 == self.device.start_state["co2"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert (
self.state().filter_life_remaining == self.device.start_state["filter_life"]
)
assert self.state().filter_hours_used == self.device.start_state["f1_hour_used"]
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().motor_speed == self.device.start_state["motor1_speed"]
assert self.state().led == (self.device.start_state["led"] == "on")
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_level"]
)
assert self.state().buzzer == (self.device.start_state["buzzer"] == "on")
assert self.state().child_lock == (
self.device.start_state["child_lock"] == "on"
)
assert self.state().extra_features == self.device.start_state["app_extra"]
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Auto)
assert mode() == OperationMode.Auto
self.device.set_mode(OperationMode.Silent)
assert mode() == OperationMode.Silent
self.device.set_mode(OperationMode.Interval)
assert mode() == OperationMode.Interval
self.device.set_mode(OperationMode.Low)
assert mode() == OperationMode.Low
self.device.set_mode(OperationMode.Middle)
assert mode() == OperationMode.Middle
self.device.set_mode(OperationMode.Strong)
assert mode() == OperationMode.Strong
def test_set_led(self):
def led():
return self.device.status().led
self.device.set_led(True)
assert led() is True
self.device.set_led(False)
assert led() is False
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_set_extra_features(self):
def extra_features():
return self.device.status().extra_features
self.device.set_extra_features(0)
assert extra_features() == 0
self.device.set_extra_features(1)
assert extra_features() == 1
self.device.set_extra_features(2)
assert extra_features() == 2
with pytest.raises(AirFreshException):
self.device.set_extra_features(-1)
def test_reset_filter(self):
def filter_hours_used():
return self.device.status().filter_hours_used
def filter_life_remaining():
return self.device.status().filter_life_remaining
self.device._reset_state()
assert filter_hours_used() != 0
assert filter_life_remaining() != 100
self.device.reset_filter()
assert filter_hours_used() == 0
assert filter_life_remaining() == 100
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_airfresh_t2017.py 0000644 0001750 0001750 00000017057 00000000000 021746 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import AirFreshT2017
from miio.airfresh_t2017 import (
MODEL_AIRFRESH_T2017,
AirFreshException,
AirFreshStatus,
DisplayOrientation,
OperationMode,
PtcLevel,
)
from .dummies import DummyDevice
class DummyAirFreshT2017(DummyDevice, AirFreshT2017):
def __init__(self, *args, **kwargs):
self.model = MODEL_AIRFRESH_T2017
self.state = {
"power": True,
"mode": "favourite",
"pm25": 1,
"co2": 550,
"temperature_outside": 24,
"favourite_speed": 241,
"control_speed": 241,
"filter_intermediate": 99,
"filter_inter_day": 89,
"filter_efficient": 99,
"filter_effi_day": 179,
"ptc_on": False,
"ptc_level": "low",
"ptc_status": False,
"child_lock": False,
"sound": True,
"display": False,
"screen_direction": "forward",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", [(x[0] == "on")]),
"set_mode": lambda x: self._set_state("mode", x),
"set_sound": lambda x: self._set_state("sound", [(x[0] == "on")]),
"set_child_lock": lambda x: self._set_state("child_lock", [(x[0] == "on")]),
"set_display": lambda x: self._set_state("display", [(x[0] == "on")]),
"set_screen_direction": lambda x: self._set_state("screen_direction", x),
"set_ptc_level": lambda x: self._set_state("ptc_level", x),
"set_favourite_speed": lambda x: self._set_state("favourite_speed", x),
"set_filter_reset": lambda x: self._set_filter_reset(x),
}
super().__init__(args, kwargs)
def _set_filter_reset(self, value: str):
if value[0] == "efficient":
self._set_state("filter_efficient", [100])
self._set_state("filter_effi_day", [180])
if value[0] == "intermediate":
self._set_state("filter_intermediate", [100])
self._set_state("filter_inter_day", [90])
@pytest.fixture(scope="class")
def airfresht2017(request):
request.cls.device = DummyAirFreshT2017()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airfresht2017")
class TestAirFreshT2017(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(AirFreshStatus(self.device.start_state))
assert self.is_on() is True
assert (
self.state().temperature == self.device.start_state["temperature_outside"]
)
assert self.state().co2 == self.device.start_state["co2"]
assert self.state().pm25 == self.device.start_state["pm25"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().buzzer == self.device.start_state["sound"]
assert self.state().child_lock == self.device.start_state["child_lock"]
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Off)
assert mode() == OperationMode.Off
self.device.set_mode(OperationMode.Auto)
assert mode() == OperationMode.Auto
self.device.set_mode(OperationMode.Sleep)
assert mode() == OperationMode.Sleep
self.device.set_mode(OperationMode.Favorite)
assert mode() == OperationMode.Favorite
def test_set_display(self):
def display():
return self.device.status().display
self.device.set_display(True)
assert display() is True
self.device.set_display(False)
assert display() is False
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_reset_dust_filter(self):
def dust_filter_life_remaining():
return self.device.status().dust_filter_life_remaining
def dust_filter_life_remaining_days():
return self.device.status().dust_filter_life_remaining_days
self.device._reset_state()
assert dust_filter_life_remaining() != 100
assert dust_filter_life_remaining_days() != 90
self.device.reset_dust_filter()
assert dust_filter_life_remaining() == 100
assert dust_filter_life_remaining_days() == 90
def test_reset_upper_filter(self):
def upper_filter_life_remaining():
return self.device.status().upper_filter_life_remaining
def upper_filter_life_remaining_days():
return self.device.status().upper_filter_life_remaining_days
self.device._reset_state()
assert upper_filter_life_remaining() != 100
assert upper_filter_life_remaining_days() != 180
self.device.reset_upper_filter()
assert upper_filter_life_remaining() == 100
assert upper_filter_life_remaining_days() == 180
def test_set_favorite_speed(self):
def favorite_speed():
return self.device.status().favorite_speed
self.device.set_favorite_speed(60)
assert favorite_speed() == 60
self.device.set_favorite_speed(120)
assert favorite_speed() == 120
self.device.set_favorite_speed(240)
assert favorite_speed() == 240
self.device.set_favorite_speed(300)
assert favorite_speed() == 300
with pytest.raises(AirFreshException):
self.device.set_favorite_speed(-1)
with pytest.raises(AirFreshException):
self.device.set_favorite_speed(59)
with pytest.raises(AirFreshException):
self.device.set_favorite_speed(301)
def test_set_ptc_level(self):
def ptc_level():
return self.device.status().ptc_level
self.device.set_ptc_level(PtcLevel.Off)
assert ptc_level() == PtcLevel.Off
self.device.set_ptc_level(PtcLevel.Low)
assert ptc_level() == PtcLevel.Low
self.device.set_ptc_level(PtcLevel.Medium)
assert ptc_level() == PtcLevel.Medium
self.device.set_ptc_level(PtcLevel.High)
assert ptc_level() == PtcLevel.High
def test_set_display_orientation(self):
def display_orientation():
return self.device.status().display_orientation
self.device.set_display_orientation(DisplayOrientation.Portrait)
assert display_orientation() == DisplayOrientation.Portrait
self.device.set_display_orientation(DisplayOrientation.LandscapeLeft)
assert display_orientation() == DisplayOrientation.LandscapeLeft
self.device.set_display_orientation(DisplayOrientation.LandscapeRight)
assert display_orientation() == DisplayOrientation.LandscapeRight
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_airhumidifier.py 0000644 0001750 0001750 00000055025 00000000000 022124 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import AirHumidifier
from miio.airhumidifier import (
MODEL_HUMIDIFIER_CA1,
MODEL_HUMIDIFIER_CB1,
MODEL_HUMIDIFIER_V1,
AirHumidifierException,
AirHumidifierStatus,
LedBrightness,
OperationMode,
)
from miio.device import DeviceInfo
from .dummies import DummyDevice
class DummyAirHumidifierV1(DummyDevice, AirHumidifier):
def __init__(self, *args, **kwargs):
self.model = MODEL_HUMIDIFIER_V1
self.dummy_device_info = {
"fw_ver": "1.2.9_5033",
"token": "68ffffffffffffffffffffffffffffff",
"otu_stat": [101, 74, 5343, 0, 5327, 407],
"mmfree": 228248,
"netif": {
"gw": "192.168.0.1",
"localIp": "192.168.0.25",
"mask": "255.255.255.0",
},
"ott_stat": [0, 0, 0, 0],
"model": "zhimi.humidifier.v1",
"cfg_time": 0,
"life": 575661,
"ap": {"rssi": -35, "ssid": "ap", "bssid": "FF:FF:FF:FF:FF:FF"},
"wifi_fw_ver": "SD878x-14.76.36.p84-702.1.0-WM",
"hw_ver": "MW300",
"ot": "otu",
"mac": "78:11:FF:FF:FF:FF",
}
self.device_info = None
self.state = {
"power": "on",
"mode": "medium",
"temp_dec": 294,
"humidity": 33,
"buzzer": "off",
"led_b": 2,
"child_lock": "on",
"limit_hum": 40,
"trans_level": 85,
"use_time": 941100,
"button_pressed": "led",
"hw_version": 0,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_mode": lambda x: self._set_state("mode", x),
"set_led_b": lambda x: self._set_state("led_b", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_limit_hum": lambda x: self._set_state("limit_hum", x),
"miIO.info": self._get_device_info,
}
super().__init__(args, kwargs)
def _get_device_info(self, _):
"""Return dummy device info."""
return self.dummy_device_info
@pytest.fixture(scope="class")
def airhumidifierv1(request):
request.cls.device = DummyAirHumidifierV1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airhumidifierv1")
class TestAirHumidifierV1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
device_info = DeviceInfo(self.device.dummy_device_info)
assert repr(self.state()) == repr(
AirHumidifierStatus(self.device.start_state, device_info)
)
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temp_dec"] / 10.0
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_b"]
)
assert self.state().buzzer == (self.device.start_state["buzzer"] == "on")
assert self.state().child_lock == (
self.device.start_state["child_lock"] == "on"
)
assert self.state().target_humidity == self.device.start_state["limit_hum"]
assert self.state().trans_level == self.device.start_state["trans_level"]
assert self.state().motor_speed is None
assert self.state().depth is None
assert self.state().dry is None
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().hardware_version == self.device.start_state["hw_version"]
assert self.state().button_pressed == self.device.start_state["button_pressed"]
assert self.state().firmware_version == device_info.firmware_version
assert (
self.state().firmware_version_major
== device_info.firmware_version.rsplit("_", 1)[0]
)
assert self.state().firmware_version_minor == int(
device_info.firmware_version.rsplit("_", 1)[1]
)
assert self.state().strong_mode_enabled is False
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Silent)
assert mode() == OperationMode.Silent
self.device.set_mode(OperationMode.Medium)
assert mode() == OperationMode.Medium
self.device.set_mode(OperationMode.High)
assert mode() == OperationMode.High
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_led(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led(True)
assert led_brightness() == LedBrightness.Bright
self.device.set_led(False)
assert led_brightness() == LedBrightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_status_without_temperature(self):
self.device._reset_state()
self.device.state["temp_dec"] = None
assert self.state().temperature is None
def test_status_without_led_brightness(self):
self.device._reset_state()
self.device.state["led_b"] = None
assert self.state().led_brightness is None
def test_set_target_humidity(self):
def target_humidity():
return self.device.status().target_humidity
self.device.set_target_humidity(30)
assert target_humidity() == 30
self.device.set_target_humidity(60)
assert target_humidity() == 60
self.device.set_target_humidity(80)
assert target_humidity() == 80
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(-1)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(20)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(90)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(110)
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
class DummyAirHumidifierCA1(DummyDevice, AirHumidifier):
def __init__(self, *args, **kwargs):
self.model = MODEL_HUMIDIFIER_CA1
self.dummy_device_info = {
"fw_ver": "1.6.6",
"token": "68ffffffffffffffffffffffffffffff",
"otu_stat": [101, 74, 5343, 0, 5327, 407],
"mmfree": 228248,
"netif": {
"gw": "192.168.0.1",
"localIp": "192.168.0.25",
"mask": "255.255.255.0",
},
"ott_stat": [0, 0, 0, 0],
"model": "zhimi.humidifier.v1",
"cfg_time": 0,
"life": 575661,
"ap": {"rssi": -35, "ssid": "ap", "bssid": "FF:FF:FF:FF:FF:FF"},
"wifi_fw_ver": "SD878x-14.76.36.p84-702.1.0-WM",
"hw_ver": "MW300",
"ot": "otu",
"mac": "78:11:FF:FF:FF:FF",
}
self.device_info = None
self.state = {
"power": "on",
"mode": "medium",
"temp_dec": 294,
"humidity": 33,
"buzzer": "off",
"led_b": 2,
"child_lock": "on",
"limit_hum": 40,
"use_time": 941100,
"hw_version": 0,
# Additional attributes of the zhimi.humidifier.ca1
"speed": 100,
"depth": 1,
"dry": "off",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_mode": lambda x: self._set_state("mode", x),
"set_led_b": lambda x: self._set_state("led_b", [int(x[0])]),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_limit_hum": lambda x: self._set_state("limit_hum", x),
"set_dry": lambda x: self._set_state("dry", x),
"miIO.info": self._get_device_info,
}
super().__init__(args, kwargs)
def _get_device_info(self, _):
"""Return dummy device info."""
return self.dummy_device_info
@pytest.fixture(scope="class")
def airhumidifierca1(request):
request.cls.device = DummyAirHumidifierCA1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airhumidifierca1")
class TestAirHumidifierCA1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
device_info = DeviceInfo(self.device.dummy_device_info)
assert repr(self.state()) == repr(
AirHumidifierStatus(self.device.start_state, device_info)
)
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temp_dec"] / 10.0
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_b"]
)
assert self.state().buzzer == (self.device.start_state["buzzer"] == "on")
assert self.state().child_lock == (
self.device.start_state["child_lock"] == "on"
)
assert self.state().target_humidity == self.device.start_state["limit_hum"]
assert self.state().trans_level is None
assert self.state().motor_speed == self.device.start_state["speed"]
assert self.state().depth == self.device.start_state["depth"]
assert self.state().dry == (self.device.start_state["dry"] == "on")
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().hardware_version == self.device.start_state["hw_version"]
assert self.state().button_pressed is None
assert self.state().firmware_version == device_info.firmware_version
assert (
self.state().firmware_version_major
== device_info.firmware_version.rsplit("_", 1)[0]
)
try:
version_minor = int(device_info.firmware_version.rsplit("_", 1)[1])
except IndexError:
version_minor = 0
assert self.state().firmware_version_minor == version_minor
assert self.state().strong_mode_enabled is False
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Silent)
assert mode() == OperationMode.Silent
self.device.set_mode(OperationMode.Medium)
assert mode() == OperationMode.Medium
self.device.set_mode(OperationMode.High)
assert mode() == OperationMode.High
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_led(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led(True)
assert led_brightness() == LedBrightness.Bright
self.device.set_led(False)
assert led_brightness() == LedBrightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_status_without_temperature(self):
self.device._reset_state()
self.device.state["temp_dec"] = None
assert self.state().temperature is None
def test_status_without_led_brightness(self):
self.device._reset_state()
self.device.state["led_b"] = None
assert self.state().led_brightness is None
def test_set_target_humidity(self):
def target_humidity():
return self.device.status().target_humidity
self.device.set_target_humidity(30)
assert target_humidity() == 30
self.device.set_target_humidity(60)
assert target_humidity() == 60
self.device.set_target_humidity(80)
assert target_humidity() == 80
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(-1)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(20)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(90)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(110)
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_set_dry(self):
def dry():
return self.device.status().dry
self.device.set_dry(True)
assert dry() is True
self.device.set_dry(False)
assert dry() is False
class DummyAirHumidifierCB1(DummyDevice, AirHumidifier):
def __init__(self, *args, **kwargs):
self.model = MODEL_HUMIDIFIER_CB1
self.dummy_device_info = {
"fw_ver": "1.2.9_5033",
"token": "68ffffffffffffffffffffffffffffff",
"otu_stat": [101, 74, 5343, 0, 5327, 407],
"mmfree": 228248,
"netif": {
"gw": "192.168.0.1",
"localIp": "192.168.0.25",
"mask": "255.255.255.0",
},
"ott_stat": [0, 0, 0, 0],
"model": "zhimi.humidifier.v1",
"cfg_time": 0,
"life": 575661,
"ap": {"rssi": -35, "ssid": "ap", "bssid": "FF:FF:FF:FF:FF:FF"},
"wifi_fw_ver": "SD878x-14.76.36.p84-702.1.0-WM",
"hw_ver": "MW300",
"ot": "otu",
"mac": "78:11:FF:FF:FF:FF",
}
self.device_info = None
self.state = {
"power": "on",
"mode": "medium",
"humidity": 33,
"buzzer": "off",
"led_b": 2,
"child_lock": "on",
"limit_hum": 40,
"use_time": 941100,
"hw_version": 0,
# Additional attributes of the zhimi.humidifier.cb1
"temperature": 29.4,
"speed": 100,
"depth": 1,
"dry": "off",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_mode": lambda x: self._set_state("mode", x),
"set_led_b": lambda x: self._set_state("led_b", [int(x[0])]),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_limit_hum": lambda x: self._set_state("limit_hum", x),
"set_dry": lambda x: self._set_state("dry", x),
"miIO.info": self._get_device_info,
}
super().__init__(args, kwargs)
def _get_device_info(self, _):
"""Return dummy device info."""
return self.dummy_device_info
@pytest.fixture(scope="class")
def airhumidifiercb1(request):
request.cls.device = DummyAirHumidifierCB1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airhumidifiercb1")
class TestAirHumidifierCB1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
device_info = DeviceInfo(self.device.dummy_device_info)
assert repr(self.state()) == repr(
AirHumidifierStatus(self.device.start_state, device_info)
)
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_b"]
)
assert self.state().buzzer == (self.device.start_state["buzzer"] == "on")
assert self.state().child_lock == (
self.device.start_state["child_lock"] == "on"
)
assert self.state().target_humidity == self.device.start_state["limit_hum"]
assert self.state().trans_level is None
assert self.state().motor_speed == self.device.start_state["speed"]
assert self.state().depth == self.device.start_state["depth"]
assert self.state().dry == (self.device.start_state["dry"] == "on")
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().hardware_version == self.device.start_state["hw_version"]
assert self.state().button_pressed is None
assert self.state().firmware_version == device_info.firmware_version
assert (
self.state().firmware_version_major
== device_info.firmware_version.rsplit("_", 1)[0]
)
assert self.state().firmware_version_minor == int(
device_info.firmware_version.rsplit("_", 1)[1]
)
assert self.state().strong_mode_enabled is False
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Silent)
assert mode() == OperationMode.Silent
self.device.set_mode(OperationMode.Medium)
assert mode() == OperationMode.Medium
self.device.set_mode(OperationMode.High)
assert mode() == OperationMode.High
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_led(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led(True)
assert led_brightness() == LedBrightness.Bright
self.device.set_led(False)
assert led_brightness() == LedBrightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_status_without_temperature(self):
self.device._reset_state()
self.device.state["temperature"] = None
assert self.state().temperature is None
def test_status_without_led_brightness(self):
self.device._reset_state()
self.device.state["led_b"] = None
assert self.state().led_brightness is None
def test_set_target_humidity(self):
def target_humidity():
return self.device.status().target_humidity
self.device.set_target_humidity(30)
assert target_humidity() == 30
self.device.set_target_humidity(60)
assert target_humidity() == 60
self.device.set_target_humidity(80)
assert target_humidity() == 80
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(-1)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(20)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(90)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(110)
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_set_dry(self):
def dry():
return self.device.status().dry
self.device.set_dry(True)
assert dry() is True
self.device.set_dry(False)
assert dry() is False
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500788.0
python-miio-0.5.0.1/miio/tests/test_airhumidifier_jsq.py 0000644 0001750 0001750 00000023301 00000000000 022771 0 ustar 00tpr tpr 0000000 0000000 from collections import OrderedDict
from unittest import TestCase
import pytest
from miio import AirHumidifierJsq
from miio.airhumidifier import AirHumidifierException
from miio.airhumidifier_jsq import (
MODEL_HUMIDIFIER_JSQ001,
AirHumidifierStatus,
LedBrightness,
OperationMode,
)
from .dummies import DummyDevice
class DummyAirHumidifierJsq(DummyDevice, AirHumidifierJsq):
def __init__(self, *args, **kwargs):
self.model = MODEL_HUMIDIFIER_JSQ001
self.dummy_device_info = {
"life": 575661,
"token": "68ffffffffffffffffffffffffffffff",
"mac": "78:11:FF:FF:FF:FF",
"fw_ver": "1.3.9",
"hw_ver": "ESP8266",
"uid": "1111111111",
"model": self.model,
"mcu_fw_ver": "0001",
"wifi_fw_ver": "1.5.0-dev(7efd021)",
"ap": {"rssi": -71, "ssid": "ap", "bssid": "FF:FF:FF:FF:FF:FF"},
"netif": {
"gw": "192.168.0.1",
"localIp": "192.168.0.25",
"mask": "255.255.255.0",
},
"mmfree": 228248,
}
self.device_info = None
self.state = OrderedDict(
(
("temperature", 24),
("humidity", 29),
("mode", 3),
("buzzer", 1),
("child_lock", 1),
("led_brightness", 2),
("power", 1),
("no_water", 1),
("lid_opened", 1),
)
)
self.start_state = self.state.copy()
self.return_values = {
"get_props": self._get_state,
"set_start": lambda x: self._set_state("power", x),
"set_mode": lambda x: self._set_state("mode", x),
"set_brightness": lambda x: self._set_state("led_brightness", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_lock": lambda x: self._set_state("child_lock", x),
"miIO.info": self._get_device_info,
}
super().__init__(args, kwargs)
def _get_device_info(self, _):
"""Return dummy device info."""
return self.dummy_device_info
def _get_state(self, props):
"""Return wanted properties"""
return list(self.state.values())
@pytest.fixture(scope="class")
def airhumidifier_jsq(request):
request.cls.device = DummyAirHumidifierJsq()
# TODO add ability to test on a real device
class Bunch:
def __init__(self, **kwds):
self.__dict__.update(kwds)
@pytest.mark.usefixtures("airhumidifier_jsq")
class TestAirHumidifierJsq(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(AirHumidifierStatus(self.device.start_state))
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().buzzer == (self.device.start_state["buzzer"] == 1)
assert self.state().child_lock == (self.device.start_state["child_lock"] == 1)
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_brightness"]
)
assert self.is_on() is True
assert self.state().no_water == (self.device.start_state["no_water"] == 1)
assert self.state().lid_opened == (self.device.start_state["lid_opened"] == 1)
def test_status_wrong_input(self):
def mode():
return self.device.status().mode
def led_brightness():
return self.device.status().led_brightness
self.device._reset_state()
self.device.state["mode"] = 10
assert mode() == OperationMode.Intelligent
self.device.state["mode"] = "smth"
assert mode() == OperationMode.Intelligent
self.device.state["led_brightness"] = 10
assert led_brightness() == LedBrightness.Off
self.device.state["led_brightness"] = "smth"
assert led_brightness() == LedBrightness.Off
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Intelligent)
assert mode() == OperationMode.Intelligent
self.device.set_mode(OperationMode.Level1)
assert mode() == OperationMode.Level1
self.device.set_mode(OperationMode.Level4)
assert mode() == OperationMode.Level4
def test_set_mode_wrong_input(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Level3)
assert mode() == OperationMode.Level3
with pytest.raises(AirHumidifierException) as excinfo:
self.device.set_mode(Bunch(value=10))
assert "10 is not a valid OperationMode value" == str(excinfo.value)
assert mode() == OperationMode.Level3
with pytest.raises(AirHumidifierException) as excinfo:
self.device.set_mode(Bunch(value=-1))
assert "-1 is not a valid OperationMode value" == str(excinfo.value)
assert mode() == OperationMode.Level3
with pytest.raises(AirHumidifierException) as excinfo:
self.device.set_mode(Bunch(value="smth"))
assert "smth is not a valid OperationMode value" == str(excinfo.value)
assert mode() == OperationMode.Level3
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
self.device.set_led_brightness(LedBrightness.Low)
assert led_brightness() == LedBrightness.Low
self.device.set_led_brightness(LedBrightness.High)
assert led_brightness() == LedBrightness.High
def test_set_led_brightness_wrong_input(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Low)
assert led_brightness() == LedBrightness.Low
with pytest.raises(AirHumidifierException) as excinfo:
self.device.set_led_brightness(Bunch(value=10))
assert "10 is not a valid LedBrightness value" == str(excinfo.value)
assert led_brightness() == LedBrightness.Low
with pytest.raises(AirHumidifierException) as excinfo:
self.device.set_led_brightness(Bunch(value=-10))
assert "-10 is not a valid LedBrightness value" == str(excinfo.value)
assert led_brightness() == LedBrightness.Low
with pytest.raises(AirHumidifierException) as excinfo:
self.device.set_led_brightness(Bunch(value="smth"))
assert "smth is not a valid LedBrightness value" == str(excinfo.value)
assert led_brightness() == LedBrightness.Low
def test_set_led(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led(True)
assert led_brightness() == LedBrightness.High
self.device.set_led(False)
assert led_brightness() == LedBrightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
# if user uses wrong type for buzzer value
self.device.set_buzzer(1)
assert buzzer() is True
self.device.set_buzzer(0)
assert buzzer() is False
self.device.set_buzzer("not_empty_str")
assert buzzer() is True
self.device.set_buzzer("on")
assert buzzer() is True
# all string values are considered to by True, even "off"
self.device.set_buzzer("off")
assert buzzer() is True
self.device.set_buzzer("")
assert buzzer() is False
def test_status_without_temperature(self):
self.device._reset_state()
self.device.state["temperature"] = None
assert self.state().temperature is None
def test_status_without_led_brightness(self):
self.device._reset_state()
self.device.state["led_brightness"] = None
assert self.state().led_brightness is LedBrightness.Off
def test_status_without_mode(self):
self.device._reset_state()
self.device.state["mode"] = None
assert self.state().mode is OperationMode.Intelligent
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
# if user uses wrong type for buzzer value
self.device.set_child_lock(1)
assert child_lock() is True
self.device.set_child_lock(0)
assert child_lock() is False
self.device.set_child_lock("not_empty_str")
assert child_lock() is True
self.device.set_child_lock("on")
assert child_lock() is True
# all string values are considered to by True, even "off"
self.device.set_child_lock("off")
assert child_lock() is True
self.device.set_child_lock("")
assert child_lock() is False
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_airhumidifier_mjjsq.py 0000644 0001750 0001750 00000010624 00000000000 023324 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import AirHumidifierMjjsq
from miio.airhumidifier_mjjsq import (
MODEL_HUMIDIFIER_MJJSQ,
AirHumidifierException,
AirHumidifierStatus,
OperationMode,
)
from .dummies import DummyDevice
class DummyAirHumidifierMjjsq(DummyDevice, AirHumidifierMjjsq):
def __init__(self, *args, **kwargs):
self.model = MODEL_HUMIDIFIER_MJJSQ
self.state = {
"Humidifier_Gear": 1,
"Humidity_Value": 44,
"HumiSet_Value": 11,
"Led_State": 0,
"OnOff_State": 1,
"TemperatureValue": 21,
"TipSound_State": 0,
"waterstatus": 1,
"watertankstatus": 1,
}
self.return_values = {
"get_prop": self._get_state,
"Set_OnOff": lambda x: self._set_state("OnOff_State", x),
"Set_HumidifierGears": lambda x: self._set_state("Humidifier_Gear", x),
"SetLedState": lambda x: self._set_state("Led_State", x),
"SetTipSound_Status": lambda x: self._set_state("TipSound_State", x),
"Set_HumiValue": lambda x: self._set_state("HumiSet_Value", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def airhumidifiermjjsq(request):
request.cls.device = DummyAirHumidifierMjjsq()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airhumidifiermjjsq")
class TestAirHumidifierMjjsq(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(AirHumidifierStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["TemperatureValue"]
assert self.state().humidity == self.device.start_state["Humidity_Value"]
assert self.state().mode == OperationMode(
self.device.start_state["Humidifier_Gear"]
)
assert self.state().led is (self.device.start_state["Led_State"] == 1)
assert self.state().buzzer is (self.device.start_state["TipSound_State"] == 1)
assert self.state().target_humidity == self.device.start_state["HumiSet_Value"]
assert self.state().no_water is (self.device.start_state["waterstatus"] == 0)
assert self.state().water_tank_detached is (
self.device.start_state["watertankstatus"] == 0
)
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Low)
assert mode() == OperationMode.Low
self.device.set_mode(OperationMode.Medium)
assert mode() == OperationMode.Medium
self.device.set_mode(OperationMode.High)
assert mode() == OperationMode.High
self.device.set_mode(OperationMode.Humidity)
assert mode() == OperationMode.Humidity
def test_set_led(self):
def led():
return self.device.status().led
self.device.set_led(True)
assert led() is True
self.device.set_led(False)
assert led() is False
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_target_humidity(self):
def target_humidity():
return self.device.status().target_humidity
self.device.set_target_humidity(0)
assert target_humidity() == 0
self.device.set_target_humidity(50)
assert target_humidity() == 50
self.device.set_target_humidity(99)
assert target_humidity() == 99
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(-1)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(100)
with pytest.raises(AirHumidifierException):
self.device.set_target_humidity(101)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_airpurifier.py 0000644 0001750 0001750 00000031440 00000000000 021617 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import AirPurifier
from miio.airpurifier import (
AirPurifierException,
AirPurifierStatus,
FilterType,
LedBrightness,
OperationMode,
SleepMode,
)
from .dummies import DummyDevice
class DummyAirPurifier(DummyDevice, AirPurifier):
def __init__(self, *args, **kwargs):
self.state = {
"power": "on",
"aqi": 10,
"average_aqi": 8,
"humidity": 62,
"temp_dec": 186,
"mode": "auto",
"favorite_level": 10,
"filter1_life": 80,
"f1_hour_used": 682,
"use_time": 2457000,
"motor1_speed": 354,
"motor2_speed": 800,
"purify_volume": 25262,
"f1_hour": 3500,
"led": "off",
"led_b": 2,
"bright": 83,
"buzzer": "off",
"child_lock": "off",
"volume": 50,
"rfid_product_id": "0:0:41:30",
"rfid_tag": "10:20:30:40:50:60:7",
"act_sleep": "close",
"sleep_mode": "idle",
"sleep_time": 83890,
"sleep_data_num": 22,
"app_extra": 1,
"act_det": "off",
"button_pressed": "power",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_mode": lambda x: self._set_state("mode", x),
"set_led": lambda x: self._set_state("led", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_level_favorite": lambda x: self._set_state("favorite_level", x),
"set_led_b": lambda x: self._set_state("led_b", x),
"set_volume": lambda x: self._set_state("volume", x),
"set_act_sleep": lambda x: self._set_state("act_sleep", x),
"reset_filter1": lambda x: (
self._set_state("f1_hour_used", [0]),
self._set_state("filter1_life", [100]),
),
"set_act_det": lambda x: self._set_state("act_det", x),
"set_app_extra": lambda x: self._set_state("app_extra", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def airpurifier(request):
request.cls.device = DummyAirPurifier()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airpurifier")
class TestAirPurifier(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(AirPurifierStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().aqi == self.device.start_state["aqi"]
assert self.state().average_aqi == self.device.start_state["average_aqi"]
assert self.state().temperature == self.device.start_state["temp_dec"] / 10.0
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().favorite_level == self.device.start_state["favorite_level"]
assert (
self.state().filter_life_remaining
== self.device.start_state["filter1_life"]
)
assert self.state().filter_hours_used == self.device.start_state["f1_hour_used"]
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().motor_speed == self.device.start_state["motor1_speed"]
assert self.state().motor2_speed == self.device.start_state["motor2_speed"]
assert self.state().purify_volume == self.device.start_state["purify_volume"]
assert self.state().led == (self.device.start_state["led"] == "on")
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_b"]
)
assert self.state().buzzer == (self.device.start_state["buzzer"] == "on")
assert self.state().child_lock == (
self.device.start_state["child_lock"] == "on"
)
assert self.state().illuminance == self.device.start_state["bright"]
assert self.state().volume == self.device.start_state["volume"]
assert (
self.state().filter_rfid_product_id
== self.device.start_state["rfid_product_id"]
)
assert self.state().sleep_mode == SleepMode(
self.device.start_state["sleep_mode"]
)
assert self.state().sleep_time == self.device.start_state["sleep_time"]
assert (
self.state().sleep_mode_learn_count
== self.device.start_state["sleep_data_num"]
)
assert self.state().extra_features == self.device.start_state["app_extra"]
assert self.state().turbo_mode_supported == (
self.device.start_state["app_extra"] == 1
)
assert self.state().auto_detect == (self.device.start_state["act_det"] == "on")
assert self.state().button_pressed == self.device.start_state["button_pressed"]
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Silent)
assert mode() == OperationMode.Silent
self.device.set_mode(OperationMode.Auto)
assert mode() == OperationMode.Auto
self.device.set_mode(OperationMode.Favorite)
assert mode() == OperationMode.Favorite
self.device.set_mode(OperationMode.Idle)
assert mode() == OperationMode.Idle
def test_set_favorite_level(self):
def favorite_level():
return self.device.status().favorite_level
self.device.set_favorite_level(0)
assert favorite_level() == 0
self.device.set_favorite_level(6)
assert favorite_level() == 6
self.device.set_favorite_level(10)
assert favorite_level() == 10
with pytest.raises(AirPurifierException):
self.device.set_favorite_level(-1)
with pytest.raises(AirPurifierException):
self.device.set_favorite_level(18)
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_led(self):
def led():
return self.device.status().led
self.device.set_led(True)
assert led() is True
self.device.set_led(False)
assert led() is False
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_set_volume(self):
def volume():
return self.device.status().volume
self.device.set_volume(0)
assert volume() == 0
self.device.set_volume(35)
assert volume() == 35
self.device.set_volume(100)
assert volume() == 100
with pytest.raises(AirPurifierException):
self.device.set_volume(-1)
with pytest.raises(AirPurifierException):
self.device.set_volume(101)
def test_set_learn_mode(self):
def learn_mode():
return self.device.status().learn_mode
self.device.set_learn_mode(True)
assert learn_mode() is True
self.device.set_learn_mode(False)
assert learn_mode() is False
def test_set_auto_detect(self):
def auto_detect():
return self.device.status().auto_detect
self.device.set_auto_detect(True)
assert auto_detect() is True
self.device.set_auto_detect(False)
assert auto_detect() is False
def test_set_extra_features(self):
def extra_features():
return self.device.status().extra_features
self.device.set_extra_features(0)
assert extra_features() == 0
self.device.set_extra_features(1)
assert extra_features() == 1
self.device.set_extra_features(2)
assert extra_features() == 2
with pytest.raises(AirPurifierException):
self.device.set_extra_features(-1)
def test_reset_filter(self):
def filter_hours_used():
return self.device.status().filter_hours_used
def filter_life_remaining():
return self.device.status().filter_life_remaining
self.device._reset_state()
assert filter_hours_used() != 0
assert filter_life_remaining() != 100
self.device.reset_filter()
assert filter_hours_used() == 0
assert filter_life_remaining() == 100
def test_status_without_volume(self):
self.device._reset_state()
# The Air Purifier 2 doesn't support volume
self.device.state["volume"] = None
assert self.state().volume is None
def test_status_without_led_brightness(self):
self.device._reset_state()
# The Air Purifier Pro doesn't support LED brightness
self.device.state["led_b"] = None
assert self.state().led_brightness is None
def test_status_unknown_led_brightness(self):
self.device._reset_state()
# The Air Purifier V3 returns a led brightness of 10 f.e.
self.device.state["led_b"] = 10
assert self.state().led_brightness is None
def test_status_without_temperature(self):
self.device._reset_state()
self.device.state["temp_dec"] = None
assert self.state().temperature is None
def test_status_without_illuminance(self):
self.device._reset_state()
# The Air Purifier 2 doesn't provide illuminance
self.device.state["bright"] = None
assert self.state().illuminance is None
def test_status_without_buzzer(self):
self.device._reset_state()
# The Air Purifier Pro doesn't provide the buzzer property
self.device.state["buzzer"] = None
assert self.state().buzzer is None
def test_status_without_motor2_speed(self):
self.device._reset_state()
# The Air Purifier Pro doesn't provide the buzzer property
self.device.state["motor2_speed"] = None
assert self.state().motor2_speed is None
def test_status_without_filter_rfid_tag(self):
self.device._reset_state()
self.device.state["rfid_tag"] = None
assert self.state().filter_rfid_tag is None
assert self.state().filter_type is None
def test_status_with_filter_rfid_tag_zeros(self):
self.device._reset_state()
self.device.state["rfid_tag"] = "0:0:0:0:0:0:0"
assert self.state().filter_type is FilterType.Unknown
def test_status_without_filter_rfid_product_id(self):
self.device._reset_state()
self.device.state["rfid_product_id"] = None
assert self.state().filter_type is FilterType.Regular
def test_status_filter_rfid_product_ids(self):
self.device._reset_state()
self.device.state["rfid_product_id"] = "0:0:30:31"
assert self.state().filter_type is FilterType.AntiFormaldehyde
self.device.state["rfid_product_id"] = "0:0:30:32"
assert self.state().filter_type is FilterType.Regular
self.device.state["rfid_product_id"] = "0:0:41:30"
assert self.state().filter_type is FilterType.AntiBacterial
def test_status_without_sleep_mode(self):
self.device._reset_state()
self.device.state["sleep_mode"] = None
assert self.state().sleep_mode is None
def test_status_without_app_extra(self):
self.device._reset_state()
self.device.state["app_extra"] = None
assert self.state().extra_features is None
assert self.state().turbo_mode_supported is None
def test_status_without_auto_detect(self):
self.device._reset_state()
self.device.state["act_det"] = None
assert self.state().auto_detect is None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584295509.0
python-miio-0.5.0.1/miio/tests/test_airpurifier_miot.py 0000644 0001750 0001750 00000015007 00000000000 022650 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import AirPurifierMiot
from miio.airfilter_util import FilterType
from miio.airpurifier_miot import AirPurifierMiotException, LedBrightness, OperationMode
from .dummies import DummyMiotDevice
_INITIAL_STATE = {
"power": True,
"aqi": 10,
"average_aqi": 8,
"humidity": 62,
"temperature": 18.6,
"fan_level": 2,
"mode": 0,
"led": True,
"led_brightness": 1,
"buzzer": False,
"buzzer_volume": 0,
"child_lock": False,
"favorite_level": 10,
"filter_life_remaining": 80,
"filter_hours_used": 682,
"use_time": 2457000,
"purify_volume": 25262,
"motor_speed": 354,
"filter_rfid_product_id": "0:0:41:30",
"filter_rfid_tag": "10:20:30:40:50:60:7",
"button_pressed": "power",
}
class DummyAirPurifierMiot(DummyMiotDevice, AirPurifierMiot):
def __init__(self, *args, **kwargs):
self.state = _INITIAL_STATE
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_mode": lambda x: self._set_state("mode", x),
"set_led": lambda x: self._set_state("led", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_level_favorite": lambda x: self._set_state("favorite_level", x),
"set_led_b": lambda x: self._set_state("led_b", x),
"set_volume": lambda x: self._set_state("volume", x),
"set_act_sleep": lambda x: self._set_state("act_sleep", x),
"reset_filter1": lambda x: (
self._set_state("f1_hour_used", [0]),
self._set_state("filter1_life", [100]),
),
"set_act_det": lambda x: self._set_state("act_det", x),
"set_app_extra": lambda x: self._set_state("app_extra", x),
}
super().__init__(*args, **kwargs)
@pytest.fixture(scope="function")
def airpurifier(request):
request.cls.device = DummyAirPurifierMiot()
@pytest.mark.usefixtures("airpurifier")
class TestAirPurifier(TestCase):
def test_on(self):
self.device.off() # ensure off
assert self.device.status().is_on is False
self.device.on()
assert self.device.status().is_on is True
def test_off(self):
self.device.on() # ensure on
assert self.device.status().is_on is True
self.device.off()
assert self.device.status().is_on is False
def test_status(self):
status = self.device.status()
assert status.is_on is _INITIAL_STATE["power"]
assert status.aqi == _INITIAL_STATE["aqi"]
assert status.average_aqi == _INITIAL_STATE["average_aqi"]
assert status.humidity == _INITIAL_STATE["humidity"]
assert status.temperature == _INITIAL_STATE["temperature"]
assert status.fan_level == _INITIAL_STATE["fan_level"]
assert status.mode == OperationMode(_INITIAL_STATE["mode"])
assert status.led == _INITIAL_STATE["led"]
assert status.led_brightness == LedBrightness(_INITIAL_STATE["led_brightness"])
assert status.buzzer == _INITIAL_STATE["buzzer"]
assert status.child_lock == _INITIAL_STATE["child_lock"]
assert status.favorite_level == _INITIAL_STATE["favorite_level"]
assert status.filter_life_remaining == _INITIAL_STATE["filter_life_remaining"]
assert status.filter_hours_used == _INITIAL_STATE["filter_hours_used"]
assert status.use_time == _INITIAL_STATE["use_time"]
assert status.purify_volume == _INITIAL_STATE["purify_volume"]
assert status.motor_speed == _INITIAL_STATE["motor_speed"]
assert status.filter_rfid_product_id == _INITIAL_STATE["filter_rfid_product_id"]
assert status.filter_type == FilterType.AntiBacterial
def test_set_fan_level(self):
def fan_level():
return self.device.status().fan_level
self.device.set_fan_level(1)
assert fan_level() == 1
self.device.set_fan_level(2)
assert fan_level() == 2
self.device.set_fan_level(3)
assert fan_level() == 3
with pytest.raises(AirPurifierMiotException):
self.device.set_fan_level(0)
with pytest.raises(AirPurifierMiotException):
self.device.set_fan_level(4)
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Auto)
assert mode() == OperationMode.Auto
self.device.set_mode(OperationMode.Silent)
assert mode() == OperationMode.Silent
self.device.set_mode(OperationMode.Favorite)
assert mode() == OperationMode.Favorite
self.device.set_mode(OperationMode.Fan)
assert mode() == OperationMode.Fan
def test_set_favorite_level(self):
def favorite_level():
return self.device.status().favorite_level
self.device.set_favorite_level(0)
assert favorite_level() == 0
self.device.set_favorite_level(6)
assert favorite_level() == 6
self.device.set_favorite_level(14)
assert favorite_level() == 14
with pytest.raises(AirPurifierMiotException):
self.device.set_favorite_level(-1)
with pytest.raises(AirPurifierMiotException):
self.device.set_favorite_level(15)
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_led(self):
def led():
return self.device.status().led
self.device.set_led(True)
assert led() is True
self.device.set_led(False)
assert led() is False
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_airqualitymonitor.py 0000644 0001750 0001750 00000013511 00000000000 023071 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import AirQualityMonitor
from miio.airqualitymonitor import (
MODEL_AIRQUALITYMONITOR_B1,
MODEL_AIRQUALITYMONITOR_S1,
MODEL_AIRQUALITYMONITOR_V1,
AirQualityMonitorStatus,
)
from .dummies import DummyDevice
class DummyAirQualityMonitorV1(DummyDevice, AirQualityMonitor):
def __init__(self, *args, **kwargs):
self.model = MODEL_AIRQUALITYMONITOR_V1
self.state = {
"power": "on",
"aqi": 34,
"battery": 100,
"usb_state": "off",
"time_state": "on",
"night_state": "on",
"night_beg_time": "format unknown",
"night_end_time": "format unknown",
"sensor_state": "format unknown",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_time_state": lambda x: self._set_state("time_state", x),
"set_night_state": lambda x: self._set_state("night_state", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def airqualitymonitorv1(request):
request.cls.device = DummyAirQualityMonitorV1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airqualitymonitorv1")
class TestAirQualityMonitorV1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(
AirQualityMonitorStatus(self.device.start_state)
)
assert self.is_on() is True
assert self.state().aqi == self.device.start_state["aqi"]
assert self.state().battery == self.device.start_state["battery"]
assert self.state().usb_power is (self.device.start_state["usb_state"] == "on")
assert self.state().display_clock is (
self.device.start_state["time_state"] == "on"
)
assert self.state().night_mode is (
self.device.start_state["night_state"] == "on"
)
class DummyAirQualityMonitorS1(DummyDevice, AirQualityMonitor):
def __init__(self, *args, **kwargs):
self.model = MODEL_AIRQUALITYMONITOR_S1
self.state = {
"battery": 100,
"co2": 695,
"humidity": 62.1,
"pm25": 19.4,
"temperature": 27.4,
"tvoc": 254,
}
self.return_values = {"get_prop": self._get_state}
super().__init__(args, kwargs)
def _get_state(self, props):
"""Return wanted properties"""
return self.state
@pytest.fixture(scope="class")
def airqualitymonitors1(request):
request.cls.device = DummyAirQualityMonitorS1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airqualitymonitors1")
class TestAirQualityMonitorS1(TestCase):
def state(self):
return self.device.status()
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(
AirQualityMonitorStatus(self.device.start_state)
)
assert self.state().battery == self.device.start_state["battery"]
assert self.state().co2 == self.device.start_state["co2"]
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().pm25 == self.device.start_state["pm25"]
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().tvoc == self.device.start_state["tvoc"]
assert self.state().aqi is None
assert self.state().usb_power is None
assert self.state().display_clock is None
assert self.state().night_mode is None
class DummyAirQualityMonitorB1(DummyDevice, AirQualityMonitor):
def __init__(self, *args, **kwargs):
self.model = MODEL_AIRQUALITYMONITOR_B1
self.state = {
"co2e": 1466,
"humidity": 59.79999923706055,
"pm25": 2,
"temperature": 19.799999237060547,
"temperature_unit": "c",
"tvoc": 1.3948699235916138,
"tvoc_unit": "mg_m3",
}
self.return_values = {"get_air_data": self._get_state}
super().__init__(args, kwargs)
def _get_state(self, props):
"""Return wanted properties"""
return self.state
@pytest.fixture(scope="class")
def airqualitymonitorb1(request):
request.cls.device = DummyAirQualityMonitorB1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("airqualitymonitorb1")
class TestAirQualityMonitorB1(TestCase):
def state(self):
return self.device.status()
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(
AirQualityMonitorStatus(self.device.start_state)
)
assert self.state().power is None
assert self.state().usb_power is None
assert self.state().battery is None
assert self.state().aqi is None
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().co2 is None
assert self.state().co2e == self.device.start_state["co2e"]
assert self.state().pm25 == self.device.start_state["pm25"]
assert self.state().tvoc == self.device.start_state["tvoc"]
assert self.state().display_clock is None
assert self.state().night_mode is None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_ceil.py 0000644 0001750 0001750 00000014304 00000000000 020212 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import Ceil
from miio.ceil import CeilException, CeilStatus
from .dummies import DummyDevice
class DummyCeil(DummyDevice, Ceil):
def __init__(self, *args, **kwargs):
self.state = {
"power": "on",
"bright": 50,
"snm": 4,
"dv": 0,
"cctsw": [[0, 3], [0, 2], [0, 1]],
"bl": 1,
"mb": 1,
"ac": 1,
"mssw": 1,
"cct": 99,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_bright": lambda x: self._set_state("bright", x),
"apply_fixed_scene": lambda x: self._set_state("snm", x),
"delay_off": lambda x: self._set_state("dv", x),
"enable_bl": lambda x: self._set_state("bl", x),
"enable_ac": lambda x: self._set_state("ac", x),
"set_cct": lambda x: self._set_state("cct", x),
"set_bricct": lambda x: (
self._set_state("bright", [x[0]]),
self._set_state("cct", [x[1]]),
),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def ceil(request):
request.cls.device = DummyCeil()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("ceil")
class TestCeil(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(CeilStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().brightness == self.device.start_state["bright"]
assert self.state().color_temperature == self.device.start_state["cct"]
assert self.state().scene == self.device.start_state["snm"]
assert self.state().delay_off_countdown == self.device.start_state["dv"]
assert self.state().smart_night_light is (self.device.start_state["bl"] == 1)
assert self.state().automatic_color_temperature is (
self.device.start_state["ac"] == 1
)
def test_set_brightness(self):
def brightness():
return self.device.status().brightness
self.device.set_brightness(10)
assert brightness() == 10
self.device.set_brightness(20)
assert brightness() == 20
with pytest.raises(CeilException):
self.device.set_brightness(-1)
with pytest.raises(CeilException):
self.device.set_brightness(101)
def test_set_color_temperature(self):
def color_temperature():
return self.device.status().color_temperature
self.device.set_color_temperature(30)
assert color_temperature() == 30
self.device.set_color_temperature(20)
assert color_temperature() == 20
with pytest.raises(CeilException):
self.device.set_color_temperature(-1)
with pytest.raises(CeilException):
self.device.set_color_temperature(101)
def test_set_brightness_and_color_temperature(self):
def color_temperature():
return self.device.status().color_temperature
def brightness():
return self.device.status().brightness
self.device.set_brightness_and_color_temperature(20, 21)
assert brightness() == 20
assert color_temperature() == 21
self.device.set_brightness_and_color_temperature(31, 30)
assert brightness() == 31
assert color_temperature() == 30
self.device.set_brightness_and_color_temperature(10, 11)
assert brightness() == 10
assert color_temperature() == 11
with pytest.raises(CeilException):
self.device.set_brightness_and_color_temperature(-1, 10)
with pytest.raises(CeilException):
self.device.set_brightness_and_color_temperature(10, -1)
with pytest.raises(CeilException):
self.device.set_brightness_and_color_temperature(0, 10)
with pytest.raises(CeilException):
self.device.set_brightness_and_color_temperature(10, 0)
with pytest.raises(CeilException):
self.device.set_brightness_and_color_temperature(101, 10)
with pytest.raises(CeilException):
self.device.set_brightness_and_color_temperature(10, 101)
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(CeilException):
self.device.delay_off(0)
with pytest.raises(CeilException):
self.device.delay_off(-1)
def test_set_scene(self):
def scene():
return self.device.status().scene
self.device.set_scene(1)
assert scene() == 1
self.device.set_scene(4)
assert scene() == 4
with pytest.raises(CeilException):
self.device.set_scene(0)
with pytest.raises(CeilException):
self.device.set_scene(5)
def test_smart_night_light_on(self):
def smart_night_light():
return self.device.status().smart_night_light
self.device.smart_night_light_off()
assert smart_night_light() is False
self.device.smart_night_light_on()
assert smart_night_light() is True
def test_automatic_color_temperature_on(self):
def automatic_color_temperature():
return self.device.status().automatic_color_temperature
self.device.automatic_color_temperature_on()
assert automatic_color_temperature() is True
self.device.automatic_color_temperature_off()
assert automatic_color_temperature() is False
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1548870856.0
python-miio-0.5.0.1/miio/tests/test_chuangmi_ir.json 0000644 0001750 0001750 00000012505 00000000000 022105 0 ustar 00tpr tpr 0000000 0000000 {
"test_raw_ok": [
{
"in": [
"Z6VPAAUCAABgAgAAxQYAAOUIAACUEQAAqyIAADSeAABwdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAgECAQEBAQIBAgECAgICBgNXA1cDUA"
],
"out": [
"Z6VPAAUCAABgAgAAxQYAAOUIAACUEQAAqyIAADSeAABwdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAgECAQEBAQIBAgECAgICBgNXA1cDUA",
38400
]
},
{
"in": [
"Z6VPAAUCAABgAgAAxQYAAOUIAACUEQAAqyIAADSeAABwdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAgECAQEBAQIBAgECAgICBgNXA1cDUA",
19200
],
"out": [
"Z6VPAAUCAABgAgAAxQYAAOUIAACUEQAAqyIAADSeAABwdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAgECAQEBAQIBAgECAgICBgNXA1cDUA",
19200
]
}
],
"test_pronto_ok": [
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - with spaces",
"in": [
"0000 006C 0022 0002 015B 00AD 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0016 0016 0016 0016 0041 0016 0016 0016 0041 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0016 0016 0041 0016 0016 0016 0041 0016 0041 0016 0041 0016 0041 0016 0622 015B 0057 0016 0E6C"
],
"out": [
"Z6VHAD0CAACdBgAA2ggAAJsRAABQIwAAyZ8AAMF3AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBQJGA=",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - without spaces",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C"
],
"out": [
"Z6VHAD0CAACdBgAA2ggAAJsRAABQIwAAyZ8AAMF3AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBQJGA=",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 0 repeat frames",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
0
],
"out": [
"Z6VDAD0CAACdBgAAmxEAAFAjAADJnwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBA",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 2 repeat frames",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
2
],
"out": [
"Z6VLAD0CAACdBgAA2ggAAJsRAABQIwAAyZ8AAMF3AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBQJGAkYA==",
38381
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0"
],
"out": [
"Z6VTAFoCAAC0BAAAaAkAAJBGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAQAAAAAAAAAAAAAAAAAAAAAAADACAQAAAAAAAAAAAAAAAAAAAAAAADA=",
39857
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 0 repeats",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
0
],
"out": [
"Z6UpAFoCAAC0BAAAaAkAAJBGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAQAAAAAAAAAAAAAAAAAAAAAAADA=",
39857
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 2 repeats",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
2
],
"out": [
"Z6V9AFoCAAC0BAAAaAkAAJBGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAQAAAAAAAAAAAAAAAAAAAAAAADACAQAAAAAAAAAAAAAAAAAAAAAAADACAQAAAAAAAAAAAAAAAAAAAAAAADA=",
39857
]
}
],
"test_pronto_exception": [
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - invalid repeats value",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
-1
]
},
{
"desc": "Invalid pronto command",
"in": [
"FFFFFFFFFFFF",
0
]
}
]
} ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_chuangmi_ir.py 0000644 0001750 0001750 00000010462 00000000000 021564 0 ustar 00tpr tpr 0000000 0000000 import base64
import json
import os
from unittest import TestCase
import pytest
from miio import ChuangmiIr
from miio.chuangmi_ir import ChuangmiIrException
from .dummies import DummyDevice
with open(os.path.join(os.path.dirname(__file__), "test_chuangmi_ir.json")) as inp:
test_data = json.load(inp)
class DummyChuangmiIr(DummyDevice, ChuangmiIr):
def __init__(self, *args, **kwargs):
self.state = {"last_ir_played": None}
self.return_values = {
"miIO.ir_learn": lambda x: True,
"miIO.ir_read": lambda x: True,
"miIO.ir_play": self._ir_play_input_validation,
}
super().__init__(args, kwargs)
def _ir_play_input_validation(self, payload):
try:
base64.b64decode(payload["code"])
self._set_state("last_ir_played", [[payload["code"], payload.get("freq")]])
return True
except TypeError:
return False
@pytest.fixture(scope="class")
def chuangmiir(request):
request.cls.device = DummyChuangmiIr()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("chuangmiir")
class TestChuangmiIr(TestCase):
def test_learn(self):
assert self.device.learn() is True
assert self.device.learn(30) is True
with pytest.raises(ChuangmiIrException):
self.device.learn(-1)
with pytest.raises(ChuangmiIrException):
self.device.learn(1000001)
def test_read(self):
assert self.device.read() is True
assert self.device.read(30) is True
with pytest.raises(ChuangmiIrException):
self.device.read(-1)
with pytest.raises(ChuangmiIrException):
self.device.read(1000001)
def test_play_raw(self):
for args in test_data["test_raw_ok"]:
with self.subTest():
self.device._reset_state()
self.assertTrue(self.device.play_raw(*args["in"]))
self.assertSequenceEqual(
self.device.state["last_ir_played"], args["out"]
)
def test_pronto_to_raw(self):
for args in test_data["test_pronto_ok"]:
with self.subTest():
self.assertSequenceEqual(
ChuangmiIr.pronto_to_raw(*args["in"]), args["out"]
)
for args in test_data["test_pronto_exception"]:
with self.subTest():
with pytest.raises(ChuangmiIrException):
ChuangmiIr.pronto_to_raw(*args["in"])
def test_play_pronto(self):
for args in test_data["test_pronto_ok"]:
with self.subTest():
self.device._reset_state()
self.assertTrue(self.device.play_pronto(*args["in"]))
self.assertSequenceEqual(
self.device.state["last_ir_played"], args["out"]
)
for args in test_data["test_pronto_exception"]:
with pytest.raises(ChuangmiIrException):
self.device.play_pronto(*args["in"])
def test_play_auto(self):
for args in test_data["test_raw_ok"] + test_data["test_pronto_ok"]:
if len(args["in"]) > 1: # autodetect doesn't take any extra args
continue
with self.subTest():
self.device._reset_state()
self.assertTrue(self.device.play(*args["in"]))
self.assertSequenceEqual(
self.device.state["last_ir_played"], args["out"]
)
def test_play_with_type(self):
for type_, tests in [
("raw", test_data["test_raw_ok"]),
("pronto", test_data["test_pronto_ok"]),
]:
for args in tests:
with self.subTest():
command = "{}:{}".format(type_, ":".join(map(str, args["in"])))
self.assertTrue(self.device.play(command))
self.assertSequenceEqual(
self.device.state["last_ir_played"], args["out"]
)
with pytest.raises(ChuangmiIrException):
self.device.play("invalid:command")
with pytest.raises(ChuangmiIrException):
self.device.play("pronto:command:invalid:argument:count")
with pytest.raises(ChuangmiIrException):
self.device.play("pronto:command:invalidargument")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_chuangmi_plug.py 0000644 0001750 0001750 00000015016 00000000000 022121 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import ChuangmiPlug
from miio.chuangmi_plug import (
MODEL_CHUANGMI_PLUG_M1,
MODEL_CHUANGMI_PLUG_V1,
MODEL_CHUANGMI_PLUG_V3,
ChuangmiPlugStatus,
)
from .dummies import DummyDevice
class DummyChuangmiPlugV1(DummyDevice, ChuangmiPlug):
def __init__(self, *args, **kwargs):
self.model = MODEL_CHUANGMI_PLUG_V1
self.state = {"on": True, "usb_on": True, "temperature": 32}
self.return_values = {
"get_prop": self._get_state,
"set_on": lambda x: self._set_state_basic("on", True),
"set_off": lambda x: self._set_state_basic("on", False),
"set_usb_on": lambda x: self._set_state_basic("usb_on", True),
"set_usb_off": lambda x: self._set_state_basic("usb_on", False),
}
self.start_state = self.state.copy()
super().__init__(args, kwargs)
def _set_state_basic(self, var, value):
"""Set a state of a variable"""
self.state[var] = value
@pytest.fixture(scope="class")
def chuangmiplugv1(request):
request.cls.device = DummyChuangmiPlugV1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("chuangmiplugv1")
class TestChuangmiPlugV1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(ChuangmiPlugStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().usb_power is True
assert self.state().temperature == self.device.start_state["temperature"]
def test_usb_on(self):
self.device.usb_off() # ensure off
assert self.device.status().usb_power is False
self.device.usb_on()
assert self.device.status().usb_power is True
def test_usb_off(self):
self.device.usb_on() # ensure on
assert self.device.status().usb_power is True
self.device.usb_off()
assert self.device.status().usb_power is False
class DummyChuangmiPlugV3(DummyDevice, ChuangmiPlug):
def __init__(self, *args, **kwargs):
self.model = MODEL_CHUANGMI_PLUG_V3
self.state = {"on": True, "usb_on": True, "temperature": 32, "wifi_led": "off"}
self.return_values = {
"get_prop": self._get_state,
"get_power": self._get_load_power,
"set_power": lambda x: self._set_state_basic("on", x == ["on"]),
"set_usb_on": lambda x: self._set_state_basic("usb_on", True),
"set_usb_off": lambda x: self._set_state_basic("usb_on", False),
"set_wifi_led": lambda x: self._set_state("wifi_led", x),
}
self.start_state = self.state.copy()
super().__init__(args, kwargs)
def _set_state_basic(self, var, value):
"""Set a state of a variable"""
self.state[var] = value
def _get_load_power(self, props=None):
"""Return load power"""
return [300]
@pytest.fixture(scope="class")
def chuangmiplugv3(request):
request.cls.device = DummyChuangmiPlugV3()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("chuangmiplugv3")
class TestChuangmiPlugV3(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
load_power = float(self.device._get_load_power().pop(0) * 0.01)
start_state_extended = self.device.start_state.copy()
start_state_extended["load_power"] = load_power
assert repr(self.state()) == repr(ChuangmiPlugStatus(start_state_extended))
assert self.is_on() is True
assert self.state().usb_power is True
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().load_power == load_power
def test_usb_on(self):
self.device.usb_off() # ensure off
assert self.device.status().usb_power is False
self.device.usb_on()
assert self.device.status().usb_power is True
def test_usb_off(self):
self.device.usb_on() # ensure on
assert self.device.status().usb_power is True
self.device.usb_off()
assert self.device.status().usb_power is False
def test_set_wifi_led(self):
def wifi_led():
return self.device.status().wifi_led
self.device.set_wifi_led(True)
assert wifi_led() is True
self.device.set_wifi_led(False)
assert wifi_led() is False
class DummyChuangmiPlugM1(DummyDevice, ChuangmiPlug):
def __init__(self, *args, **kwargs):
self.model = MODEL_CHUANGMI_PLUG_M1
self.state = {"power": "on", "temperature": 32}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def chuangmiplugm1(request):
request.cls.device = DummyChuangmiPlugM1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("chuangmiplugm1")
class TestChuangmiPlugM1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(ChuangmiPlugStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temperature"]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_click_common.py 0000644 0001750 0001750 00000000336 00000000000 021733 0 ustar 00tpr tpr 0000000 0000000 from miio.click_common import validate_ip, validate_token
def test_validate_token_empty():
assert not validate_token(None, None, None)
def test_validate_ip_empty():
assert validate_ip(None, None, None) is None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_fan.py 0000644 0001750 0001750 00000072266 00000000000 020055 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import Fan, FanP5
from miio.fan import (
MODEL_FAN_P5,
MODEL_FAN_SA1,
MODEL_FAN_V2,
MODEL_FAN_V3,
FanException,
FanStatus,
FanStatusP5,
LedBrightness,
MoveDirection,
OperationMode,
)
from .dummies import DummyDevice
class DummyFanV2(DummyDevice, Fan):
def __init__(self, *args, **kwargs):
self.model = MODEL_FAN_V2
# This example response is just a guess. Please update!
self.state = {
"temp_dec": 232,
"humidity": 46,
"angle": 118,
"speed": 298,
"poweroff_time": 0,
"power": "on",
"ac_power": "off",
"battery": 98,
"angle_enable": "off",
"speed_level": 1,
"natural_level": 0,
"child_lock": "off",
"buzzer": "on",
"led_b": 1,
"led": "on",
"natural_enable": None,
"use_time": 0,
"bat_charge": "complete",
"bat_state": None,
"button_pressed": "speed",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_speed_level": lambda x: self._set_state("speed_level", x),
"set_natural_level": lambda x: self._set_state("natural_level", x),
"set_move": lambda x: True,
"set_angle": lambda x: self._set_state("angle", x),
"set_angle_enable": lambda x: self._set_state("angle_enable", x),
"set_led_b": lambda x: self._set_state("led_b", x),
"set_led": lambda x: self._set_state("led", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_poweroff_time": lambda x: self._set_state("poweroff_time", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def fanv2(request):
request.cls.device = DummyFanV2()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("fanv2")
class TestFanV2(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(FanStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temp_dec"] / 10.0
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().angle == self.device.start_state["angle"]
assert self.state().speed == self.device.start_state["speed"]
assert (
self.state().delay_off_countdown == self.device.start_state["poweroff_time"]
)
assert self.state().ac_power is (self.device.start_state["ac_power"] == "on")
assert self.state().battery == self.device.start_state["battery"]
assert self.state().oscillate is (
self.device.start_state["angle_enable"] == "on"
)
assert self.state().direct_speed == self.device.start_state["speed_level"]
assert self.state().natural_speed == self.device.start_state["natural_level"]
assert self.state().child_lock is (
self.device.start_state["child_lock"] == "on"
)
assert self.state().buzzer is (self.device.start_state["buzzer"] == "on")
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_b"]
)
assert self.state().led is (self.device.start_state["led"] == "on")
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().battery_charge == self.device.start_state["bat_charge"]
assert self.state().battery_state == self.device.start_state["bat_state"]
assert self.state().button_pressed == self.device.start_state["button_pressed"]
def test_status_without_led_brightness(self):
self.device._reset_state()
self.device.state["led_b"] = None
assert self.state().led_brightness is None
def test_status_without_battery_charge(self):
self.device._reset_state()
self.device.state["bat_charge"] = None
assert self.state().battery_charge is None
def test_status_without_battery_state(self):
self.device._reset_state()
self.device.state["bat_state"] = None
assert self.state().battery_state is None
def test_status_without_button_pressed(self):
self.device._reset_state()
self.device.state["button_pressed"] = None
assert self.state().button_pressed is None
def test_set_led(self):
def led():
return self.device.status().led
self.device.set_led(True)
assert led() is True
self.device.set_led(False)
assert led() is False
def test_set_direct_speed(self):
def direct_speed():
return self.device.status().direct_speed
self.device.set_direct_speed(0)
assert direct_speed() == 0
self.device.set_direct_speed(1)
assert direct_speed() == 1
self.device.set_direct_speed(100)
assert direct_speed() == 100
with pytest.raises(FanException):
self.device.set_direct_speed(-1)
with pytest.raises(FanException):
self.device.set_direct_speed(101)
def test_set_rotate(self):
"""The method is open-loop. The new state cannot be retrieved."""
self.device.set_rotate(MoveDirection.Left)
self.device.set_rotate(MoveDirection.Right)
def test_set_angle(self):
"""This test doesn't implement the real behaviour of the device may be.
The property "angle" doesn't provide the current setting.
It's a measurement of the current position probably.
"""
def angle():
return self.device.status().angle
self.device.set_angle(0) # TODO: Is this value allowed?
assert angle() == 0
self.device.set_angle(1) # TODO: Is this value allowed?
assert angle() == 1
self.device.set_angle(30)
assert angle() == 30
self.device.set_angle(60)
assert angle() == 60
self.device.set_angle(90)
assert angle() == 90
self.device.set_angle(120)
assert angle() == 120
with pytest.raises(FanException):
self.device.set_angle(-1)
with pytest.raises(FanException):
self.device.set_angle(121)
def test_set_oscillate(self):
def oscillate():
return self.device.status().oscillate
self.device.set_oscillate(True)
assert oscillate() is True
self.device.set_oscillate(False)
assert oscillate() is False
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(FanException):
self.device.delay_off(-1)
with pytest.raises(FanException):
self.device.delay_off(0)
class DummyFanV3(DummyDevice, Fan):
def __init__(self, *args, **kwargs):
self.model = MODEL_FAN_V3
self.state = {
"temp_dec": 232,
"humidity": 46,
"angle": 118,
"speed": 298,
"poweroff_time": 0,
"power": "on",
"ac_power": "off",
"battery": 98,
"angle_enable": "off",
"speed_level": 1,
"natural_level": 0,
"child_lock": "off",
"buzzer": "on",
"led_b": 1,
"led": None,
"natural_enable": None,
"use_time": 0,
"bat_charge": "complete",
"bat_state": None,
"button_pressed": "speed",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_speed_level": lambda x: self._set_state("speed_level", x),
"set_natural_level": lambda x: self._set_state("natural_level", x),
"set_move": lambda x: True,
"set_angle": lambda x: self._set_state("angle", x),
"set_angle_enable": lambda x: self._set_state("angle_enable", x),
"set_led_b": lambda x: self._set_state("led_b", x),
"set_led": lambda x: self._set_state("led", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_poweroff_time": lambda x: self._set_state("poweroff_time", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def fanv3(request):
request.cls.device = DummyFanV3()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("fanv3")
class TestFanV3(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(FanStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().temperature == self.device.start_state["temp_dec"] / 10.0
assert self.state().humidity == self.device.start_state["humidity"]
assert self.state().angle == self.device.start_state["angle"]
assert self.state().speed == self.device.start_state["speed"]
assert (
self.state().delay_off_countdown == self.device.start_state["poweroff_time"]
)
assert self.state().ac_power is (self.device.start_state["ac_power"] == "on")
assert self.state().battery == self.device.start_state["battery"]
assert self.state().oscillate is (
self.device.start_state["angle_enable"] == "on"
)
assert self.state().direct_speed == self.device.start_state["speed_level"]
assert self.state().natural_speed == self.device.start_state["natural_level"]
assert self.state().child_lock is (
self.device.start_state["child_lock"] == "on"
)
assert self.state().buzzer is (self.device.start_state["buzzer"] == "on")
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_b"]
)
assert self.state().led is None
assert self.state().use_time == self.device.start_state["use_time"]
assert self.state().battery_charge == self.device.start_state["bat_charge"]
assert self.state().battery_state == self.device.start_state["bat_state"]
assert self.state().button_pressed == self.device.start_state["button_pressed"]
def test_status_without_led_brightness(self):
self.device._reset_state()
self.device.state["led_b"] = None
assert self.state().led_brightness is None
def test_status_without_battery_charge(self):
self.device._reset_state()
self.device.state["bat_charge"] = None
assert self.state().battery_charge is None
def test_status_without_battery_state(self):
self.device._reset_state()
self.device.state["bat_state"] = None
assert self.state().battery_state is None
def test_status_without_button_pressed(self):
self.device._reset_state()
self.device.state["button_pressed"] = None
assert self.state().button_pressed is None
def test_set_direct_speed(self):
def direct_speed():
return self.device.status().direct_speed
self.device.set_direct_speed(0)
assert direct_speed() == 0
self.device.set_direct_speed(1)
assert direct_speed() == 1
self.device.set_direct_speed(100)
assert direct_speed() == 100
with pytest.raises(FanException):
self.device.set_direct_speed(-1)
with pytest.raises(FanException):
self.device.set_direct_speed(101)
def test_set_natural_speed(self):
def natural_speed():
return self.device.status().natural_speed
self.device.set_natural_speed(0)
assert natural_speed() == 0
self.device.set_natural_speed(1)
assert natural_speed() == 1
self.device.set_natural_speed(100)
assert natural_speed() == 100
with pytest.raises(FanException):
self.device.set_natural_speed(-1)
with pytest.raises(FanException):
self.device.set_natural_speed(101)
def test_set_rotate(self):
"""The method is open-loop. The new state cannot be retrieved."""
self.device.set_rotate(MoveDirection.Left)
self.device.set_rotate(MoveDirection.Right)
def test_set_angle(self):
"""This test doesn't implement the real behaviour of the device may be.
The property "angle" doesn't provide the current setting.
It's a measurement of the current position probably.
"""
def angle():
return self.device.status().angle
self.device.set_angle(0) # TODO: Is this value allowed?
assert angle() == 0
self.device.set_angle(1) # TODO: Is this value allowed?
assert angle() == 1
self.device.set_angle(30)
assert angle() == 30
self.device.set_angle(60)
assert angle() == 60
self.device.set_angle(90)
assert angle() == 90
self.device.set_angle(120)
assert angle() == 120
with pytest.raises(FanException):
self.device.set_angle(-1)
with pytest.raises(FanException):
self.device.set_angle(121)
def test_set_oscillate(self):
def oscillate():
return self.device.status().oscillate
self.device.set_oscillate(True)
assert oscillate() is True
self.device.set_oscillate(False)
assert oscillate() is False
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(FanException):
self.device.delay_off(-1)
with pytest.raises(FanException):
self.device.delay_off(0)
class DummyFanSA1(DummyDevice, Fan):
def __init__(self, *args, **kwargs):
self.model = MODEL_FAN_SA1
self.state = {
"angle": 120,
"speed": 277,
"poweroff_time": 0,
"power": "on",
"ac_power": "on",
"angle_enable": "off",
"speed_level": 1,
"natural_level": 2,
"child_lock": "off",
"buzzer": 0,
"led_b": 0,
"use_time": 2318,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_speed_level": lambda x: self._set_state("speed_level", x),
"set_natural_level": lambda x: self._set_state("natural_level", x),
"set_move": lambda x: True,
"set_angle": lambda x: self._set_state("angle", x),
"set_angle_enable": lambda x: self._set_state("angle_enable", x),
"set_led_b": lambda x: self._set_state("led_b", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_poweroff_time": lambda x: self._set_state("poweroff_time", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def fansa1(request):
request.cls.device = DummyFanSA1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("fansa1")
class TestFanSA1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(FanStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().angle == self.device.start_state["angle"]
assert self.state().speed == self.device.start_state["speed"]
assert (
self.state().delay_off_countdown == self.device.start_state["poweroff_time"]
)
assert self.state().ac_power is (self.device.start_state["ac_power"] == "on")
assert self.state().oscillate is (
self.device.start_state["angle_enable"] == "on"
)
assert self.state().direct_speed == self.device.start_state["speed_level"]
assert self.state().natural_speed == self.device.start_state["natural_level"]
assert self.state().child_lock is (
self.device.start_state["child_lock"] == "on"
)
assert self.state().buzzer is (self.device.start_state["buzzer"] == "on")
assert self.state().led_brightness == LedBrightness(
self.device.start_state["led_b"]
)
assert self.state().led is None
assert self.state().use_time == self.device.start_state["use_time"]
def test_set_direct_speed(self):
def direct_speed():
return self.device.status().direct_speed
self.device.set_direct_speed(0)
assert direct_speed() == 0
self.device.set_direct_speed(1)
assert direct_speed() == 1
self.device.set_direct_speed(100)
assert direct_speed() == 100
with pytest.raises(FanException):
self.device.set_direct_speed(-1)
with pytest.raises(FanException):
self.device.set_direct_speed(101)
def test_set_natural_speed(self):
def natural_speed():
return self.device.status().natural_speed
self.device.set_natural_speed(0)
assert natural_speed() == 0
self.device.set_natural_speed(1)
assert natural_speed() == 1
self.device.set_natural_speed(100)
assert natural_speed() == 100
with pytest.raises(FanException):
self.device.set_natural_speed(-1)
with pytest.raises(FanException):
self.device.set_natural_speed(101)
def test_set_rotate(self):
"""The method is open-loop. The new state cannot be retrieved."""
self.device.set_rotate(MoveDirection.Left)
self.device.set_rotate(MoveDirection.Right)
def test_set_angle(self):
"""This test doesn't implement the real behaviour of the device may be.
The property "angle" doesn't provide the current setting.
It's a measurement of the current position probably.
"""
def angle():
return self.device.status().angle
self.device.set_angle(0) # TODO: Is this value allowed?
assert angle() == 0
self.device.set_angle(1) # TODO: Is this value allowed?
assert angle() == 1
self.device.set_angle(30)
assert angle() == 30
self.device.set_angle(60)
assert angle() == 60
self.device.set_angle(90)
assert angle() == 90
self.device.set_angle(120)
assert angle() == 120
with pytest.raises(FanException):
self.device.set_angle(-1)
with pytest.raises(FanException):
self.device.set_angle(121)
def test_set_oscillate(self):
def oscillate():
return self.device.status().oscillate
self.device.set_oscillate(True)
assert oscillate() is True
self.device.set_oscillate(False)
assert oscillate() is False
def test_set_led_brightness(self):
def led_brightness():
return self.device.status().led_brightness
self.device.set_led_brightness(LedBrightness.Bright)
assert led_brightness() == LedBrightness.Bright
self.device.set_led_brightness(LedBrightness.Dim)
assert led_brightness() == LedBrightness.Dim
self.device.set_led_brightness(LedBrightness.Off)
assert led_brightness() == LedBrightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(FanException):
self.device.delay_off(-1)
with pytest.raises(FanException):
self.device.delay_off(0)
class DummyFanP5(DummyDevice, FanP5):
def __init__(self, *args, **kwargs):
self.model = MODEL_FAN_P5
self.state = {
"power": True,
"mode": "normal",
"speed": 35,
"roll_enable": False,
"roll_angle": 140,
"time_off": 0,
"light": True,
"beep_sound": False,
"child_lock": False,
}
self.return_values = {
"get_prop": self._get_state,
"s_power": lambda x: self._set_state("power", x),
"s_mode": lambda x: self._set_state("mode", x),
"s_speed": lambda x: self._set_state("speed", x),
"s_roll": lambda x: self._set_state("roll_enable", x),
"s_angle": lambda x: self._set_state("roll_angle", x),
"s_t_off": lambda x: self._set_state("time_off", x),
"s_light": lambda x: self._set_state("light", x),
"s_sound": lambda x: self._set_state("beep_sound", x),
"s_lock": lambda x: self._set_state("child_lock", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def fanp5(request):
request.cls.device = DummyFanP5()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("fanp5")
class TestFanP5(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(FanStatusP5(self.device.start_state))
assert self.is_on() is True
assert self.state().mode == OperationMode(self.device.start_state["mode"])
assert self.state().speed == self.device.start_state["speed"]
assert self.state().oscillate is self.device.start_state["roll_enable"]
assert self.state().angle == self.device.start_state["roll_angle"]
assert self.state().delay_off_countdown == self.device.start_state["time_off"]
assert self.state().led is self.device.start_state["light"]
assert self.state().buzzer is self.device.start_state["beep_sound"]
assert self.state().child_lock is self.device.start_state["child_lock"]
def test_set_mode(self):
def mode():
return self.device.status().mode
self.device.set_mode(OperationMode.Normal)
assert mode() == OperationMode.Normal
self.device.set_mode(OperationMode.Nature)
assert mode() == OperationMode.Nature
def test_set_speed(self):
def speed():
return self.device.status().speed
self.device.set_speed(0)
assert speed() == 0
self.device.set_speed(1)
assert speed() == 1
self.device.set_speed(100)
assert speed() == 100
with pytest.raises(FanException):
self.device.set_speed(-1)
with pytest.raises(FanException):
self.device.set_speed(101)
def test_set_angle(self):
def angle():
return self.device.status().angle
self.device.set_angle(30)
assert angle() == 30
self.device.set_angle(60)
assert angle() == 60
self.device.set_angle(90)
assert angle() == 90
self.device.set_angle(120)
assert angle() == 120
self.device.set_angle(140)
assert angle() == 140
with pytest.raises(FanException):
self.device.set_angle(-1)
with pytest.raises(FanException):
self.device.set_angle(1)
with pytest.raises(FanException):
self.device.set_angle(31)
with pytest.raises(FanException):
self.device.set_angle(141)
def test_set_oscillate(self):
def oscillate():
return self.device.status().oscillate
self.device.set_oscillate(True)
assert oscillate() is True
self.device.set_oscillate(False)
assert oscillate() is False
def test_set_led(self):
def led():
return self.device.status().led
self.device.set_led(True)
assert led() is True
self.device.set_led(False)
assert led() is False
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(FanException):
self.device.delay_off(-1)
with pytest.raises(FanException):
self.device.delay_off(0)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584291922.0
python-miio-0.5.0.1/miio/tests/test_heater.py 0000644 0001750 0001750 00000011450 00000000000 020545 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import Heater
from miio.heater import MODEL_HEATER_ZA1, Brightness, HeaterException, HeaterStatus
from .dummies import DummyDevice
class DummyHeater(DummyDevice, Heater):
def __init__(self, *args, **kwargs):
self.model = MODEL_HEATER_ZA1
# This example response is just a guess. Please update!
self.state = {
"target_temperature": 24,
"temperature": 22.1,
"relative_humidity": 46,
"poweroff_time": 0,
"power": "on",
"child_lock": "off",
"buzzer": "on",
"brightness": 1,
"use_time": 0,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_target_temperature": lambda x: self._set_state(
"target_temperature", x
),
"set_brightness": lambda x: self._set_state("brightness", x),
"set_buzzer": lambda x: self._set_state("buzzer", x),
"set_child_lock": lambda x: self._set_state("child_lock", x),
"set_poweroff_time": lambda x: self._set_state("poweroff_time", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def heater(request):
request.cls.device = DummyHeater()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("heater")
class TestHeater(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(HeaterStatus(self.device.start_state))
assert self.is_on() is True
assert (
self.state().target_temperature
== self.device.start_state["target_temperature"]
)
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().humidity == self.device.start_state["relative_humidity"]
assert (
self.state().delay_off_countdown == self.device.start_state["poweroff_time"]
)
assert self.state().child_lock is (
self.device.start_state["child_lock"] == "on"
)
assert self.state().buzzer is (self.device.start_state["buzzer"] == "on")
assert self.state().brightness == Brightness(
self.device.start_state["brightness"]
)
assert self.state().use_time == self.device.start_state["use_time"]
def test_set_target_temperature(self):
def target_temperature():
return self.device.status().target_temperature
self.device.set_target_temperature(16)
assert target_temperature() == 16
self.device.set_target_temperature(24)
assert target_temperature() == 24
self.device.set_target_temperature(32)
assert target_temperature() == 32
with pytest.raises(HeaterException):
self.device.set_target_temperature(15)
with pytest.raises(HeaterException):
self.device.set_target_temperature(33)
def test_set_brightness(self):
def brightness():
return self.device.status().brightness
self.device.set_brightness(Brightness.Bright)
assert brightness() == Brightness.Bright
self.device.set_brightness(Brightness.Dim)
assert brightness() == Brightness.Dim
self.device.set_brightness(Brightness.Off)
assert brightness() == Brightness.Off
def test_set_buzzer(self):
def buzzer():
return self.device.status().buzzer
self.device.set_buzzer(True)
assert buzzer() is True
self.device.set_buzzer(False)
assert buzzer() is False
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(0)
assert delay_off_countdown() == 0
self.device.delay_off(9)
assert delay_off_countdown() == 9
with pytest.raises(HeaterException):
self.device.delay_off(-1)
with pytest.raises(HeaterException):
self.device.delay_off(9 * 3600 + 1)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_philips_bulb.py 0000644 0001750 0001750 00000020464 00000000000 021756 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import PhilipsBulb, PhilipsWhiteBulb
from miio.philips_bulb import (
MODEL_PHILIPS_LIGHT_BULB,
MODEL_PHILIPS_LIGHT_HBULB,
PhilipsBulbException,
PhilipsBulbStatus,
)
from .dummies import DummyDevice
class DummyPhilipsBulb(DummyDevice, PhilipsBulb):
def __init__(self, *args, **kwargs):
self.model = MODEL_PHILIPS_LIGHT_BULB
self.state = {"power": "on", "bright": 100, "cct": 10, "snm": 0, "dv": 0}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_bright": lambda x: self._set_state("bright", x),
"set_cct": lambda x: self._set_state("cct", x),
"delay_off": lambda x: self._set_state("dv", x),
"apply_fixed_scene": lambda x: self._set_state("snm", x),
"set_bricct": lambda x: (
self._set_state("bright", [x[0]]),
self._set_state("cct", [x[1]]),
),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def philips_bulb(request):
request.cls.device = DummyPhilipsBulb()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("philips_bulb")
class TestPhilipsBulb(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(PhilipsBulbStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().brightness == self.device.start_state["bright"]
assert self.state().color_temperature == self.device.start_state["cct"]
assert self.state().scene == self.device.start_state["snm"]
assert self.state().delay_off_countdown == self.device.start_state["dv"]
def test_set_brightness(self):
def brightness():
return self.device.status().brightness
self.device.set_brightness(1)
assert brightness() == 1
self.device.set_brightness(50)
assert brightness() == 50
self.device.set_brightness(100)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness(-1)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness(0)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness(101)
def test_set_color_temperature(self):
def color_temperature():
return self.device.status().color_temperature
self.device.set_color_temperature(20)
assert color_temperature() == 20
self.device.set_color_temperature(30)
assert color_temperature() == 30
self.device.set_color_temperature(10)
with pytest.raises(PhilipsBulbException):
self.device.set_color_temperature(-1)
with pytest.raises(PhilipsBulbException):
self.device.set_color_temperature(0)
with pytest.raises(PhilipsBulbException):
self.device.set_color_temperature(101)
def test_set_brightness_and_color_temperature(self):
def color_temperature():
return self.device.status().color_temperature
def brightness():
return self.device.status().brightness
self.device.set_brightness_and_color_temperature(20, 21)
assert brightness() == 20
assert color_temperature() == 21
self.device.set_brightness_and_color_temperature(31, 30)
assert brightness() == 31
assert color_temperature() == 30
self.device.set_brightness_and_color_temperature(10, 11)
assert brightness() == 10
assert color_temperature() == 11
with pytest.raises(PhilipsBulbException):
self.device.set_brightness_and_color_temperature(-1, 10)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness_and_color_temperature(10, -1)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness_and_color_temperature(0, 10)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness_and_color_temperature(10, 0)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness_and_color_temperature(101, 10)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness_and_color_temperature(10, 101)
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(PhilipsBulbException):
self.device.delay_off(-1)
with pytest.raises(PhilipsBulbException):
self.device.delay_off(0)
def test_set_scene(self):
def scene():
return self.device.status().scene
self.device.set_scene(1)
assert scene() == 1
self.device.set_scene(2)
assert scene() == 2
with pytest.raises(PhilipsBulbException):
self.device.set_scene(-1)
with pytest.raises(PhilipsBulbException):
self.device.set_scene(0)
with pytest.raises(PhilipsBulbException):
self.device.set_scene(5)
class DummyPhilipsWhiteBulb(DummyDevice, PhilipsWhiteBulb):
def __init__(self, *args, **kwargs):
self.model = MODEL_PHILIPS_LIGHT_HBULB
self.state = {"power": "on", "bri": 100, "dv": 0}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_bright": lambda x: self._set_state("bri", x),
"delay_off": lambda x: self._set_state("dv", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def philips_white_bulb(request):
request.cls.device = DummyPhilipsWhiteBulb()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("philips_white_bulb")
class TestPhilipsWhiteBulb(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(PhilipsBulbStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().brightness == self.device.start_state["bri"]
assert self.state().delay_off_countdown == self.device.start_state["dv"]
assert self.state().color_temperature is None
assert self.state().scene is None
def test_set_brightness(self):
def brightness():
return self.device.status().brightness
self.device.set_brightness(1)
assert brightness() == 1
self.device.set_brightness(50)
assert brightness() == 50
self.device.set_brightness(100)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness(-1)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness(0)
with pytest.raises(PhilipsBulbException):
self.device.set_brightness(101)
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(PhilipsBulbException):
self.device.delay_off(-1)
with pytest.raises(PhilipsBulbException):
self.device.delay_off(0)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_philips_eyecare.py 0000644 0001750 0001750 00000013772 00000000000 022453 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import PhilipsEyecare
from miio.philips_eyecare import PhilipsEyecareException, PhilipsEyecareStatus
from .dummies import DummyDevice
class DummyPhilipsEyecare(DummyDevice, PhilipsEyecare):
def __init__(self, *args, **kwargs):
self.state = {
"power": "on",
"bright": 100,
"notifystatus": "off",
"ambstatus": "off",
"ambvalue": 100,
"eyecare": "on",
"scene_num": 3,
"bls": "on",
"dvalue": 0,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_eyecare": lambda x: self._set_state("eyecare", x),
"set_bright": lambda x: self._set_state("bright", x),
"set_user_scene": lambda x: self._set_state("scene_num", x),
"delay_off": lambda x: self._set_state("dvalue", x),
"enable_bl": lambda x: self._set_state("bls", x),
"set_notifyuser": lambda x: self._set_state("notifystatus", x),
"enable_amb": lambda x: self._set_state("ambstatus", x),
"set_amb_bright": lambda x: self._set_state("ambvalue", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def philips_eyecare(request):
request.cls.device = DummyPhilipsEyecare()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("philips_eyecare")
class TestPhilipsEyecare(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(PhilipsEyecareStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().brightness == self.device.start_state["bright"]
assert self.state().reminder is (
self.device.start_state["notifystatus"] == "on"
)
assert self.state().ambient is (self.device.start_state["ambstatus"] == "on")
assert self.state().ambient_brightness == self.device.start_state["ambvalue"]
assert self.state().eyecare is (self.device.start_state["eyecare"] == "on")
assert self.state().scene == self.device.start_state["scene_num"]
assert self.state().smart_night_light is (
self.device.start_state["bls"] == "on"
)
assert self.state().delay_off_countdown == self.device.start_state["dvalue"]
def test_eyecare(self):
def eyecare():
return self.device.status().eyecare
self.device.eyecare_on()
assert eyecare() is True
self.device.eyecare_off()
assert eyecare() is False
def test_set_brightness(self):
def brightness():
return self.device.status().brightness
self.device.set_brightness(1)
assert brightness() == 1
self.device.set_brightness(50)
assert brightness() == 50
self.device.set_brightness(100)
with pytest.raises(PhilipsEyecareException):
self.device.set_brightness(-1)
with pytest.raises(PhilipsEyecareException):
self.device.set_brightness(0)
with pytest.raises(PhilipsEyecareException):
self.device.set_brightness(101)
def test_set_scene(self):
def scene():
return self.device.status().scene
self.device.set_scene(1)
assert scene() == 1
self.device.set_scene(2)
assert scene() == 2
with pytest.raises(PhilipsEyecareException):
self.device.set_scene(-1)
with pytest.raises(PhilipsEyecareException):
self.device.set_scene(0)
with pytest.raises(PhilipsEyecareException):
self.device.set_scene(5)
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(1)
assert delay_off_countdown() == 1
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(PhilipsEyecareException):
self.device.delay_off(-1)
def test_smart_night_light(self):
def smart_night_light():
return self.device.status().smart_night_light
self.device.smart_night_light_on()
assert smart_night_light() is True
self.device.smart_night_light_off()
assert smart_night_light() is False
def test_reminder(self):
def reminder():
return self.device.status().reminder
self.device.reminder_on()
assert reminder() is True
self.device.reminder_off()
assert reminder() is False
def test_ambient(self):
def ambient():
return self.device.status().ambient
self.device.ambient_on()
assert ambient() is True
self.device.ambient_off()
assert ambient() is False
def test_set_ambient_brightness(self):
def ambient_brightness():
return self.device.status().ambient_brightness
self.device.set_ambient_brightness(1)
assert ambient_brightness() == 1
self.device.set_ambient_brightness(50)
assert ambient_brightness() == 50
self.device.set_ambient_brightness(100)
with pytest.raises(PhilipsEyecareException):
self.device.set_ambient_brightness(-1)
with pytest.raises(PhilipsEyecareException):
self.device.set_ambient_brightness(0)
with pytest.raises(PhilipsEyecareException):
self.device.set_ambient_brightness(101)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_philips_moonlight.py 0000644 0001750 0001750 00000020424 00000000000 023026 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import PhilipsMoonlight
from miio.philips_moonlight import PhilipsMoonlightException, PhilipsMoonlightStatus
from miio.utils import int_to_rgb, rgb_to_int
from .dummies import DummyDevice
class DummyPhilipsMoonlight(DummyDevice, PhilipsMoonlight):
def __init__(self, *args, **kwargs):
self.state = {
"pow": "on",
"sta": 0,
"bri": 1,
"rgb": 16741971,
"cct": 1,
"snm": 0,
"spr": 0,
"spt": 15,
"wke": 0,
"bl": 1,
"ms": 1,
"mb": 1,
"wkp": [0, 24, 0],
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("pow", x),
"set_bright": lambda x: self._set_state("bri", x),
"set_cct": lambda x: self._set_state("cct", x),
"set_rgb": lambda x: self._set_state("rgb", [rgb_to_int(x)]),
"apply_fixed_scene": lambda x: self._set_state("snm", x),
"go_night": lambda x: self._set_state("snm", [6]),
"set_bricct": lambda x: (
self._set_state("bri", [x[0]]),
self._set_state("cct", [x[1]]),
),
"set_brirgb": lambda x: (
self._set_state("rgb", [rgb_to_int((x[0], x[1], x[2]))]),
self._set_state("bri", [x[3]]),
),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def philips_moonlight(request):
request.cls.device = DummyPhilipsMoonlight()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("philips_moonlight")
class TestPhilipsMoonlight(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(
PhilipsMoonlightStatus(self.device.start_state)
)
assert self.is_on() is True
assert self.state().brightness == self.device.start_state["bri"]
assert self.state().color_temperature == self.device.start_state["cct"]
assert self.state().rgb == int_to_rgb(int(self.device.start_state["rgb"]))
assert self.state().scene == self.device.start_state["snm"]
def test_set_brightness(self):
def brightness():
return self.device.status().brightness
self.device.set_brightness(1)
assert brightness() == 1
self.device.set_brightness(50)
assert brightness() == 50
self.device.set_brightness(100)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness(-1)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness(0)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness(101)
def test_set_rgb(self):
def rgb():
return self.device.status().rgb
self.device.set_rgb((0, 0, 1))
assert rgb() == (0, 0, 1)
self.device.set_rgb((255, 255, 0))
assert rgb() == (255, 255, 0)
self.device.set_rgb((255, 255, 255))
assert rgb() == (255, 255, 255)
with pytest.raises(PhilipsMoonlightException):
self.device.set_rgb((-1, 0, 0))
with pytest.raises(PhilipsMoonlightException):
self.device.set_rgb((256, 0, 0))
with pytest.raises(PhilipsMoonlightException):
self.device.set_rgb((0, -1, 0))
with pytest.raises(PhilipsMoonlightException):
self.device.set_rgb((0, 256, 0))
with pytest.raises(PhilipsMoonlightException):
self.device.set_rgb((0, 0, -1))
with pytest.raises(PhilipsMoonlightException):
self.device.set_rgb((0, 0, 256))
def test_set_color_temperature(self):
def color_temperature():
return self.device.status().color_temperature
self.device.set_color_temperature(20)
assert color_temperature() == 20
self.device.set_color_temperature(30)
assert color_temperature() == 30
self.device.set_color_temperature(10)
with pytest.raises(PhilipsMoonlightException):
self.device.set_color_temperature(-1)
with pytest.raises(PhilipsMoonlightException):
self.device.set_color_temperature(0)
with pytest.raises(PhilipsMoonlightException):
self.device.set_color_temperature(101)
def test_set_brightness_and_color_temperature(self):
def color_temperature():
return self.device.status().color_temperature
def brightness():
return self.device.status().brightness
self.device.set_brightness_and_color_temperature(20, 21)
assert brightness() == 20
assert color_temperature() == 21
self.device.set_brightness_and_color_temperature(31, 30)
assert brightness() == 31
assert color_temperature() == 30
self.device.set_brightness_and_color_temperature(10, 11)
assert brightness() == 10
assert color_temperature() == 11
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_color_temperature(-1, 10)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_color_temperature(10, -1)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_color_temperature(0, 10)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_color_temperature(10, 0)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_color_temperature(101, 10)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_color_temperature(10, 101)
def test_set_brightness_and_rgb(self):
def brightness():
return self.device.status().brightness
def rgb():
return self.device.status().rgb
self.device.set_brightness_and_rgb(20, (0, 0, 0))
assert brightness() == 20
assert rgb() == (0, 0, 0)
self.device.set_brightness_and_rgb(31, (255, 0, 0))
assert brightness() == 31
assert rgb() == (255, 0, 0)
self.device.set_brightness_and_rgb(100, (255, 255, 255))
assert brightness() == 100
assert rgb() == (255, 255, 255)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(-1, 10)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(0, 10)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(101, 10)
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(10, (-1, 0, 0))
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(10, (256, 0, 0))
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(10, (0, -1, 0))
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(10, (0, 256, 0))
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(10, (0, 0, -1))
with pytest.raises(PhilipsMoonlightException):
self.device.set_brightness_and_rgb(10, (0, 0, 256))
def test_set_scene(self):
def scene():
return self.device.status().scene
self.device.set_scene(1)
assert scene() == 1
self.device.set_scene(6)
assert scene() == 6
with pytest.raises(PhilipsMoonlightException):
self.device.set_scene(-1)
with pytest.raises(PhilipsMoonlightException):
self.device.set_scene(0)
with pytest.raises(PhilipsMoonlightException):
self.device.set_scene(7)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_philips_rwread.py 0000644 0001750 0001750 00000012375 00000000000 022320 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import PhilipsRwread
from miio.philips_rwread import (
MODEL_PHILIPS_LIGHT_RWREAD,
MotionDetectionSensitivity,
PhilipsRwreadException,
PhilipsRwreadStatus,
)
from .dummies import DummyDevice
class DummyPhilipsRwread(DummyDevice, PhilipsRwread):
def __init__(self, *args, **kwargs):
self.model = MODEL_PHILIPS_LIGHT_RWREAD
self.state = {
"power": "on",
"bright": 53,
"dv": 0,
"snm": 1,
"flm": 0,
"flmv": 2,
"chl": 0,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_bright": lambda x: self._set_state("bright", x),
"apply_fixed_scene": lambda x: self._set_state("snm", x),
"delay_off": lambda x: self._set_state("dv", x),
"enable_flm": lambda x: self._set_state("flm", x),
"set_flmvalue": lambda x: self._set_state("flmv", x),
"enable_chl": lambda x: self._set_state("chl", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def philips_eyecare(request):
request.cls.device = DummyPhilipsRwread()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("philips_eyecare")
class TestPhilipsRwread(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(PhilipsRwreadStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().brightness == self.device.start_state["bright"]
assert self.state().delay_off_countdown == self.device.start_state["dv"]
assert self.state().scene == self.device.start_state["snm"]
assert self.state().motion_detection is (self.device.start_state["flm"] == 1)
assert self.state().motion_detection_sensitivity == MotionDetectionSensitivity(
self.device.start_state["flmv"]
)
assert self.state().child_lock is (self.device.start_state["chl"] == 1)
def test_set_brightness(self):
def brightness():
return self.device.status().brightness
self.device.set_brightness(1)
assert brightness() == 1
self.device.set_brightness(50)
assert brightness() == 50
self.device.set_brightness(100)
with pytest.raises(PhilipsRwreadException):
self.device.set_brightness(-1)
with pytest.raises(PhilipsRwreadException):
self.device.set_brightness(0)
with pytest.raises(PhilipsRwreadException):
self.device.set_brightness(101)
def test_set_scene(self):
def scene():
return self.device.status().scene
self.device.set_scene(1)
assert scene() == 1
self.device.set_scene(2)
assert scene() == 2
with pytest.raises(PhilipsRwreadException):
self.device.set_scene(-1)
with pytest.raises(PhilipsRwreadException):
self.device.set_scene(0)
with pytest.raises(PhilipsRwreadException):
self.device.set_scene(5)
def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown
self.device.delay_off(1)
assert delay_off_countdown() == 1
self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200
with pytest.raises(PhilipsRwreadException):
self.device.delay_off(-1)
def test_set_motion_detection(self):
def motion_detection():
return self.device.status().motion_detection
self.device.set_motion_detection(True)
assert motion_detection() is True
self.device.set_motion_detection(False)
assert motion_detection() is False
def test_set_motion_detection_sensitivity(self):
def motion_detection_sensitivity():
return self.device.status().motion_detection_sensitivity
self.device.set_motion_detection_sensitivity(MotionDetectionSensitivity.Low)
assert motion_detection_sensitivity() == MotionDetectionSensitivity.Low
self.device.set_motion_detection_sensitivity(MotionDetectionSensitivity.Medium)
assert motion_detection_sensitivity() == MotionDetectionSensitivity.Medium
self.device.set_motion_detection_sensitivity(MotionDetectionSensitivity.High)
assert motion_detection_sensitivity() == MotionDetectionSensitivity.High
def test_set_child_lock(self):
def child_lock():
return self.device.status().child_lock
self.device.set_child_lock(True)
assert child_lock() is True
self.device.set_child_lock(False)
assert child_lock() is False
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_powerstrip.py 0000644 0001750 0001750 00000016377 00000000000 021530 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import PowerStrip
from miio.powerstrip import (
MODEL_POWER_STRIP_V1,
MODEL_POWER_STRIP_V2,
PowerMode,
PowerStripException,
PowerStripStatus,
)
from .dummies import DummyDevice
class DummyPowerStripV1(DummyDevice, PowerStrip):
def __init__(self, *args, **kwargs):
self.model = MODEL_POWER_STRIP_V1
self.state = {
"power": "on",
"mode": "normal",
"temperature": 32.5,
"current": 25.5,
"power_consume_rate": 12.5,
"voltage": 23057,
"power_factor": 12,
"elec_leakage": 8,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_power_mode": lambda x: self._set_state("mode", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def powerstripv1(request):
request.cls.device = DummyPowerStripV1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("powerstripv1")
class TestPowerStripV1(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(PowerStripStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().mode == PowerMode(self.device.start_state["mode"])
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().current == self.device.start_state["current"]
assert self.state().load_power == self.device.start_state["power_consume_rate"]
assert self.state().voltage == self.device.start_state["voltage"] / 100.0
assert self.state().power_factor == self.device.start_state["power_factor"]
assert self.state().leakage_current == self.device.start_state["elec_leakage"]
def test_status_without_power_consume_rate(self):
self.device._reset_state()
self.device.state["power_consume_rate"] = None
assert self.state().load_power is None
def test_status_without_current(self):
self.device._reset_state()
self.device.state["current"] = None
assert self.state().current is None
def test_status_without_mode(self):
self.device._reset_state()
# The Power Strip 2 doesn't support power modes
self.device.state["mode"] = None
assert self.state().mode is None
def test_set_power_mode(self):
def mode():
return self.device.status().mode
self.device.set_power_mode(PowerMode.Eco)
assert mode() == PowerMode.Eco
self.device.set_power_mode(PowerMode.Normal)
assert mode() == PowerMode.Normal
class DummyPowerStripV2(DummyDevice, PowerStrip):
def __init__(self, *args, **kwargs):
self.model = MODEL_POWER_STRIP_V2
self.state = {
"power": "on",
"mode": "normal",
"temperature": 32.5,
"current": 25.5,
"power_consume_rate": 12.5,
"wifi_led": "off",
"power_price": 49,
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_power_mode": lambda x: self._set_state("mode", x),
"set_wifi_led": lambda x: self._set_state("wifi_led", x),
"set_power_price": lambda x: self._set_state("power_price", x),
"set_rt_power": lambda x: True,
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def powerstripv2(request):
request.cls.device = DummyPowerStripV2()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("powerstripv2")
class TestPowerStripV2(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(PowerStripStatus(self.device.start_state))
assert self.is_on() is True
assert self.state().mode == PowerMode(self.device.start_state["mode"])
assert self.state().temperature == self.device.start_state["temperature"]
assert self.state().current == self.device.start_state["current"]
assert self.state().load_power == self.device.start_state["power_consume_rate"]
assert self.state().voltage is None
assert self.state().power_factor is None
assert self.state().leakage_current is None
def test_status_without_power_consume_rate(self):
self.device._reset_state()
self.device.state["power_consume_rate"] = None
assert self.state().load_power is None
def test_status_without_current(self):
self.device._reset_state()
self.device.state["current"] = None
assert self.state().current is None
def test_status_without_mode(self):
self.device._reset_state()
# The Power Strip 2 doesn't support power modes
self.device.state["mode"] = None
assert self.state().mode is None
def test_set_power_mode(self):
def mode():
return self.device.status().mode
self.device.set_power_mode(PowerMode.Eco)
assert mode() == PowerMode.Eco
self.device.set_power_mode(PowerMode.Normal)
assert mode() == PowerMode.Normal
def test_set_wifi_led(self):
def wifi_led():
return self.device.status().wifi_led
self.device.set_wifi_led(True)
assert wifi_led() is True
self.device.set_wifi_led(False)
assert wifi_led() is False
def test_set_power_price(self):
def power_price():
return self.device.status().power_price
self.device.set_power_price(0)
assert power_price() == 0
self.device.set_power_price(1)
assert power_price() == 1
self.device.set_power_price(2)
assert power_price() == 2
with pytest.raises(PowerStripException):
self.device.set_power_price(-1)
with pytest.raises(PowerStripException):
self.device.set_power_price(1000)
def test_status_without_power_price(self):
self.device._reset_state()
self.device.state["power_price"] = None
assert self.state().power_price is None
def test_set_realtime_power(self):
"""The method is open-loop. The new state cannot be retrieved."""
self.device.set_realtime_power(True)
self.device.set_realtime_power(False)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585489963.0
python-miio-0.5.0.1/miio/tests/test_protocol.py 0000644 0001750 0001750 00000005671 00000000000 021146 0 ustar 00tpr tpr 0000000 0000000 import binascii
from unittest import TestCase
from .. import Utils
from ..protocol import Message
class TestProtocol(TestCase):
def test_non_bytes_payload(self):
payload = "hello world"
valid_token = 32 * b"0"
with self.assertRaises(TypeError):
Utils.encrypt(payload, valid_token)
with self.assertRaises(TypeError):
Utils.decrypt(payload, valid_token)
def test_encrypt(self):
payload = b"hello world"
token = bytes.fromhex(32 * "0")
encrypted = Utils.encrypt(payload, token)
decrypted = Utils.decrypt(encrypted, token)
assert payload == decrypted
def test_invalid_token(self):
payload = b"hello world"
wrong_type = 1234
wrong_length = bytes.fromhex(16 * "0")
with self.assertRaises(TypeError):
Utils.encrypt(payload, wrong_type)
with self.assertRaises(TypeError):
Utils.decrypt(payload, wrong_type)
with self.assertRaises(ValueError):
Utils.encrypt(payload, wrong_length)
with self.assertRaises(ValueError):
Utils.decrypt(payload, wrong_length)
def test_decode_json_payload(self):
token = bytes.fromhex(32 * "0")
ctx = {"token": token}
def build_msg(data):
encrypted_data = Utils.encrypt(data, token)
# header
magic = binascii.unhexlify(b"2131")
length = (32 + len(encrypted_data)).to_bytes(2, byteorder="big")
unknown = binascii.unhexlify(b"00000000")
did = binascii.unhexlify(b"01234567")
epoch = binascii.unhexlify(b"00000000")
checksum = Utils.md5(
magic + length + unknown + did + epoch + token + encrypted_data
)
return magic + length + unknown + did + epoch + checksum + encrypted_data
# can parse message with valid json
serialized_msg = build_msg(b'{"id": 123456}')
parsed_msg = Message.parse(serialized_msg, **ctx)
assert parsed_msg.data.value
assert isinstance(parsed_msg.data.value, dict)
assert parsed_msg.data.value["id"] == 123456
# can parse message with invalid json for edge case powerstrip
# when not connected to cloud
serialized_msg = build_msg(b'{"id": 123456,,"otu_stat":0}')
parsed_msg = Message.parse(serialized_msg, **ctx)
assert parsed_msg.data.value
assert isinstance(parsed_msg.data.value, dict)
assert parsed_msg.data.value["id"] == 123456
assert parsed_msg.data.value["otu_stat"] == 0
# can parse message with invalid json for edge case xiaomi cloud
# reply to _sync.batch_gen_room_up_url
serialized_msg = build_msg(b'{"id": 123456}\x00k')
parsed_msg = Message.parse(serialized_msg, **ctx)
assert parsed_msg.data.value
assert isinstance(parsed_msg.data.value, dict)
assert parsed_msg.data.value["id"] == 123456
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_toiletlid.py 0000644 0001750 0001750 00000011474 00000000000 021274 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio.toiletlid import (
MODEL_TOILETLID_V1,
AmbientLightColor,
Toiletlid,
ToiletlidStatus,
)
from .dummies import DummyDevice
"""
Response instance
>> status
Work: False
State: 1
Ambient Light: Yellow
Filter remaining: 100%
Filter remaining time: 180
"""
class DummyToiletlidV1(DummyDevice, Toiletlid):
def __init__(self, *args, **kwargs):
self.model = MODEL_TOILETLID_V1
self.state = {
"is_on": False,
"work_state": 1,
"work_mode": "Vacant",
"ambient_light": "Yellow",
"filter_use_flux": "100",
"filter_use_time": "180",
}
self.users = {}
self.return_values = {
"get_prop": self._get_state,
"nozzle_clean": lambda x: self._set_state("work_state", [97]),
"set_aled_v_of_uid": self.set_aled_v_of_uid,
"get_aled_v_of_uid": self.get_aled_v_of_uid,
"uid_mac_op": self.uid_mac_op,
"get_all_user_info": self.get_all_user_info,
}
super().__init__(args, kwargs)
def set_aled_v_of_uid(self, args):
uid, color = args
if uid:
if uid in self.users:
self.users.setdefault("ambient_light", AmbientLightColor(color).name)
else:
raise ValueError("This user is not bind.")
else:
return self._set_state("ambient_light", [AmbientLightColor(color).name])
def get_aled_v_of_uid(self, args):
uid = args[0]
if uid:
if uid in self.users:
color = self.users.get("ambient_light")
else:
raise ValueError("This user is not bind.")
else:
color = self._get_state(["ambient_light"])
if not AmbientLightColor._member_map_.get(color[0]):
raise ValueError(color)
return AmbientLightColor._member_map_.get(color[0]).value
def uid_mac_op(self, args):
xiaomi_id, band_mac, alias, operating = args
if operating == "bind":
info = self.users.setdefault(
xiaomi_id, {"rssi": -50, "set": "3-0-2-2-0-0-5-5"}
)
info.update(mac=band_mac, name=alias)
elif operating == "unbind":
self.users.pop(xiaomi_id)
else:
raise ValueError("operating error")
def get_all_user_info(self):
users = {}
for index, (xiaomi_id, info) in enumerate(self.users.items(), start=1):
user_id = "user%s" % index
users[user_id] = {"uid": xiaomi_id, **info}
return users
@pytest.fixture(scope="class")
def toiletlidv1(request):
request.cls.device = DummyToiletlidV1()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("toiletlidv1")
class TestToiletlidV1(TestCase):
MOCK_USER = {
"11111111": {
"mac": "ff:ff:ff:ff:ff:ff",
"name": "myband",
"rssi": -50,
"set": "3-0-2-2-0-0-5-5",
}
}
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(ToiletlidStatus(self.device.start_state))
assert self.is_on() is False
assert self.state().work_state == self.device.start_state["work_state"]
assert self.state().ambient_light == self.device.start_state["ambient_light"]
assert (
self.state().filter_use_percentage
== "%s%%" % self.device.start_state["filter_use_flux"]
)
assert (
self.state().filter_remaining_time
== self.device.start_state["filter_use_time"]
)
def test_set_ambient_light(self):
for value, enum in AmbientLightColor._member_map_.items():
self.device.set_ambient_light(enum)
assert self.device.status().ambient_light == value
def test_nozzle_clean(self):
self.device.nozzle_clean()
assert self.is_on() is True
self.device._reset_state()
def test_get_all_user_info(self):
users = self.device.get_all_user_info()
for name, info in users.items():
assert info["uid"] in self.MOCK_USER
data = self.MOCK_USER[info["uid"]]
assert info["name"] == data["name"]
assert info["mac"] == data["mac"]
def test_bind_xiaomi_band(self):
for xiaomi_id, info in self.MOCK_USER.items():
self.device.bind_xiaomi_band(xiaomi_id, info["mac"], info["name"])
assert self.device.users == self.MOCK_USER
def test_unbind_xiaomi_band(self):
for xiaomi_id, info in self.MOCK_USER.items():
self.device.unbind_xiaomi_band(xiaomi_id, info["mac"])
assert self.device.users == {}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584365257.0
python-miio-0.5.0.1/miio/tests/test_vacuum.py 0000644 0001750 0001750 00000014255 00000000000 020603 0 ustar 00tpr tpr 0000000 0000000 import datetime
from unittest import TestCase
import pytest
from miio import Vacuum, VacuumStatus
from .dummies import DummyDevice
class DummyVacuum(DummyDevice, Vacuum):
STATE_CHARGING = 8
STATE_CLEANING = 5
STATE_ZONED_CLEAN = 9
STATE_IDLE = 3
STATE_HOME = 6
STATE_SPOT = 11
STATE_GOTO = 4
STATE_ERROR = 12
STATE_PAUSED = 10
STATE_MANUAL = 7
def __init__(self, *args, **kwargs):
self.state = {
"state": 8,
"dnd_enabled": 1,
"clean_time": 0,
"msg_ver": 4,
"map_present": 1,
"error_code": 0,
"in_cleaning": 0,
"clean_area": 0,
"battery": 100,
"fan_power": 20,
"msg_seq": 320,
}
self.return_values = {
"get_status": self.vacuum_state,
"app_start": lambda x: self.change_mode("start"),
"app_stop": lambda x: self.change_mode("stop"),
"app_pause": lambda x: self.change_mode("pause"),
"app_spot": lambda x: self.change_mode("spot"),
"app_goto_target": lambda x: self.change_mode("goto"),
"app_zoned_clean": lambda x: self.change_mode("zoned clean"),
"app_charge": lambda x: self.change_mode("charge"),
}
super().__init__(args, kwargs)
def change_mode(self, new_mode):
if new_mode == "spot":
self.state["state"] = DummyVacuum.STATE_SPOT
elif new_mode == "home":
self.state["state"] = DummyVacuum.STATE_HOME
elif new_mode == "pause":
self.state["state"] = DummyVacuum.STATE_PAUSED
elif new_mode == "start":
self.state["state"] = DummyVacuum.STATE_CLEANING
elif new_mode == "stop":
self.state["state"] = DummyVacuum.STATE_IDLE
elif new_mode == "goto":
self.state["state"] = DummyVacuum.STATE_GOTO
elif new_mode == "zoned clean":
self.state["state"] = DummyVacuum.STATE_ZONED_CLEAN
elif new_mode == "charge":
self.state["state"] = DummyVacuum.STATE_CHARGING
def vacuum_state(self, _):
return [self.state]
@pytest.fixture(scope="class")
def dummyvacuum(request):
request.cls.device = DummyVacuum()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("dummyvacuum")
class TestVacuum(TestCase):
def status(self):
return self.device.status()
def test_status(self):
self.device._reset_state()
assert repr(self.status()) == repr(VacuumStatus(self.device.start_state))
status = self.status()
assert status.is_on is False
assert status.dnd is True
assert status.clean_time == datetime.timedelta()
assert status.error_code == 0
assert status.error == "No error"
assert status.fanspeed == self.device.start_state["fan_power"]
assert status.battery == self.device.start_state["battery"]
def test_status_with_errors(self):
errors = {5: "Clean main brush", 19: "Unpowered charging station"}
for errcode, error in errors.items():
self.device.state["state"] = self.device.STATE_ERROR
self.device.state["error_code"] = errcode
assert self.status().is_on is False
assert self.status().got_error is True
assert self.status().error_code == errcode
assert self.status().error == error
def test_start_and_stop(self):
assert self.status().is_on is False
self.device.start()
assert self.status().is_on is True
assert self.status().state_code == self.device.STATE_CLEANING
self.device.stop()
assert self.status().is_on is False
def test_spot(self):
assert self.status().is_on is False
self.device.spot()
assert self.status().is_on is True
assert self.status().state_code == self.device.STATE_SPOT
self.device.stop()
assert self.status().is_on is False
def test_pause(self):
self.device.start()
assert self.status().is_on is True
self.device.pause()
assert self.status().state_code == self.device.STATE_PAUSED
def test_home(self):
self.device.start()
assert self.status().is_on is True
self.device.home()
assert self.status().state_code == self.device.STATE_CHARGING
# TODO pause here and update to idle/charging and assert for that?
# Another option is to mock that app_stop mode is entered before
# the charging is activated.
def test_goto(self):
self.device.start()
assert self.status().is_on is True
self.device.goto(24000, 24000)
assert self.status().state_code == self.device.STATE_GOTO
def test_zoned_clean(self):
self.device.start()
assert self.status().is_on is True
self.device.zoned_clean(
[[25000, 25000, 25500, 25500, 3], [23000, 23000, 22500, 22500, 1]]
)
assert self.status().state_code == self.device.STATE_ZONED_CLEAN
@pytest.mark.xfail
def test_manual_control(self):
self.fail()
@pytest.mark.skip("unknown handling")
def test_log_upload(self):
self.fail()
@pytest.mark.xfail
def test_consumable_status(self):
self.fail()
@pytest.mark.skip("consumable reset is not implemented")
def test_consumable_reset(self):
self.fail()
@pytest.mark.xfail
def test_map(self):
self.fail()
@pytest.mark.xfail
def test_clean_history(self):
self.fail()
@pytest.mark.xfail
def test_clean_details(self):
self.fail()
@pytest.mark.skip("hard to test")
def test_find(self):
self.fail()
@pytest.mark.xfail
def test_timer(self):
self.fail()
@pytest.mark.xfail
def test_dnd(self):
self.fail()
@pytest.mark.xfail
def test_fan_speed(self):
self.fail()
@pytest.mark.xfail
def test_sound_info(self):
self.fail()
@pytest.mark.xfail
def test_serial_number(self):
self.fail()
@pytest.mark.xfail
def test_timezone(self):
self.fail()
@pytest.mark.xfail
def test_raw_command(self):
self.fail()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_waterpurifier.py 0000644 0001750 0001750 00000003523 00000000000 022167 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import WaterPurifier
from miio.waterpurifier import WaterPurifierStatus
from .dummies import DummyDevice
class DummyWaterPurifier(DummyDevice, WaterPurifier):
def __init__(self, *args, **kwargs):
self.state = {
"power": "on",
"mode": "unknown",
"tds": "unknown",
"filter1_life": -1,
"filter1_state": -1,
"filter_life": -1,
"filter_state": -1,
"life": -1,
"state": -1,
"level": "unknown",
"volume": "unknown",
"filter": "unknown",
"usage": "unknown",
"temperature": "unknown",
"uv_life": -1,
"uv_state": -1,
"elecval_state": "unknown",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
}
super().__init__(args, kwargs)
@pytest.fixture(scope="class")
def waterpurifier(request):
request.cls.device = DummyWaterPurifier()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("waterpurifier")
class TestWaterPurifier(TestCase):
def is_on(self):
return self.device.status().is_on
def state(self):
return self.device.status()
def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False
self.device.on()
assert self.is_on() is True
def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True
self.device.off()
assert self.is_on() is False
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(WaterPurifierStatus(self.device.start_state))
assert self.is_on() is True
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_wifirepeater.py 0000644 0001750 0001750 00000013205 00000000000 021763 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import WifiRepeater
from miio.tests.dummies import DummyDevice
from miio.wifirepeater import WifiRepeaterConfiguration, WifiRepeaterStatus
class DummyWifiRepeater(DummyDevice, WifiRepeater):
def __init__(self, *args, **kwargs):
self.state = {
"sta": {"count": 2, "access_policy": 0},
"mat": [
{
"mac": "aa:aa:aa:aa:aa:aa",
"ip": "192.168.1.133",
"last_time": 54371873,
},
{
"mac": "bb:bb:bb:bb:bb:bb",
"ip": "192.168.1.156",
"last_time": 54371496,
},
],
"access_list": {"mac": ""},
}
self.config = {"ssid": "SSID", "pwd": "PWD", "hidden": 0}
self.device_info = {
"life": 543452,
"cfg_time": 543452,
"token": "ffffffffffffffffffffffffffffffff",
"fw_ver": "2.2.14",
"hw_ver": "R02",
"uid": 1583412143,
"api_level": 2,
"mcu_fw_ver": "1000",
"wifi_fw_ver": "1.0.0",
"mac": "FF:FF:FF:FF:FF:FF",
"model": "xiaomi.repeater.v2",
"ap": {
"rssi": -63,
"ssid": "SSID",
"bssid": "EE:EE:EE:EE:EE:EE",
"rx": 136695922,
"tx": 1779521233,
},
"sta": {
"count": 2,
"ssid": "REPEATER-SSID",
"hidden": 0,
"assoclist": "cc:cc:cc:cc:cc:cc;bb:bb:bb:bb:bb:bb;",
},
"netif": {
"localIp": "192.168.1.170",
"mask": "255.255.255.0",
"gw": "192.168.1.1",
},
"desc": {
"wifi_explorer": 1,
"sn": "14923 / 20191356",
"color": 101,
"channel": "release",
},
}
self.return_values = {
"miIO.get_repeater_sta_info": self._get_state,
"miIO.get_repeater_ap_info": self._get_configuration,
"miIO.switch_wifi_explorer": self._set_wifi_explorer,
"miIO.switch_wifi_ssid": self._set_configuration,
"miIO.info": self._get_info,
}
self.start_state = self.state.copy()
self.start_config = self.config.copy()
self.start_device_info = self.device_info.copy()
super().__init__(args, kwargs)
def _reset_state(self):
"""Revert back to the original state."""
self.state = self.start_state.copy()
self.config = self.start_config.copy()
self.device_info = self.start_device_info.copy()
def _get_state(self, param):
return self.state
def _get_configuration(self, param):
return self.config
def _get_info(self, param):
return self.device_info
def _set_wifi_explorer(self, data):
self.device_info["desc"]["wifi_explorer"] = data[0]["wifi_explorer"]
def _set_configuration(self, data):
self.config = {
"ssid": data[0]["ssid"],
"pwd": data[0]["pwd"],
"hidden": data[0]["hidden"],
}
self.device_info["desc"]["wifi_explorer"] = data[0]["wifi_explorer"]
return True
@pytest.fixture(scope="class")
def wifirepeater(request):
request.cls.device = DummyWifiRepeater()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("wifirepeater")
class TestWifiRepeater(TestCase):
def state(self):
return self.device.status()
def configuration(self):
return self.device.configuration()
def info(self):
return self.device.info()
def wifi_roaming(self):
return self.device.wifi_roaming()
def rssi_accesspoint(self):
return self.device.rssi_accesspoint()
def test_status(self):
self.device._reset_state()
assert repr(self.state()) == repr(WifiRepeaterStatus(self.device.start_state))
assert (
self.state().access_policy
== self.device.start_state["sta"]["access_policy"]
)
assert self.state().associated_stations == self.device.start_state["mat"]
def test_set_wifi_roaming(self):
self.device.set_wifi_roaming(True)
assert self.wifi_roaming() is True
self.device.set_wifi_roaming(False)
assert self.wifi_roaming() is False
def test_configuration(self):
self.device._reset_state()
assert repr(self.configuration()) == repr(
WifiRepeaterConfiguration(self.device.start_config)
)
assert self.configuration().ssid == self.device.start_config["ssid"]
assert self.configuration().password == self.device.start_config["pwd"]
assert self.configuration().ssid_hidden is (
self.device.start_config["hidden"] == 1
)
def test_set_configuration(self):
def configuration():
return self.device.configuration()
dummy_configuration = {"ssid": "SSID2", "password": "PASSWORD2", "hidden": True}
self.device.set_configuration(
dummy_configuration["ssid"],
dummy_configuration["password"],
dummy_configuration["hidden"],
)
assert configuration().ssid == dummy_configuration["ssid"]
assert configuration().password == dummy_configuration["password"]
assert configuration().ssid_hidden is dummy_configuration["hidden"]
def test_rssi_accesspoint(self):
self.device._reset_state()
assert self.rssi_accesspoint() is self.device.start_device_info["ap"]["rssi"]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/tests/test_yeelight.py 0000644 0001750 0001750 00000015036 00000000000 021113 0 ustar 00tpr tpr 0000000 0000000 from unittest import TestCase
import pytest
from miio import Yeelight
from miio.yeelight import YeelightException, YeelightMode, YeelightStatus
from .dummies import DummyDevice
class DummyLight(DummyDevice, Yeelight):
def __init__(self, *args, **kwargs):
self.state = {
"power": "off",
"bright": "100",
"ct": "3584",
"rgb": "16711680",
"hue": "359",
"sat": "100",
"color_mode": "2",
"name": "test name",
"lan_ctrl": "1",
"save_state": "1",
}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_bright": lambda x: self._set_state("bright", x),
"set_ct_abx": lambda x: self._set_state("ct", x),
"set_rgb": lambda x: self._set_state("rgb", x),
"set_hsv": lambda x: self._set_state("hsv", x),
"set_name": lambda x: self._set_state("name", x),
"set_ps": lambda x: self.set_config(x),
"toggle": self.toggle_power,
"set_default": lambda x: "ok",
}
super().__init__(*args, **kwargs)
def set_config(self, x):
key, value = x
config_mapping = {"cfg_lan_ctrl": "lan_ctrl", "cfg_save_state": "save_state"}
self._set_state(config_mapping[key], [value])
def toggle_power(self, _):
if self.state["power"] == "on":
self.state["power"] = "off"
else:
self.state["power"] = "on"
@pytest.fixture(scope="class")
def dummylight(request):
request.cls.device = DummyLight()
# TODO add ability to test on a real device
@pytest.mark.usefixtures("dummylight")
class TestYeelight(TestCase):
def test_status(self):
self.device._reset_state()
status = self.device.status() # type: YeelightStatus
assert repr(status) == repr(YeelightStatus(self.device.start_state))
assert status.name == self.device.start_state["name"]
assert status.is_on is False
assert status.brightness == 100
assert status.color_temp == 3584
assert status.color_mode == YeelightMode.ColorTemperature
assert status.rgb is None
assert status.developer_mode is True
assert status.save_state_on_change is True
# following are tested in set mode tests
# assert status.rgb == 16711680
# assert status.hsv == (359, 100, 100)
def test_on(self):
self.device.off() # make sure we are off
assert self.device.status().is_on is False
self.device.on()
assert self.device.status().is_on is True
def test_off(self):
self.device.on() # make sure we are on
assert self.device.status().is_on is True
self.device.off()
assert self.device.status().is_on is False
def test_set_brightness(self):
def brightness():
return self.device.status().brightness
self.device.set_brightness(50)
assert brightness() == 50
self.device.set_brightness(0)
assert brightness() == 0
self.device.set_brightness(100)
with pytest.raises(YeelightException):
self.device.set_brightness(-100)
with pytest.raises(YeelightException):
self.device.set_brightness(200)
def test_set_color_temp(self):
def color_temp():
return self.device.status().color_temp
self.device.set_color_temp(2000)
assert color_temp() == 2000
self.device.set_color_temp(6500)
assert color_temp() == 6500
with pytest.raises(YeelightException):
self.device.set_color_temp(1000)
with pytest.raises(YeelightException):
self.device.set_color_temp(7000)
def test_set_rgb(self):
def rgb():
return self.device.status().rgb
self.device._reset_state()
self.device._set_state("color_mode", [1])
assert rgb() == (255, 0, 0)
self.device.set_rgb((0, 0, 1))
assert rgb() == (0, 0, 1)
self.device.set_rgb((255, 255, 0))
assert rgb() == (255, 255, 0)
self.device.set_rgb((255, 255, 255))
assert rgb() == (255, 255, 255)
with pytest.raises(YeelightException):
self.device.set_rgb((-1, 0, 0))
with pytest.raises(YeelightException):
self.device.set_rgb((256, 0, 0))
with pytest.raises(YeelightException):
self.device.set_rgb((0, -1, 0))
with pytest.raises(YeelightException):
self.device.set_rgb((0, 256, 0))
with pytest.raises(YeelightException):
self.device.set_rgb((0, 0, -1))
with pytest.raises(YeelightException):
self.device.set_rgb((0, 0, 256))
@pytest.mark.skip("hsv is not properly implemented")
def test_set_hsv(self):
self.reset_state()
hue, sat, val = self.device.status().hsv
assert hue == 359
assert sat == 100
assert val == 100
self.device.set_hsv()
def test_set_developer_mode(self):
def dev_mode():
return self.device.status().developer_mode
orig_mode = dev_mode()
self.device.set_developer_mode(not orig_mode)
new_mode = dev_mode()
assert new_mode is not orig_mode
self.device.set_developer_mode(not new_mode)
assert new_mode is not dev_mode()
def test_set_save_state_on_change(self):
def save_state():
return self.device.status().save_state_on_change
orig_state = save_state()
self.device.set_save_state_on_change(not orig_state)
new_state = save_state()
assert new_state is not orig_state
self.device.set_save_state_on_change(not new_state)
new_state = save_state()
assert new_state is orig_state
def test_set_name(self):
def name():
return self.device.status().name
assert name() == "test name"
self.device.set_name("new test name")
assert name() == "new test name"
def test_toggle(self):
def is_on():
return self.device.status().is_on
orig_state = is_on()
self.device.toggle()
new_state = is_on()
assert orig_state != new_state
self.device.toggle()
new_state = is_on()
assert new_state == orig_state
@pytest.mark.skip("cannot be tested easily")
def test_set_default(self):
self.fail()
@pytest.mark.skip("set_scene is not implemented")
def test_set_scene(self):
self.fail()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585421538.0
python-miio-0.5.0.1/miio/toiletlid.py 0000644 0001750 0001750 00000013502 00000000000 017065 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
from typing import Any, Dict, List
import click
from .click_common import EnumType, command, format_output
from .device import Device
_LOGGER = logging.getLogger(__name__)
MODEL_TOILETLID_V1 = "tinymu.toiletlid.v1"
AVAILABLE_PROPERTIES_COMMON = ["work_state", "filter_use_flux", "filter_use_time"]
AVAILABLE_PROPERTIES = {MODEL_TOILETLID_V1: AVAILABLE_PROPERTIES_COMMON}
class AmbientLightColor(enum.Enum):
White = "0"
Yellow = "1"
Powder = "2"
Green = "3"
Purple = "4"
Blue = "5"
Orange = "6"
Red = "7"
class ToiletlidOperatingMode(enum.Enum):
Vacant = 0
Occupied = 1
RearCleanse = 2
FrontCleanse = 3
NozzleClean = 6
class ToiletlidStatus:
def __init__(self, data: Dict[str, Any]) -> None:
# {"work_state": 1,"filter_use_flux": 100,"filter_use_time": 180, "ambient_light": "Red"}
self.data = data
@property
def work_state(self) -> int:
"""Device state code"""
return self.data["work_state"]
@property
def work_mode(self) -> ToiletlidOperatingMode:
"""Device working mode"""
return ToiletlidOperatingMode((self.work_state - 1) // 16)
@property
def is_on(self) -> bool:
return self.work_state != 1
@property
def filter_use_percentage(self) -> str:
"""Filter percentage of remaining life"""
return "{}%".format(self.data["filter_use_flux"])
@property
def filter_remaining_time(self) -> int:
"""Filter remaining life days"""
return self.data["filter_use_time"]
@property
def ambient_light(self) -> str:
"""Ambient light color."""
return self.data["ambient_light"]
def __repr__(self) -> str:
return (
""
% (
self.is_on,
self.work_state,
self.work_mode,
self.ambient_light,
self.filter_use_percentage,
self.filter_remaining_time,
)
)
class Toiletlid(Device):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_TOILETLID_V1,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_TOILETLID_V1
@command(
default_output=format_output(
"",
"Work: {result.is_on}\n"
"State: {result.work_state}\n"
"Work Mode: {result.work_mode}\n"
"Ambient Light: {result.ambient_light}\n"
"Filter remaining: {result.filter_use_percentage}\n"
"Filter remaining time: {result.filter_remaining_time}\n",
)
)
def status(self) -> ToiletlidStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
values = self.send("get_prop", properties)
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
color = self.get_ambient_light()
return ToiletlidStatus(dict(zip(properties, values), ambient_light=color))
@command(default_output=format_output("Nozzle clean"))
def nozzle_clean(self):
"""Nozzle clean."""
return self.send("nozzle_clean", ["on"])
@command(
click.argument("color", type=EnumType(AmbientLightColor, False)),
click.argument("xiaomi_id", type=str, default=""),
default_output=format_output(
"Set the ambient light to {color} color the next time you start it."
),
)
def set_ambient_light(self, color: AmbientLightColor, xiaomi_id: str = ""):
"""Set Ambient light color."""
return self.send("set_aled_v_of_uid", [xiaomi_id, color.value])
@command(
click.argument("xiaomi_id", type=str, default=""),
default_output=format_output("Get the Ambient light color."),
)
def get_ambient_light(self, xiaomi_id: str = "") -> str:
"""Get Ambient light color."""
color = self.send("get_aled_v_of_uid", [xiaomi_id])
try:
return AmbientLightColor(color[0]).name
except ValueError:
_LOGGER.warning(
"Get ambient light response error, return unknown value: %s.", color[0]
)
return "Unknown"
@command(default_output=format_output("Get user list."))
def get_all_user_info(self) -> List[Dict]:
"""Get All bind user."""
users = self.send("get_all_user_info")
return users
@command(
click.argument("xiaomi_id", type=str),
click.argument("band_mac", type=str),
click.argument("alias", type=str),
default_output=format_output("Bind xiaomi band to xiaomi id."),
)
def bind_xiaomi_band(self, xiaomi_id: str, band_mac: str, alias: str):
"""Bind xiaomi band to xiaomi id."""
return self.send("uid_mac_op", [xiaomi_id, band_mac, alias, "bind"])
@command(
click.argument("xiaomi_id", type=str),
click.argument("band_mac", type=str),
default_output=format_output("Unbind xiaomi band to xiaomi id."),
)
def unbind_xiaomi_band(self, xiaomi_id: str, band_mac: str):
"""Unbind xiaomi band to xiaomi id."""
return self.send("uid_mac_op", [xiaomi_id, band_mac, "", "unbind"])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/updater.py 0000644 0001750 0001750 00000006021 00000000000 016536 0 ustar 00tpr tpr 0000000 0000000 import hashlib
import logging
from http.server import BaseHTTPRequestHandler, HTTPServer
from os.path import basename
import netifaces
_LOGGER = logging.getLogger(__name__)
class SingleFileHandler(BaseHTTPRequestHandler):
"""A simplified handler just returning the contents of a buffer."""
def __init__(self, request, client_address, server):
self.payload = server.payload
self.server = server
super().__init__(request, client_address, server)
def handle_one_request(self):
self.server.got_request = True
self.raw_requestline = self.rfile.readline()
if not self.parse_request():
_LOGGER.error("unable to parse request: %s" % self.raw_requestline)
return
self.send_response(200)
self.send_header("Content-type", "application/octet-stream")
self.send_header("Content-Length", len(self.payload))
self.end_headers()
self.wfile.write(self.payload)
class OneShotServer:
"""A simple HTTP server for serving an update file.
The server will be started in an emphemeral port, and will only accept
a single request to keep it simple."""
def __init__(self, file, interface=None):
addr = ("", 0)
self.server = HTTPServer(addr, SingleFileHandler)
setattr(self.server, "got_request", False)
self.addr, self.port = self.server.server_address
self.server.timeout = 10
_LOGGER.info(
"Serving on %s:%s, timeout %s" % (self.addr, self.port, self.server.timeout)
)
self.file = basename(file)
with open(file, "rb") as f:
self.payload = f.read()
self.server.payload = self.payload
self.md5 = hashlib.md5(self.payload).hexdigest()
_LOGGER.info("Using local %s (md5: %s)" % (file, self.md5))
@staticmethod
def find_local_ip():
ifaces_without_lo = [
x for x in netifaces.interfaces() if not x.startswith("lo")
]
_LOGGER.debug("available interfaces: %s" % ifaces_without_lo)
for iface in ifaces_without_lo:
addresses = netifaces.ifaddresses(iface)
if netifaces.AF_INET not in addresses:
_LOGGER.debug("%s has no ipv4 addresses, skipping" % iface)
continue
for entry in addresses[netifaces.AF_INET]:
_LOGGER.debug("Got addr: %s" % entry["addr"])
return entry["addr"]
def url(self, ip=None):
if ip is None:
ip = OneShotServer.find_local_ip()
url = "http://%s:%s/%s" % (ip, self.port, self.file)
return url
def serve_once(self):
self.server.handle_request()
if getattr(self.server, "got_request"):
_LOGGER.info("Got a request, should be downloading now.")
return True
else:
_LOGGER.error("No request was made..")
return False
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
upd = OneShotServer("/tmp/test")
upd.serve_once()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/utils.py 0000644 0001750 0001750 00000006232 00000000000 016236 0 ustar 00tpr tpr 0000000 0000000 import functools
import inspect
import warnings
from datetime import datetime, timedelta
from typing import Tuple
def deprecated(reason):
"""
This is a decorator which can be used to mark functions and classes
as deprecated. It will result in a warning being emitted
when the function is used.
From https://stackoverflow.com/a/40301488
"""
string_types = (type(b""), type(u""))
if isinstance(reason, string_types):
# The @deprecated is used with a 'reason'.
#
# .. code-block:: python
#
# @deprecated("please, use another function")
# def old_function(x, y):
# pass
def decorator(func1):
if inspect.isclass(func1):
fmt1 = "Call to deprecated class {name} ({reason})."
else:
fmt1 = "Call to deprecated function {name} ({reason})."
@functools.wraps(func1)
def new_func1(*args, **kwargs):
warnings.simplefilter("always", DeprecationWarning)
warnings.warn(
fmt1.format(name=func1.__name__, reason=reason),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter("default", DeprecationWarning)
return func1(*args, **kwargs)
return new_func1
return decorator
elif inspect.isclass(reason) or inspect.isfunction(reason):
# The @deprecated is used without any 'reason'.
#
# .. code-block:: python
#
# @deprecated
# def old_function(x, y):
# pass
func2 = reason
if inspect.isclass(func2):
fmt2 = "Call to deprecated class {name}."
else:
fmt2 = "Call to deprecated function {name}."
@functools.wraps(func2)
def new_func2(*args, **kwargs):
warnings.simplefilter("always", DeprecationWarning)
warnings.warn(
fmt2.format(name=func2.__name__),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter("default", DeprecationWarning)
return func2(*args, **kwargs)
return new_func2
else:
raise TypeError(repr(type(reason)))
def pretty_seconds(x: float) -> timedelta:
"""Return a timedelta object from seconds."""
return timedelta(seconds=x)
def pretty_time(x: float) -> datetime:
"""Return a datetime object from unix timestamp."""
return datetime.fromtimestamp(x)
def int_to_rgb(x: int) -> Tuple[int, int, int]:
"""Return a RGB tuple from integer."""
red = (x >> 16) & 0xFF
green = (x >> 8) & 0xFF
blue = x & 0xFF
return red, green, blue
def rgb_to_int(x: Tuple[int, int, int]) -> int:
"""Return an integer from RGB tuple."""
return int(x[0] << 16 | x[1] << 8 | x[2])
def int_to_brightness(x: int) -> int:
""""Return brightness (0-100) from integer."""
return x >> 24
def brightness_and_color_to_int(brightness: int, color: Tuple[int, int, int]) -> int:
return int(brightness << 24 | color[0] << 16 | color[1] << 8 | color[2])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/vacuum.py 0000644 0001750 0001750 00000053007 00000000000 016400 0 ustar 00tpr tpr 0000000 0000000 import datetime
import enum
import json
import logging
import math
import os
import pathlib
import time
from typing import Dict, List, Optional, Union
import click
import pytz
from appdirs import user_cache_dir
from .click_common import DeviceGroup, GlobalContextObject, LiteralParamType, command
from .device import Device
from .exceptions import DeviceException
from .vacuumcontainers import (
CarpetModeStatus,
CleaningDetails,
CleaningSummary,
ConsumableStatus,
DNDStatus,
SoundInstallStatus,
SoundStatus,
Timer,
VacuumStatus,
)
_LOGGER = logging.getLogger(__name__)
class VacuumException(DeviceException):
pass
class TimerState(enum.Enum):
On = "on"
Off = "off"
class Consumable(enum.Enum):
MainBrush = "main_brush_work_time"
SideBrush = "side_brush_work_time"
Filter = "filter_work_time"
SensorDirty = "sensor_dirty_time"
class FanspeedV1(enum.Enum):
Silent = 38
Standard = 60
Medium = 77
Turbo = 90
class FanspeedV2(enum.Enum):
Silent = 101
Standard = 102
Medium = 103
Turbo = 104
Gentle = 105
ROCKROBO_V1 = "rockrobo.vacuum.v1"
class Vacuum(Device):
"""Main class representing the vacuum."""
def __init__(
self, ip: str, token: str = None, start_id: int = 0, debug: int = 0
) -> None:
super().__init__(ip, token, start_id, debug)
self.manual_seqnum = -1
self.model = None
self._fanspeeds = FanspeedV1
@command()
def start(self):
"""Start cleaning."""
return self.send("app_start")
@command()
def stop(self):
"""Stop cleaning.
Note, prefer 'pause' instead of this for wider support.
Some newer vacuum models do not support this command.
"""
return self.send("app_stop")
@command()
def spot(self):
"""Start spot cleaning."""
return self.send("app_spot")
@command()
def pause(self):
"""Pause cleaning."""
return self.send("app_pause")
@command()
def resume_or_start(self):
"""A shortcut for resuming or starting cleaning."""
status = self.status()
if status.in_zone_cleaning and status.is_paused:
return self.resume_zoned_clean()
return self.start()
@command()
def home(self):
"""Stop cleaning and return home."""
self.send("app_pause")
return self.send("app_charge")
@command(click.argument("x_coord", type=int), click.argument("y_coord", type=int))
def goto(self, x_coord: int, y_coord: int):
"""Go to specific target.
:param int x_coord: x coordinate
:param int y_coord: y coordinate"""
return self.send("app_goto_target", [x_coord, y_coord])
@command(click.argument("zones", type=LiteralParamType(), required=True))
def zoned_clean(self, zones: List):
"""Clean zones.
:param List zones: List of zones to clean: [[x1,y1,x2,y2, iterations],[x1,y1,x2,y2, iterations]]"""
return self.send("app_zoned_clean", zones)
@command()
def resume_zoned_clean(self):
"""Resume zone cleaning after being paused."""
return self.send("resume_zoned_clean")
@command()
def manual_start(self):
"""Start manual control mode."""
self.manual_seqnum = 0
return self.send("app_rc_start")
@command()
def manual_stop(self):
"""Stop manual control mode."""
self.manual_seqnum = 0
return self.send("app_rc_end")
@command(
click.argument("rotation", type=int),
click.argument("velocity", type=float),
click.argument("duration", type=int, required=False, default=1500),
)
def manual_control_once(self, rotation: int, velocity: float, duration: int = 1500):
"""Starts the remote control mode and executes
the action once before deactivating the mode."""
number_of_tries = 3
self.manual_start()
while number_of_tries > 0:
if self.status().state_code == 7:
time.sleep(5)
self.manual_control(rotation, velocity, duration)
time.sleep(5)
return self.manual_stop()
time.sleep(2)
number_of_tries -= 1
@command(
click.argument("rotation", type=int),
click.argument("velocity", type=float),
click.argument("duration", type=int, required=False, default=1500),
)
def manual_control(self, rotation: int, velocity: float, duration: int = 1500):
"""Give a command over manual control interface."""
if rotation < -180 or rotation > 180:
raise DeviceException(
"Given rotation is invalid, should " "be ]-180, 180[, was %s" % rotation
)
if velocity < -0.3 or velocity > 0.3:
raise DeviceException(
"Given velocity is invalid, should "
"be ]-0.3, 0.3[, was: %s" % velocity
)
self.manual_seqnum += 1
params = {
"omega": round(math.radians(rotation), 1),
"velocity": velocity,
"duration": duration,
"seqnum": self.manual_seqnum,
}
self.send("app_rc_move", [params])
@command()
def status(self) -> VacuumStatus:
"""Return status of the vacuum."""
return VacuumStatus(self.send("get_status")[0])
def enable_log_upload(self):
raise NotImplementedError("unknown parameters")
# return self.send("enable_log_upload")
@command()
def log_upload_status(self):
# {"result": [{"log_upload_status": 7}], "id": 1}
return self.send("get_log_upload_status")
@command()
def consumable_status(self) -> ConsumableStatus:
"""Return information about consumables."""
return ConsumableStatus(self.send("get_consumable")[0])
@command(click.argument("consumable", type=Consumable))
def consumable_reset(self, consumable: Consumable):
"""Reset consumable information."""
return self.send("reset_consumable", [consumable.value])
@command()
def map(self):
"""Return map token."""
# returns ['retry'] without internet
return self.send("get_map_v1")
@command(click.argument("start", type=bool))
def edit_map(self, start):
"""Start map editing?"""
if start:
return self.send("start_edit_map")[0] == "ok"
else:
return self.send("end_edit_map")[0] == "ok"
@command(click.option("--version", default=1))
def fresh_map(self, version):
"""Return fresh map?"""
if version == 1:
return self.send("get_fresh_map")
elif version == 2:
return self.send("get_fresh_map_v2")
else:
raise VacuumException("Unknown map version: %s" % version)
@command(click.option("--version", default=1))
def persist_map(self, version):
"""Return fresh map?"""
if version == 1:
return self.send("get_persist_map")
elif version == 2:
return self.send("get_persist_map_v2")
else:
raise VacuumException("Unknown map version: %s" % version)
@command(
click.argument("x1", type=int),
click.argument("y1", type=int),
click.argument("x2", type=int),
click.argument("y2", type=int),
)
def create_software_barrier(self, x1, y1, x2, y2):
"""Create software barrier (gen2 only?).
NOTE: Multiple nogo zones and barriers could be added by passing
a list of them to save_map.
Requires new fw version.
3.3.9_001633+?
"""
# First parameter indicates the type, 1 = barrier
payload = [1, x1, y1, x2, y2]
return self.send("save_map", payload)[0] == "ok"
@command(
click.argument("x1", type=int),
click.argument("y1", type=int),
click.argument("x2", type=int),
click.argument("y2", type=int),
click.argument("x3", type=int),
click.argument("y3", type=int),
click.argument("x4", type=int),
click.argument("y4", type=int),
)
def create_nogo_zone(self, x1, y1, x2, y2, x3, y3, x4, y4):
"""Create a rectangular no-go zone (gen2 only?).
NOTE: Multiple nogo zones and barriers could be added by passing
a list of them to save_map.
Requires new fw version.
3.3.9_001633+?
"""
# First parameter indicates the type, 0 = zone
payload = [0, x1, y1, x2, y2, x3, y3, x4, y4]
return self.send("save_map", payload)[0] == "ok"
@command(click.argument("enable", type=bool))
def enable_lab_mode(self, enable):
"""Enable persistent maps and software barriers.
This is required to use create_nogo_zone and create_software_barrier
commands."""
return self.send("set_lab_status", int(enable))["ok"]
@command()
def clean_history(self) -> CleaningSummary:
"""Return generic cleaning history."""
return CleaningSummary(self.send("get_clean_summary"))
@command()
def last_clean_details(self) -> Optional[CleaningDetails]:
"""Return details from the last cleaning.
Returns None if there has been no cleanups."""
history = self.clean_history()
if not history.ids:
return None
last_clean_id = history.ids.pop(0)
return self.clean_details(last_clean_id, return_list=False)
@command(
click.argument("id_", type=int, metavar="ID"),
click.argument("return_list", type=bool, default=False),
)
def clean_details(
self, id_: int, return_list=True
) -> Union[List[CleaningDetails], Optional[CleaningDetails]]:
"""Return details about specific cleaning."""
details = self.send("get_clean_record", [id_])
if not details:
_LOGGER.warning("No cleaning record found for id %s" % id_)
return None
if return_list:
_LOGGER.warning(
"This method will be returning the details "
"without wrapping them into a list in the "
"near future. The current behavior can be "
"kept by passing return_list=True and this "
"warning will be removed when the default gets "
"changed."
)
return [CleaningDetails(entry) for entry in details]
if len(details) > 1:
_LOGGER.warning("Got multiple clean details, returning the first")
res = CleaningDetails(details.pop())
return res
@command()
def find(self):
"""Find the robot."""
return self.send("find_me", [""])
@command()
def timer(self) -> List[Timer]:
"""Return a list of timers."""
timers = list()
for rec in self.send("get_timer", [""]):
timers.append(Timer(rec))
return timers
@command(
click.argument("cron"),
click.argument("command", required=False, default=""),
click.argument("parameters", required=False, default=""),
)
def add_timer(self, cron: str, command: str, parameters: str):
"""Add a timer.
:param cron: schedule in cron format
:param command: ignored by the vacuum.
:param parameters: ignored by the vacuum."""
import time
ts = int(round(time.time() * 1000))
return self.send("set_timer", [[str(ts), [cron, [command, parameters]]]])
@command(click.argument("timer_id", type=int))
def delete_timer(self, timer_id: int):
"""Delete a timer with given ID.
:param int timer_id: Timer ID"""
return self.send("del_timer", [str(timer_id)])
@command(
click.argument("timer_id", type=int), click.argument("mode", type=TimerState)
)
def update_timer(self, timer_id: int, mode: TimerState):
"""Update a timer with given ID.
:param int timer_id: Timer ID
:param TimerStae mode: either On or Off"""
if mode != TimerState.On and mode != TimerState.Off:
raise DeviceException("Only 'On' or 'Off' are allowed")
return self.send("upd_timer", [str(timer_id), mode.value])
@command()
def dnd_status(self):
"""Returns do-not-disturb status."""
# {'result': [{'enabled': 1, 'start_minute': 0, 'end_minute': 0,
# 'start_hour': 22, 'end_hour': 8}], 'id': 1}
return DNDStatus(self.send("get_dnd_timer")[0])
@command(
click.argument("start_hr", type=int),
click.argument("start_min", type=int),
click.argument("end_hr", type=int),
click.argument("end_min", type=int),
)
def set_dnd(self, start_hr: int, start_min: int, end_hr: int, end_min: int):
"""Set do-not-disturb.
:param int start_hr: Start hour
:param int start_min: Start minute
:param int end_hr: End hour
:param int end_min: End minute"""
return self.send("set_dnd_timer", [start_hr, start_min, end_hr, end_min])
@command()
def disable_dnd(self):
"""Disable do-not-disturb."""
return self.send("close_dnd_timer", [""])
@command(click.argument("speed", type=int))
def set_fan_speed(self, speed: int):
"""Set fan speed.
:param int speed: Fan speed to set"""
# speed = [38, 60 or 77]
return self.send("set_custom_mode", [speed])
@command()
def fan_speed(self):
"""Return fan speed."""
return self.send("get_custom_mode")[0]
def _autodetect_model(self):
"""Detect the model of the vacuum.
For the moment this is used only for the fanspeeds,
but that could be extended to cover other supported features."""
try:
info = self.info()
self.model = info.model
except TypeError:
# cloud-blocked vacuums will not return proper payloads
self._fanspeeds = FanspeedV1
self.model = ROCKROBO_V1
_LOGGER.debug("Unable to query model, falling back to %s", self._fanspeeds)
return
_LOGGER.info("model: %s", self.model)
if info.model == ROCKROBO_V1:
_LOGGER.debug("Got robov1, checking for firmware version")
fw_version = info.firmware_version
version, build = fw_version.split("_")
version = tuple(map(int, version.split(".")))
if version >= (3, 5, 7):
self._fanspeeds = FanspeedV2
else:
self._fanspeeds = FanspeedV1
else:
self._fanspeeds = FanspeedV2
_LOGGER.debug(
"Using new fanspeed mapping %s for %s", self._fanspeeds, info.model
)
@command()
def fan_speed_presets(self) -> Dict[str, int]:
"""Return dictionary containing supported fan speeds."""
if self.model is None:
self._autodetect_model()
return {x.name: x.value for x in list(self._fanspeeds)}
@command()
def sound_info(self):
"""Get voice settings."""
return SoundStatus(self.send("get_current_sound")[0])
@command(
click.argument("url"),
click.argument("md5sum"),
click.argument("sound_id", type=int),
)
def install_sound(self, url: str, md5sum: str, sound_id: int):
"""Install sound from the given url."""
payload = {"url": url, "md5": md5sum, "sid": int(sound_id)}
return SoundInstallStatus(self.send("dnld_install_sound", payload)[0])
@command()
def sound_install_progress(self):
"""Get sound installation progress."""
return SoundInstallStatus(self.send("get_sound_progress")[0])
@command()
def sound_volume(self) -> int:
"""Get sound volume."""
return self.send("get_sound_volume")[0]
@command(click.argument("vol", type=int))
def set_sound_volume(self, vol: int):
"""Set sound volume [0-100]."""
return self.send("change_sound_volume", [vol])
@command()
def test_sound_volume(self):
"""Test current sound volume."""
return self.send("test_sound_volume")
@command()
def serial_number(self):
"""Get serial number."""
serial = self.send("get_serial_number")
if isinstance(serial, list):
return serial[0]["serial_number"]
return serial
@command()
def locale(self):
"""Return locale information."""
return self.send("app_get_locale")
@command()
def timezone(self):
"""Get the timezone."""
return self.send("get_timezone")[0]
def set_timezone(self, new_zone):
"""Set the timezone."""
return self.send("set_timezone", [new_zone])[0] == "ok"
def configure_wifi(self, ssid, password, uid=0, timezone=None):
"""Configure the wifi settings."""
extra_params = {}
if timezone is not None:
now = datetime.datetime.now(pytz.timezone(timezone))
offset_as_float = now.utcoffset().total_seconds() / 60 / 60
extra_params["tz"] = timezone
extra_params["gmt_offset"] = offset_as_float
return super().configure_wifi(ssid, password, uid, extra_params)
@command()
def carpet_mode(self):
"""Get carpet mode settings"""
return CarpetModeStatus(self.send("get_carpet_mode")[0])
@command(
click.argument("enabled", required=True, type=bool),
click.argument("stall_time", required=False, default=10, type=int),
click.argument("low", required=False, default=400, type=int),
click.argument("high", required=False, default=500, type=int),
click.argument("integral", required=False, default=450, type=int),
)
def set_carpet_mode(
self,
enabled: bool,
stall_time: int = 10,
low: int = 400,
high: int = 500,
integral: int = 450,
):
"""Set the carpet mode."""
click.echo("Setting carpet mode: %s" % enabled)
data = {
"enable": int(enabled),
"stall_time": stall_time,
"current_low": low,
"current_high": high,
"current_integral": integral,
}
return self.send("set_carpet_mode", [data])[0] == "ok"
@command()
def stop_zoned_clean(self):
"""Stop cleaning a zone."""
return self.send("stop_zoned_clean")
@command()
def stop_segment_clean(self):
"""Stop cleaning a segment."""
return self.send("stop_segment_clean")
@command()
def resume_segment_clean(self):
"""Resuming cleaning a segment."""
return self.send("resume_segment_clean")
@command(click.argument("segments", type=LiteralParamType(), required=True))
def segment_clean(self, segments: List):
"""Clean segments.
:param List segments: List of segments to clean: [16,17,18]"""
return self.send("app_segment_clean", segments)
@command()
def get_room_mapping(self):
"""Retrieves a list of segments."""
return self.send("get_room_mapping")
@command()
def get_backup_maps(self):
"""Get backup maps."""
return self.send("get_recover_maps")
@command(click.argument("id", type=int))
def use_backup_map(self, id: int):
"""Set backup map."""
click.echo("Setting the map %s as active" % id)
return self.send("recover_map", [id])
@command()
def get_segment_status(self):
"""Get the status of a segment."""
return self.send("get_segment_status")
@property
def raw_id(self):
return self._protocol.raw_id
def name_segment(self):
raise NotImplementedError("unknown parameters")
# return self.send("name_segment")
def merge_segment(self):
raise NotImplementedError("unknown parameters")
# return self.send("merge_segment")
def split_segment(self):
raise NotImplementedError("unknown parameters")
# return self.send("split_segment")
@classmethod
def get_device_group(cls):
@click.pass_context
def callback(ctx, *args, id_file, **kwargs):
gco = ctx.find_object(GlobalContextObject)
if gco:
kwargs["debug"] = gco.debug
start_id = manual_seq = 0
try:
with open(id_file, "r") as f:
x = json.load(f)
start_id = x.get("seq", 0)
manual_seq = x.get("manual_seq", 0)
_LOGGER.debug("Read stored sequence ids: %s", x)
except (FileNotFoundError, TypeError, ValueError):
pass
ctx.obj = cls(*args, start_id=start_id, **kwargs)
ctx.obj.manual_seqnum = manual_seq
dg = DeviceGroup(
cls,
params=DeviceGroup.DEFAULT_PARAMS
+ [
click.Option(
["--id-file"],
type=click.Path(dir_okay=False, writable=True),
default=os.path.join(
user_cache_dir("python-miio"), "python-mirobo.seq"
),
)
],
callback=callback,
)
@dg.resultcallback()
@dg.device_pass
def cleanup(vac: Vacuum, *args, **kwargs):
if vac.ip is None: # dummy Device for discovery, skip teardown
return
id_file = kwargs["id_file"]
seqs = {"seq": vac._protocol.raw_id, "manual_seq": vac.manual_seqnum}
_LOGGER.debug("Writing %s to %s", seqs, id_file)
path_obj = pathlib.Path(id_file)
cache_dir = path_obj.parents[0]
cache_dir.mkdir(parents=True, exist_ok=True)
with open(id_file, "w") as f:
json.dump(seqs, f)
return dg
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585505798.0
python-miio-0.5.0.1/miio/vacuum_cli.py 0000644 0001750 0001750 00000044636 00000000000 017237 0 ustar 00tpr tpr 0000000 0000000 import ast
import json
import logging
import pathlib
import sys
import threading
import time
from pprint import pformat as pf
from typing import Any, List # noqa: F401
import click
from appdirs import user_cache_dir
from tqdm import tqdm
import miio # noqa: E402
from miio.click_common import (
ExceptionHandlerGroup,
LiteralParamType,
validate_ip,
validate_token,
)
from miio.device import UpdateState
from miio.miioprotocol import MiIOProtocol
from miio.updater import OneShotServer
_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.Device, ensure=True)
@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup)
@click.option("--ip", envvar="MIROBO_IP", callback=validate_ip)
@click.option("--token", envvar="MIROBO_TOKEN", callback=validate_token)
@click.option("-d", "--debug", default=False, count=True)
@click.option(
"--id-file",
type=click.Path(dir_okay=False, writable=True),
default=user_cache_dir("python-miio") + "/python-mirobo.seq",
)
@click.version_option()
@click.pass_context
def cli(ctx, ip: str, token: str, debug: int, id_file: str):
"""A tool to command Xiaomi Vacuum robot."""
if debug:
logging.basicConfig(level=logging.DEBUG)
_LOGGER.info("Debug mode active")
else:
logging.basicConfig(level=logging.INFO)
# if we are scanning, we do not try to connect.
if ctx.invoked_subcommand == "discover":
ctx.obj = "discover"
return
if ip is None or token is None:
click.echo("You have to give ip and token!")
sys.exit(-1)
start_id = manual_seq = 0
try:
with open(id_file, "r") as f:
x = json.load(f)
start_id = x.get("seq", 0)
manual_seq = x.get("manual_seq", 0)
_LOGGER.debug("Read stored sequence ids: %s", x)
except (FileNotFoundError, TypeError, ValueError):
pass
vac = miio.Vacuum(ip, token, start_id, debug)
vac.manual_seqnum = manual_seq
_LOGGER.debug("Connecting to %s with token %s", ip, token)
ctx.obj = vac
if ctx.invoked_subcommand is None:
ctx.invoke(status)
cleanup(vac, id_file=id_file)
@cli.resultcallback()
@pass_dev
def cleanup(vac: miio.Vacuum, *args, **kwargs):
if vac.ip is None: # dummy Device for discovery, skip teardown
return
id_file = kwargs["id_file"]
seqs = {"seq": vac.raw_id, "manual_seq": vac.manual_seqnum}
_LOGGER.debug("Writing %s to %s", seqs, id_file)
path_obj = pathlib.Path(id_file)
dir = path_obj.parents[0]
try:
dir.mkdir(parents=True)
except FileExistsError:
pass # after dropping py3.4 support, use exist_ok for mkdir
with open(id_file, "w") as f:
json.dump(seqs, f)
@cli.command()
@click.option("--handshake", type=bool, default=False)
def discover(handshake):
"""Search for robots in the network."""
if handshake:
MiIOProtocol.discover()
else:
miio.Discovery.discover_mdns()
@cli.command()
@pass_dev
def status(vac: miio.Vacuum):
"""Returns the state information."""
res = vac.status()
if not res:
return # bail out
if res.error_code:
click.echo(click.style("Error: %s !" % res.error, bold=True, fg="red"))
click.echo(click.style("State: %s" % res.state, bold=True))
click.echo("Battery: %s %%" % res.battery)
click.echo("Fanspeed: %s %%" % res.fanspeed)
click.echo("Cleaning since: %s" % res.clean_time)
click.echo("Cleaned area: %s m²" % res.clean_area)
# click.echo("DND enabled: %s" % res.dnd)
# click.echo("Map present: %s" % res.map)
# click.echo("in_cleaning: %s" % res.in_cleaning)
@cli.command()
@pass_dev
def consumables(vac: miio.Vacuum):
"""Return consumables status."""
res = vac.consumable_status()
click.echo("Main brush: %s (left %s)" % (res.main_brush, res.main_brush_left))
click.echo("Side brush: %s (left %s)" % (res.side_brush, res.side_brush_left))
click.echo("Filter: %s (left %s)" % (res.filter, res.filter_left))
click.echo("Sensor dirty: %s (left %s)" % (res.sensor_dirty, res.sensor_dirty_left))
@cli.command()
@click.argument("name", type=str, required=True)
@pass_dev
def reset_consumable(vac: miio.Vacuum, name):
"""Reset consumable state.
Allowed values: main_brush, side_brush, filter, sensor_dirty
"""
from miio.vacuum import Consumable
if name == "main_brush":
consumable = Consumable.MainBrush
elif name == "side_brush":
consumable = Consumable.SideBrush
elif name == "filter":
consumable = Consumable.Filter
elif name == "sensor_dirty":
consumable = Consumable.SensorDirty
else:
click.echo("Unexpected state name: %s" % name)
return
click.echo(
"Resetting consumable '%s': %s" % (name, vac.consumable_reset(consumable))
)
@cli.command()
@pass_dev
def start(vac: miio.Vacuum):
"""Start cleaning."""
click.echo("Starting cleaning: %s" % vac.start())
@cli.command()
@pass_dev
def spot(vac: miio.Vacuum):
"""Start spot cleaning."""
click.echo("Starting spot cleaning: %s" % vac.spot())
@cli.command()
@pass_dev
def pause(vac: miio.Vacuum):
"""Pause cleaning."""
click.echo("Pausing: %s" % vac.pause())
@cli.command()
@pass_dev
def stop(vac: miio.Vacuum):
"""Stop cleaning."""
click.echo("Stop cleaning: %s" % vac.stop())
@cli.command()
@pass_dev
def home(vac: miio.Vacuum):
"""Return home."""
click.echo("Requesting return to home: %s" % vac.home())
@cli.command()
@pass_dev
@click.argument("x_coord", type=int)
@click.argument("y_coord", type=int)
def goto(vac: miio.Vacuum, x_coord: int, y_coord: int):
"""Go to specific target."""
click.echo("Going to target : %s" % vac.goto(x_coord, y_coord))
@cli.command()
@pass_dev
@click.argument("zones", type=LiteralParamType(), required=True)
def zoned_clean(vac: miio.Vacuum, zones: List):
"""Clean zone."""
click.echo("Cleaning zone(s) : %s" % vac.zoned_clean(zones))
@cli.group()
@pass_dev
# @click.argument('command', required=False)
def manual(vac: miio.Vacuum):
"""Control the robot manually."""
command = ""
if command == "start":
click.echo("Starting manual control")
return vac.manual_start()
if command == "stop":
click.echo("Stopping manual control")
return vac.manual_stop()
# if not vac.manual_mode and command :
@manual.command() # noqa: F811 # redefinition of start
@pass_dev
def start(vac: miio.Vacuum):
"""Activate the manual mode."""
click.echo("Activating manual controls")
return vac.manual_start()
@manual.command() # noqa: F811 # redefinition of stop
@pass_dev
def stop(vac: miio.Vacuum):
"""Deactivate the manual mode."""
click.echo("Deactivating manual controls")
return vac.manual_stop()
@manual.command()
@pass_dev
@click.argument("degrees", type=int)
def left(vac: miio.Vacuum, degrees: int):
"""Turn to left."""
click.echo("Turning %s degrees left" % degrees)
return vac.manual_control(degrees, 0)
@manual.command()
@pass_dev
@click.argument("degrees", type=int)
def right(vac: miio.Vacuum, degrees: int):
"""Turn to right."""
click.echo("Turning right")
return vac.manual_control(-degrees, 0)
@manual.command()
@click.argument("amount", type=float)
@pass_dev
def forward(vac: miio.Vacuum, amount: float):
"""Run forwards."""
click.echo("Moving forwards")
return vac.manual_control(0, amount)
@manual.command()
@click.argument("amount", type=float)
@pass_dev
def backward(vac: miio.Vacuum, amount: float):
"""Run backwards."""
click.echo("Moving backwards")
return vac.manual_control(0, -amount)
@manual.command()
@pass_dev
@click.argument("rotation", type=float)
@click.argument("velocity", type=float)
@click.argument("duration", type=int)
def move(vac: miio.Vacuum, rotation: int, velocity: float, duration: int):
"""Pass raw manual values"""
return vac.manual_control(rotation, velocity, duration)
@cli.command()
@click.argument("cmd", required=False)
@click.argument("start_hr", type=int, required=False)
@click.argument("start_min", type=int, required=False)
@click.argument("end_hr", type=int, required=False)
@click.argument("end_min", type=int, required=False)
@pass_dev
def dnd(
vac: miio.Vacuum, cmd: str, start_hr: int, start_min: int, end_hr: int, end_min: int
):
"""Query and adjust do-not-disturb mode."""
if cmd == "off":
click.echo("Disabling DND..")
print(vac.disable_dnd())
elif cmd == "on":
click.echo(
"Enabling DND %s:%s to %s:%s" % (start_hr, start_min, end_hr, end_min)
)
click.echo(vac.set_dnd(start_hr, start_min, end_hr, end_min))
else:
x = vac.dnd_status()
click.echo(
click.style(
"Between %s and %s (enabled: %s)" % (x.start, x.end, x.enabled),
bold=x.enabled,
)
)
@cli.command()
@click.argument("speed", type=int, required=False)
@pass_dev
def fanspeed(vac: miio.Vacuum, speed):
"""Query and adjust the fan speed."""
if speed:
click.echo("Setting fan speed to %s" % speed)
vac.set_fan_speed(speed)
else:
click.echo("Current fan speed: %s" % vac.fan_speed())
@cli.group(invoke_without_command=True)
@pass_dev
@click.pass_context
def timer(ctx, vac: miio.Vacuum):
"""List and modify existing timers."""
if ctx.invoked_subcommand is not None:
return
timers = vac.timer()
click.echo("Timezone: %s\n" % vac.timezone())
for idx, timer in enumerate(timers):
color = "green" if timer.enabled else "yellow"
click.echo(
click.style(
"Timer #%s, id %s (ts: %s)" % (idx, timer.id, timer.ts),
bold=True,
fg=color,
)
)
click.echo(" %s" % timer.cron)
min, hr, x, y, days = timer.cron.split(" ")
cron = "%s %s %s %s %s" % (min, hr, x, y, days)
click.echo(" %s" % cron)
@timer.command()
@click.option("--cron")
@click.option("--command", default="", required=False)
@click.option("--params", default="", required=False)
@pass_dev
def add(vac: miio.Vacuum, cron, command, params):
"""Add a timer."""
click.echo(vac.add_timer(cron, command, params))
@timer.command()
@click.argument("timer_id", type=int, required=True)
@pass_dev
def delete(vac: miio.Vacuum, timer_id):
"""Delete a timer."""
click.echo(vac.delete_timer(timer_id))
@timer.command()
@click.argument("timer_id", type=int, required=True)
@click.option("--enable", is_flag=True)
@click.option("--disable", is_flag=True)
@pass_dev
def update(vac: miio.Vacuum, timer_id, enable, disable):
"""Enable/disable a timer."""
from miio.vacuum import TimerState
if enable and not disable:
vac.update_timer(timer_id, TimerState.On)
elif disable and not enable:
vac.update_timer(timer_id, TimerState.Off)
else:
click.echo("You need to specify either --enable or --disable")
@cli.command()
@pass_dev
def find(vac: miio.Vacuum):
"""Find the robot."""
click.echo("Sending find the robot calls.")
click.echo(vac.find())
@cli.command()
@pass_dev
def map(vac: miio.Vacuum):
"""Return the map token."""
click.echo(vac.map())
@cli.command()
@pass_dev
def info(vac: miio.Vacuum):
"""Return device information."""
try:
res = vac.info()
click.echo("%s" % res)
_LOGGER.debug("Full response: %s", pf(res.raw))
except TypeError:
click.echo(
"Unable to fetch info, this can happen when the vacuum "
"is not connected to the Xiaomi cloud."
)
@cli.command()
@pass_dev
def cleaning_history(vac: miio.Vacuum):
"""Query the cleaning history."""
res = vac.clean_history()
click.echo("Total clean count: %s" % res.count)
click.echo("Cleaned for: %s (area: %s m²)" % (res.total_duration, res.total_area))
click.echo()
for idx, id_ in enumerate(res.ids):
details = vac.clean_details(id_, return_list=False)
color = "green" if details.complete else "yellow"
click.echo(
click.style(
"Clean #%s: %s-%s (complete: %s, error: %s)"
% (idx, details.start, details.end, details.complete, details.error),
bold=True,
fg=color,
)
)
click.echo(" Area cleaned: %s m²" % details.area)
click.echo(" Duration: (%s)" % details.duration)
click.echo()
@cli.command()
@click.argument("volume", type=int, required=False)
@click.option("--test", "test_mode", is_flag=True, help="play a test tune")
@pass_dev
def sound(vac: miio.Vacuum, volume: int, test_mode: bool):
"""Query and change sound settings."""
if volume is not None:
click.echo("Setting sound volume to %s" % volume)
vac.set_sound_volume(volume)
if test_mode:
vac.test_sound_volume()
click.echo("Current sound: %s" % vac.sound_info())
click.echo("Current volume: %s" % vac.sound_volume())
click.echo("Install progress: %s" % vac.sound_install_progress())
@cli.command()
@click.argument("url")
@click.argument("md5sum", required=False, default=None)
@click.option("--sid", type=int, required=False, default=10000)
@click.option("--ip", required=False)
@pass_dev
def install_sound(vac: miio.Vacuum, url: str, md5sum: str, sid: int, ip: str):
"""Install a sound.
When passing a local file this will create a self-hosting server
for the given file and the md5sum will be calculated automatically.
For URLs you have to specify the md5sum manually.
`--ip` can be used to override automatically detected IP address for
the device to contact for the update.
"""
click.echo("Installing from %s (md5: %s) for id %s" % (url, md5sum, sid))
local_url = None
server = None
if url.startswith("http"):
if md5sum is None:
click.echo("You need to pass md5 when using URL for updating.")
return
local_url = url
else:
server = OneShotServer(url)
local_url = server.url(ip)
md5sum = server.md5
t = threading.Thread(target=server.serve_once)
t.start()
click.echo("Hosting file at %s" % local_url)
click.echo(vac.install_sound(local_url, md5sum, sid))
progress = vac.sound_install_progress()
while progress.is_installing:
progress = vac.sound_install_progress()
print("%s (%s %%)" % (progress.state.name, progress.progress))
time.sleep(1)
progress = vac.sound_install_progress()
if progress.is_errored:
click.echo("Error during installation: %s" % progress.error)
else:
click.echo("Installation of sid '%s' complete!" % sid)
if server is not None:
t.join()
@cli.command()
@pass_dev
def serial_number(vac: miio.Vacuum):
"""Query serial number."""
click.echo("Serial#: %s" % vac.serial_number())
@cli.command()
@click.argument("tz", required=False)
@pass_dev
def timezone(vac: miio.Vacuum, tz=None):
"""Query or set the timezone."""
if tz is not None:
click.echo("Setting timezone to: %s" % tz)
click.echo(vac.set_timezone(tz))
else:
click.echo("Timezone: %s" % vac.timezone())
@cli.command()
@click.argument("enabled", required=False, type=bool)
@pass_dev
def carpet_mode(vac: miio.Vacuum, enabled=None):
"""Query or set the carpet mode."""
if enabled is None:
click.echo(vac.carpet_mode())
else:
click.echo(vac.set_carpet_mode(enabled))
@cli.command()
@click.argument("ssid", required=True)
@click.argument("password", required=True)
@click.argument("uid", type=int, required=False)
@click.option("--timezone", type=str, required=False, default=None)
@pass_dev
def configure_wifi(vac: miio.Vacuum, ssid: str, password: str, uid: int, timezone: str):
"""Configure the wifi settings.
Note that some newer firmwares may expect you to define the timezone
by using --timezone."""
click.echo("Configuring wifi to SSID: %s" % ssid)
click.echo(vac.configure_wifi(ssid, password, uid, timezone))
@cli.command()
@pass_dev
def update_status(vac: miio.Vacuum):
"""Return update state and progress."""
update_state = vac.update_state()
click.echo("Update state: %s" % update_state)
if update_state == UpdateState.Downloading:
click.echo("Update progress: %s" % vac.update_progress())
@cli.command()
@click.argument("url", required=True)
@click.argument("md5", required=False, default=None)
@click.option("--ip", required=False)
@pass_dev
def update_firmware(vac: miio.Vacuum, url: str, md5: str, ip: str):
"""Update device firmware.
If `url` starts with http* it is expected to be an URL.
In that case md5sum of the file has to be given.
`--ip` can be used to override automatically detected IP address for
the device to contact for the update.
"""
# TODO Check that the device is in updateable state.
click.echo("Going to update from %s" % url)
if url.lower().startswith("http"):
if md5 is None:
click.echo("You need to pass md5 when using URL for updating.")
return
click.echo("Using %s (md5: %s)" % (url, md5))
else:
server = OneShotServer(url)
url = server.url(ip)
t = threading.Thread(target=server.serve_once)
t.start()
click.echo("Hosting file at %s" % url)
md5 = server.md5
update_res = vac.update(url, md5)
if update_res:
click.echo("Update started!")
else:
click.echo("Starting the update failed: %s" % update_res)
with tqdm(total=100) as t:
state = vac.update_state()
while state == UpdateState.Downloading:
try:
state = vac.update_state()
progress = vac.update_progress()
except: # we may not get our messages through during upload # noqa
continue
if state == UpdateState.Installing:
click.echo("Installation started, please wait until the vacuum reboots")
break
t.update(progress - t.n)
t.set_description("%s" % state.name)
time.sleep(1)
@cli.command()
@click.argument("cmd", required=True)
@click.argument("parameters", required=False)
@pass_dev
def raw_command(vac: miio.Vacuum, cmd, parameters):
"""Run a raw command."""
params = [] # type: Any
if parameters:
params = ast.literal_eval(parameters)
click.echo("Sending cmd %s with params %s" % (cmd, params))
click.echo(vac.raw_command(cmd, params))
if __name__ == "__main__":
cli()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/vacuumcontainers.py 0000644 0001750 0001750 00000041062 00000000000 020464 0 ustar 00tpr tpr 0000000 0000000 # -*- coding: UTF-8 -*#
from datetime import datetime, time, timedelta
from enum import IntEnum
from typing import Any, Dict, List
from .utils import deprecated, pretty_seconds, pretty_time
def pretty_area(x: float) -> float:
return int(x) / 1000000
error_codes = { # from vacuum_cleaner-EN.pdf
0: "No error",
1: "Laser distance sensor error",
2: "Collision sensor error",
3: "Wheels on top of void, move robot",
4: "Clean hovering sensors, move robot",
5: "Clean main brush",
6: "Clean side brush",
7: "Main wheel stuck?",
8: "Device stuck, clean area",
9: "Dust collector missing",
10: "Clean filter",
11: "Stuck in magnetic barrier",
12: "Low battery",
13: "Charging fault",
14: "Battery fault",
15: "Wall sensors dirty, wipe them",
16: "Place me on flat surface",
17: "Side brushes problem, reboot me",
18: "Suction fan problem",
19: "Unpowered charging station",
21: "Laser disance sensor blocked",
22: "Clean the dock charging contacts",
23: "Docking station not reachable",
24: "No-go zone or invisible wall detected",
}
class VacuumStatus:
"""Container for status reports from the vacuum."""
def __init__(self, data: Dict[str, Any]) -> None:
# {'result': [{'state': 8, 'dnd_enabled': 1, 'clean_time': 0,
# 'msg_ver': 4, 'map_present': 1, 'error_code': 0, 'in_cleaning': 0,
# 'clean_area': 0, 'battery': 100, 'fan_power': 20, 'msg_seq': 320}],
# 'id': 1}
# v8 new items
# clean_mode, begin_time, clean_trigger,
# back_trigger, clean_strategy, and completed
# TODO: create getters if wanted
#
# {"msg_ver":8,"msg_seq":60,"state":5,"battery":93,"clean_mode":0,
# "fan_power":50,"error_code":0,"map_present":1,"in_cleaning":1,
# "dnd_enabled":0,"begin_time":1534333389,"clean_time":21,
# "clean_area":202500,"clean_trigger":2,"back_trigger":0,
# "completed":0,"clean_strategy":1}
self.data = data
@property
def state_code(self) -> int:
"""State code as returned by the device."""
return int(self.data["state"])
@property
def state(self) -> str:
"""Human readable state description, see also :func:`state_code`."""
states = {
1: "Starting",
2: "Charger disconnected",
3: "Idle",
4: "Remote control active",
5: "Cleaning",
6: "Returning home",
7: "Manual mode",
8: "Charging",
9: "Charging problem",
10: "Paused",
11: "Spot cleaning",
12: "Error",
13: "Shutting down",
14: "Updating",
15: "Docking",
16: "Going to target",
17: "Zoned cleaning",
18: "Segment cleaning",
100: "Charging complete",
101: "Device offline",
}
try:
return states[int(self.state_code)]
except KeyError:
return "Definition missing for state %s" % self.state_code
@property
def error_code(self) -> int:
"""Error code as returned by the device."""
return int(self.data["error_code"])
@property
def error(self) -> str:
"""Human readable error description, see also :func:`error_code`."""
try:
return error_codes[self.error_code]
except KeyError:
return "Definition missing for error %s" % self.error_code
@property
def battery(self) -> int:
"""Remaining battery in percentage. """
return int(self.data["battery"])
@property
def fanspeed(self) -> int:
"""Current fan speed."""
return int(self.data["fan_power"])
@property
def clean_time(self) -> timedelta:
"""Time used for cleaning (if finished, shows how long it took)."""
return pretty_seconds(self.data["clean_time"])
@property
def clean_area(self) -> float:
"""Cleaned area in m2."""
return pretty_area(self.data["clean_area"])
@property
@deprecated("Use vacuum's dnd_status() instead, which is more accurate")
def dnd(self) -> bool:
"""DnD status. Use :func:`vacuum.dnd_status` instead of this."""
return bool(self.data["dnd_enabled"])
@property
def map(self) -> bool:
"""Map token."""
return bool(self.data["map_present"])
@property
@deprecated("See is_on")
def in_cleaning(self) -> bool:
"""True if currently cleaning. Please use :func:`is_on` instead of this."""
return self.is_on
# we are not using in_cleaning as it does not seem to work properly.
# return bool(self.data["in_cleaning"])
@property
def in_zone_cleaning(self) -> bool:
"""Return True if the vacuum is in zone cleaning mode."""
return self.data["in_cleaning"] == 2
@property
def is_paused(self) -> bool:
"""Return True if vacuum is paused."""
return self.state_code == 10
@property
def is_on(self) -> bool:
"""True if device is currently cleaning (either automatic, manual,
spot, or zone)."""
return (
self.state_code == 5
or self.state_code == 7
or self.state_code == 11
or self.state_code == 17
)
@property
def got_error(self) -> bool:
"""True if an error has occured."""
return self.error_code != 0
def __repr__(self) -> str:
s = "" % (self.clean_area, self.clean_time)
return s
def __json__(self):
return self.data
class CleaningSummary:
"""Contains summarized information about available cleaning runs."""
def __init__(self, data: List[Any]) -> None:
# total duration, total area, amount of cleans
# [ list, of, ids ]
# { "result": [ 174145, 2410150000, 82,
# [ 1488240000, 1488153600, 1488067200, 1487980800,
# 1487894400, 1487808000, 1487548800 ] ],
# "id": 1 }
self.data = data
@property
def total_duration(self) -> timedelta:
"""Total cleaning duration."""
return pretty_seconds(self.data[0])
@property
def total_area(self) -> float:
"""Total cleaned area."""
return pretty_area(self.data[1])
@property
def count(self) -> int:
"""Number of cleaning runs."""
return int(self.data[2])
@property
def ids(self) -> List[int]:
"""A list of available cleaning IDs, see also :class:`CleaningDetails`."""
return list(self.data[3])
def __repr__(self) -> str:
return (
""
% (self.count, self.total_duration, self.total_area, self.ids) # noqa: E501
)
def __json__(self):
return self.data
class CleaningDetails:
"""Contains details about a specific cleaning run."""
def __init__(self, data: List[Any]) -> None:
# start, end, duration, area, unk, complete
# { "result": [ [ 1488347071, 1488347123, 16, 0, 0, 0 ] ], "id": 1 }
self.data = data
@property
def start(self) -> datetime:
"""When cleaning was started."""
return pretty_time(self.data[0])
@property
def end(self) -> datetime:
"""When cleaning was finished."""
return pretty_time(self.data[1])
@property
def duration(self) -> timedelta:
"""Total duration of the cleaning run."""
return pretty_seconds(self.data[2])
@property
def area(self) -> float:
"""Total cleaned area."""
return pretty_area(self.data[3])
@property
def error_code(self) -> int:
"""Error code."""
return int(self.data[4])
@property
def error(self) -> str:
"""Error state of this cleaning run."""
return error_codes[self.data[4]]
@property
def complete(self) -> bool:
"""Return True if the cleaning run was complete (e.g. without errors).
see also :func:`error`."""
return bool(self.data[5] == 1)
def __repr__(self) -> str:
return "" % (
self.start,
self.duration,
self.complete,
self.area,
)
def __json__(self):
return self.data
class ConsumableStatus:
"""Container for consumable status information,
including information about brushes and duration until they should be changed.
The methods returning time left are based on the following lifetimes:
- Sensor cleanup time: XXX FIXME
- Main brush: 300 hours
- Side brush: 200 hours
- Filter: 150 hours
"""
def __init__(self, data: Dict[str, Any]) -> None:
# {'id': 1, 'result': [{'filter_work_time': 32454,
# 'sensor_dirty_time': 3798,
# 'side_brush_work_time': 32454,
# 'main_brush_work_time': 32454}]}
self.data = data
self.main_brush_total = timedelta(hours=300)
self.side_brush_total = timedelta(hours=200)
self.filter_total = timedelta(hours=150)
self.sensor_dirty_total = timedelta(hours=30)
@property
def main_brush(self) -> timedelta:
"""Main brush usage time."""
return pretty_seconds(self.data["main_brush_work_time"])
@property
def main_brush_left(self) -> timedelta:
"""How long until the main brush should be changed."""
return self.main_brush_total - self.main_brush
@property
def side_brush(self) -> timedelta:
"""Side brush usage time."""
return pretty_seconds(self.data["side_brush_work_time"])
@property
def side_brush_left(self) -> timedelta:
"""How long until the side brush should be changed."""
return self.side_brush_total - self.side_brush
@property
def filter(self) -> timedelta:
"""Filter usage time."""
return pretty_seconds(self.data["filter_work_time"])
@property
def filter_left(self) -> timedelta:
"""How long until the filter should be changed."""
return self.filter_total - self.filter
@property
def sensor_dirty(self) -> timedelta:
"""Return ``sensor_dirty_time``"""
return pretty_seconds(self.data["sensor_dirty_time"])
@property
def sensor_dirty_left(self) -> timedelta:
return self.sensor_dirty_total - self.sensor_dirty
def __repr__(self) -> str:
return (
""
% ( # noqa: E501
self.main_brush,
self.side_brush,
self.filter,
self.sensor_dirty,
)
)
def __json__(self):
return self.data
class DNDStatus:
"""A container for the do-not-disturb status."""
def __init__(self, data: Dict[str, Any]):
# {'end_minute': 0, 'enabled': 1, 'start_minute': 0,
# 'start_hour': 22, 'end_hour': 8}
self.data = data
@property
def enabled(self) -> bool:
"""True if DnD is enabled."""
return bool(self.data["enabled"])
@property
def start(self) -> time:
"""Start time of DnD."""
return time(hour=self.data["start_hour"], minute=self.data["start_minute"])
@property
def end(self) -> time:
"""End time of DnD."""
return time(hour=self.data["end_hour"], minute=self.data["end_minute"])
def __repr__(self):
return "" % (
self.enabled,
self.start,
self.end,
)
def __json__(self):
return self.data
class Timer:
"""A container for scheduling.
The timers are accessed using an integer ID, which is based on the unix
timestamp of the creation time."""
def __init__(self, data: List[Any]) -> None:
# id / timestamp, enabled, ['', ['command', 'params']
# [['1488667794112', 'off', ['49 22 * * 6', ['start_clean', '']]],
# ['1488667777661', 'off', ['49 21 * * 3,4,5,6', ['start_clean', '']]
# ],
self.data = data
@property
def id(self) -> int:
"""ID which can be used to point to this timer."""
return int(self.data[0])
@property
def ts(self) -> datetime:
"""Pretty-printed ID (timestamp) presentation as time."""
return pretty_time(int(self.data[0]) / 1000)
@property
def enabled(self) -> bool:
"""True if the timer is active."""
return bool(self.data[1] == "on")
@property
def cron(self) -> str:
"""Cron-formated timer string."""
return str(self.data[2][0])
@property
def action(self) -> str:
"""The action to be taken on the given time.
Note, this seems to be always 'start'."""
return str(self.data[2][1])
def __repr__(self) -> str:
return "" % (
self.id,
self.ts,
self.enabled,
self.cron,
)
def __json__(self):
return self.data
class SoundStatus:
"""Container for sound status."""
def __init__(self, data):
# {'sid_in_progress': 0, 'sid_in_use': 1004}
self.data = data
@property
def current(self):
return self.data["sid_in_use"]
@property
def being_installed(self):
return self.data["sid_in_progress"]
def __repr__(self):
return "" % (
self.current,
self.being_installed,
)
def __json__(self):
return self.data
class SoundInstallState(IntEnum):
Unknown = 0
Downloading = 1
Installing = 2
Installed = 3
Error = 4
class SoundInstallStatus:
"""Container for sound installation status."""
def __init__(self, data):
# {'progress': 0, 'sid_in_progress': 0, 'state': 0, 'error': 0}
# error 0 = no error
# error 1 = unknown 1
# error 2 = download error
# error 3 = checksum error
# error 4 = unknown 4
self.data = data
@property
def state(self) -> SoundInstallState:
"""Installation state."""
return SoundInstallState(self.data["state"])
@property
def progress(self) -> int:
"""Progress in percentages."""
return self.data["progress"]
@property
def sid(self) -> int:
"""Sound ID for the sound being installed."""
# this is missing on install confirmation, so let's use get
return self.data.get("sid_in_progress", None)
@property
def error(self) -> int:
"""Error code, 0 is no error, other values unknown."""
return self.data["error"]
@property
def is_installing(self) -> bool:
"""True if install is in progress."""
return (
self.state == SoundInstallState.Downloading
or self.state == SoundInstallState.Installing
)
@property
def is_errored(self) -> bool:
"""True if the state has an error, use `error` to access it."""
return self.state == SoundInstallState.Error
def __repr__(self) -> str:
return (
"" % (self.sid, self.state, self.error, self.progress)
)
def __json__(self):
return self.data
class CarpetModeStatus:
"""Container for carpet mode status."""
def __init__(self, data):
# {'current_high': 500, 'enable': 1, 'current_integral': 450,
# 'current_low': 400, 'stall_time': 10}
self.data = data
@property
def enabled(self) -> bool:
"""True if carpet mode is enabled."""
return self.data["enable"] == 1
@property
def stall_time(self) -> int:
return self.data["stall_time"]
@property
def current_low(self) -> int:
return self.data["current_low"]
@property
def current_high(self) -> int:
return self.data["current_high"]
@property
def current_integral(self) -> int:
return self.data["current_integral"]
def __repr__(self):
return (
""
% (
self.enabled,
self.stall_time,
self.current_low,
self.current_high,
self.current_integral,
)
)
def __json__(self):
return self.data
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507535.0
python-miio-0.5.0.1/miio/version.py 0000644 0001750 0001750 00000000047 00000000000 016561 0 ustar 00tpr tpr 0000000 0000000 # flake8: noqa
__version__ = "0.5.0.1"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500865.0
python-miio-0.5.0.1/miio/viomivacuum.py 0000644 0001750 0001750 00000022565 00000000000 017451 0 ustar 00tpr tpr 0000000 0000000 import logging
import time
from collections import defaultdict
from datetime import timedelta
from enum import Enum
from typing import Dict
import click
from .click_common import EnumType, command, format_output
from .device import Device
from .utils import pretty_seconds
from .vacuumcontainers import DNDStatus
_LOGGER = logging.getLogger(__name__)
ERROR_CODES = {
500: "Radar timed out",
501: "Wheels stuck",
502: "Low battery",
503: "Dust bin missing",
508: "Uneven ground",
509: "Cliff sensor error",
510: "Collision sensor error",
511: "Could not return to dock",
512: "Could not return to dock",
513: "Could not navigate",
514: "Vacuum stuck",
515: "Charging error",
516: "Mop temperature error",
521: "Water tank is not installed",
522: "Mop is not installed",
525: "Insufficient water in water tank",
527: "Remove mop",
528: "Dust bin missing",
529: "Mop and water tank missing",
530: "Mop and water tank missing",
531: "Water tank is not installed",
2101: "Unsufficient battery, continuing cleaning after recharge",
}
class ViomiVacuumSpeed(Enum):
Silent = 0
Standard = 1
Medium = 2
Turbo = 3
class ViomiVacuumState(Enum):
IdleNotDocked = 0
Idle = 1
Idle2 = 2
Cleaning = 3
Returning = 4
Docked = 5
class ViomiMode(Enum):
Vacuum = 0 # No Mop, Vacuum only
VacuumAndMop = 1
Mop = 2
class ViomiLanguage(Enum):
CN = 1 # Chinese (default)
EN = 2 # English
class ViomiLedState(Enum):
Off = 0
On = 1
class ViomiCarpetTurbo(Enum):
Off = 0
Medium = 1
Turbo = 2
class ViomiMovementDirection(Enum):
Forward = 1
Left = 2 # Rotate
Right = 3 # Rotate
Backward = 4
Stop = 5
Unknown = 10
class ViomiBinType(Enum):
Vacuum = 1
Water = 2
VacuumAndWater = 3
class ViomiWaterGrade(Enum):
Low = 11
Medium = 12
High = 13
class ViomiVacuumStatus:
def __init__(self, data):
# ["run_state","mode","err_state","battary_life","box_type","mop_type","s_time","s_area",
# [ 5, 0, 2103, 85, 3, 1, 0, 0,
# "suction_grade","water_grade","remember_map","has_map","is_mop","has_newmap"]'
# 1, 11, 1, 1, 1, 0 ]
self.data = data
@property
def state(self):
"""State of the vacuum."""
return ViomiVacuumState(self.data["run_state"])
@property
def is_on(self) -> bool:
"""True if cleaning."""
return self.state == ViomiVacuumState.Cleaning
@property
def mode(self):
"""Active mode.
TODO: is this same as mop_type property?
"""
return ViomiMode(self.data["mode"])
@property
def error_code(self) -> int:
"""Error code from vacuum."""
return self.data["error_state"]
@property
def error(self) -> str:
"""String presentation for the error code."""
return ERROR_CODES.get(self.error_code, f"Unknown error {self.error_code}")
@property
def battery(self) -> int:
"""Battery in percentage."""
return self.data["battary_life"]
@property
def bin_type(self) -> ViomiBinType:
"""Type of the inserted bin."""
return ViomiBinType(self.data["box_type"])
@property
def clean_time(self) -> timedelta:
"""Cleaning time."""
return pretty_seconds(self.data["s_time"])
@property
def clean_area(self) -> float:
"""Cleaned area in square meters."""
return self.data["s_area"]
@property
def fanspeed(self) -> ViomiVacuumSpeed:
"""Current fan speed."""
return ViomiVacuumSpeed(self.data["suction_grade"])
@property
def water_grade(self) -> ViomiWaterGrade:
"""Water grade."""
return ViomiWaterGrade(self.data["water_grade"])
@property
def remember_map(self) -> bool:
"""True to remember the map."""
return bool(self.data["remember_map"])
@property
def has_map(self) -> bool:
"""True if device has map?"""
return bool(self.data["has_map"])
@property
def has_new_map(self) -> bool:
"""TODO: unknown"""
return bool(self.data["has_newmap"])
@property
def mop_mode(self) -> ViomiMode:
"""Whether mopping is enabled and if so which mode
TODO: is this really the same as mode?
"""
return ViomiMode(self.data["is_mop"])
class ViomiVacuum(Device):
"""Interface for Viomi vacuums (viomi.vacuum.v7)."""
@command(
default_output=format_output(
"",
"State: {result.state}\n"
"Mode: {result.mode}\n"
"Error: {result.error}\n"
"Battery: {result.battery}\n"
"Fan speed: {result.fanspeed}\n"
"Box type: {result.box_type}\n"
"Mop type: {result.mop_type}\n"
"Clean time: {result.clean_time}\n"
"Clean area: {result.clean_area}\n"
"Water level: {result.water_level}\n"
"Remember map: {result.remember_map}\n"
"Has map: {result.has_map}\n"
"Has new map: {result.has_new_map}\n"
"Mop mode: {result.mop_mode}\n",
)
)
def status(self) -> ViomiVacuumStatus:
"""Retrieve properties."""
properties = [
"run_state",
"mode",
"err_state",
"battary_life",
"box_type",
"mop_type",
"s_time",
"s_area",
"suction_grade",
"water_grade",
"remember_map",
"has_map",
"is_mop",
"has_newmap",
]
values = self.send("get_prop", properties)
return ViomiVacuumStatus(defaultdict(lambda: None, zip(properties, values)))
@command()
def start(self):
"""Start cleaning."""
# TODO figure out the parameters
self.send("set_mode_withroom", [0, 1, 0])
@command()
def stop(self):
"""Stop cleaning."""
self.send("set_mode", [0])
@command()
def pause(self):
"""Pause cleaning."""
self.send("set_mode_withroom", [0, 2, 0])
@command(click.argument("speed", type=EnumType(ViomiVacuumSpeed, False)))
def set_fan_speed(self, speed: ViomiVacuumSpeed):
"""Set fanspeed [silent, standard, medium, turbo]."""
self.send("set_suction", [speed.value])
@command(click.argument("watergrade"))
def set_water_grade(self, watergrade: ViomiWaterGrade):
"""Set water grade [low, medium, high]."""
self.send("set_suction", [watergrade.value])
@command()
def home(self):
"""Return to home."""
self.send("set_charge", [1])
@command(
click.argument("direction", type=EnumType(ViomiMovementDirection, False)),
click.option(
"--duration",
type=float,
default=0.5,
help="number of seconds to perform this movement",
),
)
def move(self, direction, duration=0.5):
"""Manual movement."""
start = time.time()
while time.time() - start < duration:
self.send("set_direction", [direction.value])
time.sleep(0.1)
self.send("set_direction", [ViomiMovementDirection.Stop.value])
@command(click.argument("mode", type=EnumType(ViomiMode, False)))
def mop_mode(self, mode):
"""Set mopping mode."""
self.send("set_mop", [mode.value])
@command()
def dnd_status(self):
"""Returns do-not-disturb status."""
status = self.send("get_notdisturb")
return DNDStatus(
dict(
enabled=status[0],
start_hour=status[1],
start_minute=status[2],
end_hour=status[3],
end_minute=status[4],
)
)
@command(
click.option("--disable", is_flag=True),
click.argument("start_hr", type=int),
click.argument("start_min", type=int),
click.argument("end_hr", type=int),
click.argument("end_min", type=int),
)
def set_dnd(
self, disable: bool, start_hr: int, start_min: int, end_hr: int, end_min: int
):
"""Set do-not-disturb.
:param int start_hr: Start hour
:param int start_min: Start minute
:param int end_hr: End hour
:param int end_min: End minute"""
return self.send(
"set_notdisturb",
[0 if disable else 1, start_hr, start_min, end_hr, end_min],
)
@command(click.argument("language", type=EnumType(ViomiLanguage, False)))
def set_language(self, language: ViomiLanguage):
"""Set the device's audio language."""
return self.send("set_language", [language.value])
@command(click.argument("state", type=EnumType(ViomiLedState, False)))
def led(self, state: ViomiLedState):
"""Switch the button leds on or off."""
return self.send("set_light", [state.value])
@command(click.argument("mode", type=EnumType(ViomiCarpetTurbo)))
def carpet_mode(self, mode: ViomiCarpetTurbo):
"""Set the carpet mode."""
return self.send("set_carpetturbo", [mode.value])
@command()
def fan_speed_presets(self) -> Dict[str, int]:
"""Return dictionary containing supported fanspeeds."""
return {x.name: x.value for x in list(ViomiVacuumSpeed)}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585421538.0
python-miio-0.5.0.1/miio/waterpurifier.py 0000644 0001750 0001750 00000013176 00000000000 017773 0 ustar 00tpr tpr 0000000 0000000 import logging
from typing import Any, Dict
from .click_common import command, format_output
from .device import Device
_LOGGER = logging.getLogger(__name__)
class WaterPurifierStatus:
"""Container for status reports from the water purifier."""
def __init__(self, data: Dict[str, Any]) -> None:
self.data = data
@property
def power(self) -> str:
return self.data["power"]
@property
def is_on(self) -> bool:
return self.power == "on"
@property
def mode(self) -> str:
"""Current operation mode."""
return self.data["mode"]
@property
def tds(self) -> str:
return self.data["tds"]
@property
def filter_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["filter1_life"]
@property
def filter_state(self) -> str:
return self.data["filter1_state"]
@property
def filter2_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["filter_life"]
@property
def filter2_state(self) -> str:
return self.data["filter_state"]
@property
def life(self) -> str:
return self.data["life"]
@property
def state(self) -> str:
return self.data["state"]
@property
def level(self) -> str:
return self.data["level"]
@property
def volume(self) -> str:
return self.data["volume"]
@property
def filter(self) -> str:
return self.data["filter"]
@property
def usage(self) -> str:
return self.data["usage"]
@property
def temperature(self) -> str:
return self.data["temperature"]
@property
def uv_filter_life_remaining(self) -> int:
"""Time until the filter should be changed."""
return self.data["uv_life"]
@property
def uv_filter_state(self) -> str:
return self.data["uv_state"]
@property
def valve(self) -> str:
return self.data["elecval_state"]
def __repr__(self) -> str:
return (
""
% (
self.power,
self.mode,
self.tds,
self.filter_life_remaining,
self.filter_state,
self.filter2_life_remaining,
self.filter2_state,
self.life,
self.state,
self.level,
self.volume,
self.filter,
self.usage,
self.temperature,
self.uv_filter_life_remaining,
self.uv_filter_state,
self.valve,
)
)
def __json__(self):
return self.data
class WaterPurifier(Device):
"""Main class representing the waiter purifier."""
@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Mode: {result.mode}\n"
"TDS: {result.tds}\n"
"Filter life remaining: {result.filter_life_remaining}\n"
"Filter state: {result.filter_state}\n"
"Filter2 life remaining: {result.filter2_life_remaining}\n"
"Filter2 state: {result.filter2_state}\n"
"Life remaining: {result.life_remaining}\n"
"State: {result.state}\n"
"Level: {result.level}\n"
"Volume: {result.volume}\n"
"Filter: {result.filter}\n"
"Usage: {result.usage}\n"
"Temperature: {result.temperature}\n"
"UV filter life remaining: {result.uv_filter_life_remaining}\n"
"UV filter state: {result.uv_filter_state}\n"
"Valve: {result.valve}\n",
)
)
def status(self) -> WaterPurifierStatus:
"""Retrieve properties."""
properties = [
"power",
"mode",
"tds",
"filter1_life",
"filter1_state",
"filter_life",
"filter_state",
"life",
"state",
"level",
"volume",
"filter",
"usage",
"temperature",
"uv_life",
"uv_state",
"elecval_state",
]
_props_per_request = 1
_props = properties.copy()
values = []
while _props:
values.extend(self.send("get_prop", _props[:_props_per_request]))
_props[:] = _props[_props_per_request:]
properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.error(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count,
values_count,
)
return WaterPurifierStatus(dict(zip(properties, values)))
@command(default_output=format_output("Powering on"))
def on(self):
"""Power on."""
return self.send("set_power", ["on"])
@command(default_output=format_output("Powering off"))
def off(self):
"""Power off."""
return self.send("set_power", ["off"])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/wifirepeater.py 0000644 0001750 0001750 00000010654 00000000000 017567 0 ustar 00tpr tpr 0000000 0000000 import logging
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
_LOGGER = logging.getLogger(__name__)
class WifiRepeaterException(DeviceException):
pass
class WifiRepeaterStatus:
def __init__(self, data):
"""
Response of a xiaomi.repeater.v2:
{
'sta': {'count': 2, 'access_policy': 0},
'mat': [
{'mac': 'aa:aa:aa:aa:aa:aa', 'ip': '192.168.1.133', 'last_time': 54371873},
{'mac': 'bb:bb:bb:bb:bb:bb', 'ip': '192.168.1.156', 'last_time': 54371496}
],
'access_list': {'mac': ''}
}
"""
self.data = data
@property
def access_policy(self) -> int:
"""Access policy of the associated stations."""
return self.data["sta"]["access_policy"]
@property
def associated_stations(self) -> dict:
"""List of associated stations."""
return self.data["mat"]
def __repr__(self) -> str:
s = "" % (
self.access_policy,
len(self.associated_stations),
)
return s
def __json__(self):
return self.data
class WifiRepeaterConfiguration:
def __init__(self, data):
"""
Response of a xiaomi.repeater.v2:
{'ssid': 'SSID', 'pwd': 'PWD', 'hidden': 0}
"""
self.data = data
@property
def ssid(self) -> str:
return self.data["ssid"]
@property
def password(self) -> str:
return self.data["pwd"]
@property
def ssid_hidden(self) -> bool:
return self.data["hidden"] == 1
def __repr__(self) -> str:
s = (
"" % (self.ssid, self.password, self.ssid_hidden)
)
return s
def __json__(self):
return self.data
class WifiRepeater(Device):
"""Device class for Xiaomi Mi WiFi Repeater 2."""
@command(
default_output=format_output(
"",
"Access policy: {result.access_policy}\n"
"Associated stations: {result.associated_stations}\n",
)
)
def status(self) -> WifiRepeaterStatus:
"""Return the associated stations."""
return WifiRepeaterStatus(self.send("miIO.get_repeater_sta_info"))
@command(
default_output=format_output(
"",
"SSID: {result.ssid}\n"
"Password: {result.password}\n"
"SSID hidden: {result.ssid_hidden}\n",
)
)
def configuration(self) -> WifiRepeaterConfiguration:
"""Return the configuration of the accesspoint."""
return WifiRepeaterConfiguration(self.send("miIO.get_repeater_ap_info"))
@command(
click.argument("wifi_roaming", type=bool),
default_output=format_output(
lambda led: "Turning on WiFi roaming" if led else "Turning off WiFi roaming"
),
)
def set_wifi_roaming(self, wifi_roaming: bool):
"""Turn the WiFi roaming on/off."""
return self.send(
"miIO.switch_wifi_explorer", [{"wifi_explorer": int(wifi_roaming)}]
)
@command(
click.argument("ssid", type=str),
click.argument("password", type=str),
click.argument("ssid_hidden", type=bool),
default_output=format_output("Setting accesspoint configuration"),
)
def set_configuration(self, ssid: str, password: str, ssid_hidden: bool = False):
"""Update the configuration of the accesspoint."""
return self.send(
"miIO.switch_wifi_ssid",
[
{
"ssid": ssid,
"pwd": password,
"hidden": int(ssid_hidden),
"wifi_explorer": 0,
}
],
)
@command(
default_output=format_output(
lambda result: "WiFi roaming is enabled"
if result
else "WiFi roaming is disabled"
)
)
def wifi_roaming(self) -> bool:
"""Return the roaming setting."""
return self.info().raw["desc"]["wifi_explorer"] == 1
@command(default_output=format_output("RSSI of the accesspoint: {result}"))
def rssi_accesspoint(self) -> int:
"""Received signal strength indicator of the accesspoint."""
return self.info().accesspoint["rssi"]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/miio/wifispeaker.py 0000644 0001750 0001750 00000013536 00000000000 017414 0 ustar 00tpr tpr 0000000 0000000 import enum
import logging
import warnings
import click
from .click_common import command, format_output
from .device import Device
_LOGGER = logging.getLogger(__name__)
class PlayState(enum.Enum):
Playing = "PLAYING"
Stopped = "STOPPED"
Paused = "PAUSED_PLAYBACK"
NoMedia = "NO_MEDIA_PRESENT"
Transitioning = "TRANSITIONING"
class TransportChannel(enum.Enum):
Playlist = "PLAYLIST"
OneTime = "ONETIME"
Auxiliary = "AUX"
Bluetooth = "BT"
Radio = "RADIO"
Air = "AIR"
Qplay = "QPLAY"
class WifiSpeakerStatus:
"""Container of a speaker state.
This contains information such as the name of the device,
and what is currently being played by it."""
def __init__(self, data):
"""
Example response of a xiaomi.wifispeaker.v2:
{"DeviceName": "Mi Internet Speaker", "channel_title\": "XXX",
"current_state": "PLAYING", "hardware_version": "S602",
"play_mode": "REPEAT_ALL", "track_artist": "XXX",
"track_duration": "00:04:58", "track_title": "XXX",
"transport_channel": "PLAYLIST"}
"""
self.data = data
@property
def device_name(self) -> str:
"""Name of the device."""
return self.data["DeviceName"]
@property
def channel(self) -> str:
"""Name of the channel."""
return self.data["channel_title"]
@property
def state(self) -> PlayState:
"""State of the device, e.g. PLAYING."""
return PlayState(self.data["current_state"])
@property
def hardware_version(self) -> str:
return self.data["hardware_version"]
@property
def play_mode(self):
"""Play mode such as REPEAT_ALL."""
# note: this can be enumized when all values are known
return self.data["play_mode"]
@property
def track_artist(self) -> str:
"""Artist of the current track."""
return self.data["track_artist"]
@property
def track_title(self) -> str:
"""Title of the current track."""
return self.data["track_title"]
@property
def track_duration(self) -> str:
"""Total duration of the current track."""
return self.data["track_duration"]
@property
def transport_channel(self) -> TransportChannel:
"""Transport channel, e.g. PLAYLIST"""
return TransportChannel(self.data["transport_channel"])
def __repr__(self) -> str:
s = (
""
% (
self.device_name,
self.channel,
self.state,
self.play_mode,
self.track_artist,
self.track_title,
self.track_duration,
self.transport_channel,
self.hardware_version,
)
)
return s
def __json__(self):
return self.data
class WifiSpeaker(Device):
"""Device class for Xiaomi Smart Wifi Speaker."""
def __init__(self, *args, **kwargs):
warnings.warn(
"Please help to complete this by providing more "
"information about possible values for `state`, "
"`play_mode` and `transport_channel`.",
stacklevel=2,
)
super().__init__(*args, **kwargs)
@command(
default_output=format_output(
"",
"Device name: {result.device_name}\n"
"Channel: {result.channel}\n"
"State: {result.state}\n"
"Play mode: {result.play_mode}\n"
"Track artist: {result.track_artist}\n"
"Track title: {result.track_title}\n"
"Track duration: {result.track_duration}\n"
"Transport channel: {result.transport_channel}\n"
"Hardware version: {result.hardware_version}\n",
)
)
def status(self) -> WifiSpeakerStatus:
"""Return device status."""
return WifiSpeakerStatus(self.send("get_prop", ["umi"]))
@command(default_output=format_output("Powering on"))
def power(self):
"""Toggle power on and off."""
# is this a toggle?
return self.send("power")
@command(default_output=format_output("Toggling play"))
def toggle(self):
"""Toggle play."""
return self.send("toggle")
@command(
click.argument("amount", type=int),
default_output=format_output("Increasing volume by {amount} percent"),
)
def volume_up(self, amount: int = 5):
"""Set volume up."""
return self.send("vol_up", [amount])
@command(
click.argument("amount", type=int),
default_output=format_output("Decreasing volume by {amount} percent"),
)
def volume_down(self, amount: int = 5):
"""Set volume down."""
return self.send("vol_down", [amount])
@command(default_output=format_output("Playing previous track"))
def track_previous(self):
"""Move to previous track."""
return self.send("previous_track")
@command(default_output=format_output("Playing next track"))
def track_next(self):
"""Move to next track."""
return self.send("next_track")
@command(default_output=format_output("Switching to the next transport channel"))
def channel_next(self):
"""Change transport channel."""
return self.send("next_channel")
@command(default_output=format_output("Track position: {result.rel_time}"))
def track_position(self):
"""Return current track position."""
return self.send("get_prop", ["rel_time"])
def volume(self):
"""Speaker volume."""
return self.send("get_prop", ["volume"])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585500852.0
python-miio-0.5.0.1/miio/yeelight.py 0000644 0001750 0001750 00000021122 00000000000 016703 0 ustar 00tpr tpr 0000000 0000000 import warnings
from enum import IntEnum
from typing import Optional, Tuple
import click
from .click_common import command, format_output
from .device import Device
from .exceptions import DeviceException
from .utils import int_to_rgb, rgb_to_int
class YeelightException(DeviceException):
pass
class YeelightMode(IntEnum):
RGB = 1
ColorTemperature = 2
HSV = 3
class YeelightStatus:
def __init__(self, data):
# ['power', 'bright', 'ct', 'rgb', 'hue', 'sat', 'color_mode', 'name', 'lan_ctrl', 'save_state']
# ['on', '100', '3584', '16711680', '359', '100', '2', 'name', '1', '1']
self.data = data
@property
def is_on(self) -> bool:
"""Return whether the bulb is on or off."""
return self.data["power"] == "on"
@property
def brightness(self) -> int:
"""Return current brightness."""
return int(self.data["bright"])
@property
def rgb(self) -> Optional[Tuple[int, int, int]]:
"""Return color in RGB if RGB mode is active."""
if self.color_mode == YeelightMode.RGB:
return int_to_rgb(int(self.data["rgb"]))
return None
@property
def color_mode(self) -> YeelightMode:
"""Return current color mode."""
return YeelightMode(int(self.data["color_mode"]))
@property
def hsv(self) -> Optional[Tuple[int, int, int]]:
"""Return current color in HSV if HSV mode is active."""
if self.color_mode == YeelightMode.HSV:
return self.data["hue"], self.data["sat"], self.data["bright"]
return None
@property
def color_temp(self) -> Optional[int]:
"""Return current color temperature, if applicable."""
if self.color_mode == YeelightMode.ColorTemperature:
return int(self.data["ct"])
return None
@property
def developer_mode(self) -> bool:
"""Return whether the developer mode is active."""
return bool(int(self.data["lan_ctrl"]))
@property
def save_state_on_change(self) -> bool:
"""Return whether the bulb state is saved on change."""
return bool(int(self.data["save_state"]))
@property
def name(self) -> str:
"""Return the internal name of the bulb."""
return self.data["name"]
def __repr__(self):
s = (
""
% (
self.is_on,
self.color_mode,
self.brightness,
self.color_temp,
self.rgb,
self.hsv,
self.developer_mode,
self.save_state_on_change,
self.name,
)
)
return s
class Yeelight(Device):
"""A rudimentary support for Yeelight bulbs.
The API is the same as defined in
https://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf
and only partially implmented here.
For a more complete implementation please refer to python-yeelight package
(https://yeelight.readthedocs.io/en/latest/),
which however requires enabling the developer mode on the bulbs.
"""
def __init__(self, *args, **kwargs):
warnings.warn(
"Please consider using python-yeelight " "for more complete support.",
stacklevel=2,
)
super().__init__(*args, **kwargs)
@command(
default_output=format_output(
"",
"Name: {result.name}\n"
"Power: {result.is_on}\n"
"Brightness: {result.brightness}\n"
"Color mode: {result.color_mode}\n"
"RGB: {result.rgb}\n"
"HSV: {result.hsv}\n"
"Temperature: {result.color_temp}\n"
"Developer mode: {result.developer_mode}\n"
"Update default on change: {result.save_state_on_change}\n"
"\n",
)
)
def status(self) -> YeelightStatus:
"""Retrieve properties."""
properties = [
"power",
"bright",
"ct",
"rgb",
"hue",
"sat",
"color_mode",
"name",
"lan_ctrl",
"save_state",
]
values = self.send("get_prop", properties)
return YeelightStatus(dict(zip(properties, values)))
@command(
click.option("--transition", type=int, required=False, default=0),
click.option("--mode", type=int, required=False, default=0),
default_output=format_output("Powering on"),
)
def on(self, transition=0, mode=0):
"""Power on."""
"""
set_power ["on|off", "smooth", time_in_ms, mode]
where mode:
0: last mode
1: normal mode
2: rgb mode
3: hsv mode
4: color flow
5: moonlight
"""
if transition > 0 or mode > 0:
return self.send("set_power", ["on", "smooth", transition, mode])
return self.send("set_power", ["on"])
@command(
click.option("--transition", type=int, required=False, default=0),
default_output=format_output("Powering off"),
)
def off(self, transition=0):
"""Power off."""
if transition > 0:
return self.send("set_power", ["off", "smooth", transition])
return self.send("set_power", ["off"])
@command(
click.argument("level", type=int),
click.option("--transition", type=int, required=False, default=0),
default_output=format_output("Setting brightness to {level}"),
)
def set_brightness(self, level, transition=0):
"""Set brightness."""
if level < 0 or level > 100:
raise YeelightException("Invalid brightness: %s" % level)
if transition > 0:
return self.send("set_bright", [level, "smooth", transition])
return self.send("set_bright", [level])
@command(
click.argument("level", type=int),
click.option("--transition", type=int, required=False, default=0),
default_output=format_output("Setting color temperature to {level}"),
)
def set_color_temp(self, level, transition=500):
"""Set color temp in kelvin."""
if level > 6500 or level < 1700:
raise YeelightException("Invalid color temperature: %s" % level)
if transition > 0:
return self.send("set_ct_abx", [level, "smooth", transition])
else:
# Bedside lamp requires transition
return self.send("set_ct_abx", [level, "sudden", 0])
@command(
click.argument("rgb", default=[255] * 3, type=click.Tuple([int, int, int])),
default_output=format_output("Setting color to {rgb}"),
)
def set_rgb(self, rgb: Tuple[int, int, int]):
"""Set color in RGB."""
for color in rgb:
if color < 0 or color > 255:
raise YeelightException("Invalid color: %s" % color)
return self.send("set_rgb", [rgb_to_int(rgb)])
def set_hsv(self, hsv):
"""Set color in HSV."""
return self.send("set_hsv", [hsv])
@command(
click.argument("enable", type=bool),
default_output=format_output("Setting developer mode to {enable}"),
)
def set_developer_mode(self, enable: bool) -> bool:
"""Enable or disable the developer mode."""
return self.send("set_ps", ["cfg_lan_ctrl", str(int(enable))])
@command(
click.argument("enable", type=bool),
default_output=format_output("Setting save state on change {enable}"),
)
def set_save_state_on_change(self, enable: bool) -> bool:
"""Enable or disable saving the state on changes."""
return self.send("set_ps", ["cfg_save_state", str(int(enable))])
@command(
click.argument("name", type=str),
default_output=format_output("Setting name to {name}"),
)
def set_name(self, name: str) -> bool:
"""Set an internal name for the bulb."""
return self.send("set_name", [name])
@command(default_output=format_output("Toggling the bulb"))
def toggle(self):
"""Toggle bulb state."""
return self.send("toggle")
@command(default_output=format_output("Setting current settings to default"))
def set_default(self):
"""Set current state as default."""
return self.send("set_default")
def set_scene(self, scene, *vals):
"""Set the scene."""
raise NotImplementedError("Setting the scene is not implemented yet.")
# return self.send("set_scene", [scene, *vals])
def __str__(self):
return "" % (self.ip, self.token)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1585507846.5139406
python-miio-0.5.0.1/python_miio.egg-info/ 0000755 0001750 0001750 00000000000 00000000000 017614 5 ustar 00tpr tpr 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507846.0
python-miio-0.5.0.1/python_miio.egg-info/PKG-INFO 0000644 0001750 0001750 00000013545 00000000000 020721 0 ustar 00tpr tpr 0000000 0000000 Metadata-Version: 2.1
Name: python-miio
Version: 0.5.0.1
Summary: Python library for interfacing with Xiaomi smart appliances
Home-page: https://github.com/rytilahti/python-miio
Author: Teemu Rytilahti
Author-email: tpr@iki.fi
License: GPLv3
Description: python-miio
===========
|PyPI version| |Build Status| |Coverage Status| |Docs| |Black| |Hound|
This library (and its accompanying cli tool) is used to interface with devices using Xiaomi's `miIO protocol `__.
Supported devices
-----------------
- Xiaomi Mi Robot Vacuum V1, S5, M1S
- Xiaomi Mi Home Air Conditioner Companion
- Xiaomi Mi Air Purifier
- Xiaomi Aqara Camera
- Xiaomi Aqara Gateway (basic implementation, alarm, lights)
- Xiaomi Mijia 360 1080p
- Xiaomi Mijia STYJ02YM (Viomi)
- Xiaomi Mi Smart WiFi Socket
- Xiaomi Chuangmi Plug V1 (1 Socket, 1 USB Port)
- Xiaomi Chuangmi Plug V3 (1 Socket, 2 USB Ports)
- Xiaomi Smart Power Strip V1 and V2 (WiFi, 6 Ports)
- Xiaomi Philips Eyecare Smart Lamp 2
- Xiaomi Philips RW Read (philips.light.rwread)
- Xiaomi Philips LED Ceiling Lamp
- Xiaomi Philips LED Ball Lamp (philips.light.bulb)
- Xiaomi Philips LED Ball Lamp White (philips.light.hbulb)
- Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp
- Xiaomi Philips Zhirui Bedroom Smart Lamp
- Xiaomi Universal IR Remote Controller (Chuangmi IR)
- Xiaomi Mi Smart Pedestal Fan V2, V3, SA1, ZA1, ZA3, ZA4, P5
- Xiaomi Mi Air Humidifier V1, CA1, CB1, MJJSQ, JSQ001
- Xiaomi Mi Water Purifier (Basic support: Turn on & off)
- Xiaomi PM2.5 Air Quality Monitor V1, B1, S1
- Xiaomi Smart WiFi Speaker
- Xiaomi Mi WiFi Repeater 2
- Xiaomi Mi Smart Rice Cooker
- Xiaomi Smartmi Fresh Air System VA2 (zhimi.airfresh.va2), T2017 (dmaker.airfresh.t2017)
- Yeelight lights (basic support, we recommend using `python-yeelight `__)
- Xiaomi Mi Air Dehumidifier
- Xiaomi Tinymu Smart Toilet Cover
- Xiaomi 16 Relays Module
- Xiaomi Xiao AI Smart Alarm Clock
- Smartmi Radiant Heater Smart Version (ZA1 version)
- Xiaomi Mi Smart Space Heater
*Feel free to create a pull request to add support for new devices as
well as additional features for supported devices.*
Getting started
---------------
Refer `the manual `__ for getting started.
Contributing
------------
We welcome all sorts of contributions from patches to add improvements or fixing bugs to improving the documentation.
To ease the process of setting up a development environment we have prepared `a short guide `__ for getting you started.
Home Assistant support
----------------------
- `Xiaomi Mi Robot Vacuum `__
- `Xiaomi Philips Light `__
- `Xiaomi Mi Air Purifier and Air Humidifier `__
- `Xiaomi Smart WiFi Socket and Smart Power Strip `__
- `Xiaomi Universal IR Remote Controller `__
- `Xiaomi Mi Air Quality Monitor (PM2.5) `__
- `Xiaomi Aqara Gateway Alarm `__
- `Xiaomi Mi Home Air Conditioner Companion `__
- `Xiaomi Mi WiFi Repeater 2 `__
- `Xiaomi Mi Smart Pedestal Fan `__
- `Xiaomi Mi Smart Rice Cooker `__
- `Xiaomi Raw Sensor `__
.. |PyPI version| image:: https://badge.fury.io/py/python-miio.svg
:target: https://badge.fury.io/py/python-miio
.. |Build Status| image:: https://travis-ci.org/rytilahti/python-miio.svg?branch=master
:target: https://travis-ci.org/rytilahti/python-miio
.. |Coverage Status| image:: https://coveralls.io/repos/github/rytilahti/python-miio/badge.svg?branch=master
:target: https://coveralls.io/github/rytilahti/python-miio?branch=master
.. |Docs| image:: https://readthedocs.org/projects/python-miio/badge/?version=latest
:alt: Documentation status
:target: https://python-miio.readthedocs.io/en/latest/?badge=latest
.. |Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
:alt: Hound
:target: https://houndci.com
.. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
Keywords: xiaomi miio vacuum
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.6
Provides-Extra: Android backup extraction
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507846.0
python-miio-0.5.0.1/python_miio.egg-info/SOURCES.txt 0000644 0001750 0001750 00000005223 00000000000 021502 0 ustar 00tpr tpr 0000000 0000000 .pre-commit-config.yaml
.readthedocs.yml
.travis.yml
CHANGELOG.md
LICENSE
LICENSE.md
MANIFEST.in
README.rst
RELEASING.md
azure-pipelines.yml
requirements.txt
requirements_docs.txt
setup.py
tox.ini
docs/Makefile
docs/ceil.rst
docs/conf.py
docs/discovery.rst
docs/eyecare.rst
docs/index.rst
docs/miio.rst
docs/new_devices.rst
docs/plug.rst
docs/troubleshooting.rst
docs/vacuum.rst
docs/yeelight.rst
miio/__init__.py
miio/airconditioningcompanion.py
miio/airdehumidifier.py
miio/airfilter_util.py
miio/airfresh.py
miio/airfresh_t2017.py
miio/airhumidifier.py
miio/airhumidifier_jsq.py
miio/airhumidifier_mjjsq.py
miio/airpurifier.py
miio/airpurifier_miot.py
miio/airqualitymonitor.py
miio/alarmclock.py
miio/aqaracamera.py
miio/ceil.py
miio/ceil_cli.py
miio/chuangmi_camera.py
miio/chuangmi_ir.py
miio/chuangmi_plug.py
miio/cli.py
miio/click_common.py
miio/cooker.py
miio/device.py
miio/discovery.py
miio/exceptions.py
miio/extract_tokens.py
miio/fan.py
miio/gateway.py
miio/heater.py
miio/miioprotocol.py
miio/miot_device.py
miio/philips_bulb.py
miio/philips_eyecare.py
miio/philips_eyecare_cli.py
miio/philips_moonlight.py
miio/philips_rwread.py
miio/plug_cli.py
miio/powerstrip.py
miio/protocol.py
miio/pwzn_relay.py
miio/toiletlid.py
miio/updater.py
miio/utils.py
miio/vacuum.py
miio/vacuum_cli.py
miio/vacuumcontainers.py
miio/version.py
miio/viomivacuum.py
miio/waterpurifier.py
miio/wifirepeater.py
miio/wifispeaker.py
miio/yeelight.py
miio/data/cooker_profiles.json
miio/tests/__init__.py
miio/tests/dummies.py
miio/tests/test_airconditioningcompanion.json
miio/tests/test_airconditioningcompanion.py
miio/tests/test_airdehumidifier.py
miio/tests/test_airfilter_util.py
miio/tests/test_airfresh.py
miio/tests/test_airfresh_t2017.py
miio/tests/test_airhumidifier.py
miio/tests/test_airhumidifier_jsq.py
miio/tests/test_airhumidifier_mjjsq.py
miio/tests/test_airpurifier.py
miio/tests/test_airpurifier_miot.py
miio/tests/test_airqualitymonitor.py
miio/tests/test_ceil.py
miio/tests/test_chuangmi_ir.json
miio/tests/test_chuangmi_ir.py
miio/tests/test_chuangmi_plug.py
miio/tests/test_click_common.py
miio/tests/test_fan.py
miio/tests/test_heater.py
miio/tests/test_philips_bulb.py
miio/tests/test_philips_eyecare.py
miio/tests/test_philips_moonlight.py
miio/tests/test_philips_rwread.py
miio/tests/test_powerstrip.py
miio/tests/test_protocol.py
miio/tests/test_toiletlid.py
miio/tests/test_vacuum.py
miio/tests/test_waterpurifier.py
miio/tests/test_wifirepeater.py
miio/tests/test_yeelight.py
python_miio.egg-info/PKG-INFO
python_miio.egg-info/SOURCES.txt
python_miio.egg-info/dependency_links.txt
python_miio.egg-info/entry_points.txt
python_miio.egg-info/requires.txt
python_miio.egg-info/top_level.txt ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507846.0
python-miio-0.5.0.1/python_miio.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 00000000000 023662 0 ustar 00tpr tpr 0000000 0000000
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507846.0
python-miio-0.5.0.1/python_miio.egg-info/entry_points.txt 0000644 0001750 0001750 00000000330 00000000000 023106 0 ustar 00tpr tpr 0000000 0000000 [console_scripts]
miceil = miio.ceil_cli:cli
mieye = miio.philips_eyecare_cli:cli
miio-extract-tokens = miio.extract_tokens:main
miiocli = miio.cli:create_cli
miplug = miio.plug_cli:cli
mirobo = miio.vacuum_cli:cli
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507846.0
python-miio-0.5.0.1/python_miio.egg-info/requires.txt 0000644 0001750 0001750 00000000167 00000000000 022220 0 ustar 00tpr tpr 0000000 0000000 construct
click>=7
cryptography
zeroconf
attrs
pytz
appdirs
tqdm
netifaces
[Android backup extraction]
android_backup
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585507846.0
python-miio-0.5.0.1/python_miio.egg-info/top_level.txt 0000644 0001750 0001750 00000000005 00000000000 022341 0 ustar 00tpr tpr 0000000 0000000 miio
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/requirements.txt 0000644 0001750 0001750 00000000240 00000000000 017044 0 ustar 00tpr tpr 0000000 0000000 click
cryptography
construct
zeroconf
attrs
pytz # for tz offset in vacuum
appdirs # for user_cache_dir of vacuum_cli
tqdm
netifaces # for updater
pre-commit
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1519088451.0
python-miio-0.5.0.1/requirements_docs.txt 0000644 0001750 0001750 00000000111 00000000000 020051 0 ustar 00tpr tpr 0000000 0000000 sphinx
doc8
restructuredtext_lint
sphinx-autodoc-typehints
sphinx-click
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1585507846.5139406
python-miio-0.5.0.1/setup.cfg 0000644 0001750 0001750 00000000046 00000000000 015405 0 ustar 00tpr tpr 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1584288842.0
python-miio-0.5.0.1/setup.py 0000644 0001750 0001750 00000003353 00000000000 015302 0 ustar 00tpr tpr 0000000 0000000 import re
from setuptools import setup
with open("miio/version.py") as f:
exec(f.read())
def readme():
# we have intersphinx link in our readme, so let's replace them
# for the long_description to make pypi happy
reg = re.compile(r":.+?:`(.+?)\s?(<.+?>)?`")
with open("README.rst") as f:
return re.sub(reg, r"\1", f.read())
setup(
name="python-miio",
version=__version__, # type: ignore # noqa: F821
description="Python library for interfacing with Xiaomi smart appliances",
long_description=readme(),
url="https://github.com/rytilahti/python-miio",
author="Teemu Rytilahti",
author_email="tpr@iki.fi",
license="GPLv3",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3 :: Only",
],
keywords="xiaomi miio vacuum",
packages=["miio"],
include_package_data=True,
python_requires=">=3.6",
install_requires=[
"construct",
"click>=7",
"cryptography",
"zeroconf",
"attrs",
"pytz",
"appdirs",
"tqdm",
"netifaces",
],
extras_require={"Android backup extraction": "android_backup"},
entry_points={
"console_scripts": [
"mirobo=miio.vacuum_cli:cli",
"miplug=miio.plug_cli:cli",
"miceil=miio.ceil_cli:cli",
"mieye=miio.philips_eyecare_cli:cli",
"miio-extract-tokens=miio.extract_tokens:main",
"miiocli=miio.cli:create_cli",
]
},
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1585505438.0
python-miio-0.5.0.1/tox.ini 0000644 0001750 0001750 00000003320 00000000000 015075 0 ustar 00tpr tpr 0000000 0000000 [tox]
envlist=py36,py37,py38,flake8,docs,manifest,pypi-description
[tox:travis]
3.6 = py36
3.7 = py37
3.8 = py38
[testenv]
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
deps=
pytest
pytest-cov
voluptuous
commands=
py.test --cov --cov-config=tox.ini miio
[testenv:docs]
basepython=python
extras=docs
deps=
sphinx
doc8
restructuredtext_lint
sphinx-autodoc-typehints
sphinx-click
commands=
doc8 docs
rst-lint README.rst docs/*.rst
sphinx-build -W -b html -d {envtmpdir}/docs docs {envtmpdir}/html
[doc8]
ignore-path = docs/_build*,.tox
max-line-length = 120
[testenv:flake8]
deps=flake8
commands=flake8 miio
[flake8]
exclude = .git,.tox,__pycache__
max-line-length = 88
select = C,E,F,W,B,B950
ignore = E501,W503,E203
[testenv:lint]
deps = pre-commit
skip_install = true
commands = pre-commit run --all-files
[testenv:typing]
deps=mypy
commands=mypy --ignore-missing-imports miio
[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
known_first_party=miio
forced_separate=miio.discover
known_third_party=
appdirs
attr
click
construct
cryptography
netifaces
pytest
pytz
setuptools
tqdm
zeroconf
[coverage:run]
source = miio
branch = True
omit =
miio/*cli.py
miio/extract_tokens.py
miio/tests/*
miio/version.py
[coverage:report]
exclude_lines =
def __repr__
[testenv:pypi-description]
basepython = python3.7
skip_install = true
deps =
twine
pip >= 18.0.0
commands =
pip wheel -w {envtmpdir}/build --no-deps .
twine check {envtmpdir}/build/*
[testenv:manifest]
basepython = python3.7
deps = check-manifest
skip_install = true
commands = check-manifest
[check-manifest]
ignore =
devtools
devtools/*