././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1585507846.5139406 python-miio-0.5.0.1/0000755000175000017500000000000000000000000013564 5ustar00tprtpr00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585505438.0 python-miio-0.5.0.1/.pre-commit-config.yaml0000644000175000017500000000124000000000000020042 0ustar00tprtpr00000000000000repos: - 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1549371166.0 python-miio-0.5.0.1/.readthedocs.yml0000644000175000017500000000012700000000000016652 0ustar00tprtpr00000000000000requirements_file: requirements_docs.txt python: version: 3 setup_py_install: true ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/.travis.yml0000644000175000017500000000021000000000000015666 0ustar00tprtpr00000000000000sudo: false language: python python: - "3.6" - "3.7" install: pip install tox-travis coveralls script: tox after_success: coveralls ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507535.0 python-miio-0.5.0.1/CHANGELOG.md0000644000175000017500000026634300000000000015413 0ustar00tprtpr00000000000000# 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)* ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1519088451.0 python-miio-0.5.0.1/LICENSE0000644000175000017500000010451300000000000014575 0ustar00tprtpr00000000000000 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 . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1519088451.0 python-miio-0.5.0.1/LICENSE.md0000644000175000017500000010414600000000000015176 0ustar00tprtpr00000000000000# 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). ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/MANIFEST.in0000644000175000017500000000035300000000000015323 0ustar00tprtpr00000000000000include *.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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1585507846.5139406 python-miio-0.5.0.1/PKG-INFO0000644000175000017500000001354500000000000014671 0ustar00tprtpr00000000000000Metadata-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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500788.0 python-miio-0.5.0.1/README.rst0000644000175000017500000001066000000000000015256 0ustar00tprtpr00000000000000python-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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1548870856.0 python-miio-0.5.0.1/RELEASING.md0000644000175000017500000000147400000000000015425 0ustar00tprtpr000000000000001. 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585505438.0 python-miio-0.5.0.1/azure-pipelines.yml0000644000175000017500000000204200000000000017421 0ustar00tprtpr00000000000000trigger: - 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' ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1585507846.5039406 python-miio-0.5.0.1/docs/0000755000175000017500000000000000000000000014514 5ustar00tprtpr00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1519088451.0 python-miio-0.5.0.1/docs/Makefile0000644000175000017500000000114300000000000016153 0ustar00tprtpr00000000000000# 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)././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1519088451.0 python-miio-0.5.0.1/docs/ceil.rst0000644000175000017500000000035600000000000016166 0ustar00tprtpr00000000000000Ceil ==== .. 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: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/docs/conf.py0000644000175000017500000001266400000000000016024 0ustar00tprtpr00000000000000#!/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)} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/docs/discovery.rst0000644000175000017500000002560300000000000017263 0ustar00tprtpr00000000000000Getting 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1519088451.0 python-miio-0.5.0.1/docs/eyecare.rst0000644000175000017500000000041100000000000016657 0ustar00tprtpr00000000000000Philips 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: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1549371166.0 python-miio-0.5.0.1/docs/index.rst0000644000175000017500000000230200000000000016352 0ustar00tprtpr00000000000000.. 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584295509.0 python-miio-0.5.0.1/docs/miio.rst0000644000175000017500000001033500000000000016205 0ustar00tprtpr00000000000000miio 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: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/docs/new_devices.rst0000644000175000017500000000645500000000000017553 0ustar00tprtpr00000000000000Contributing ************ 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/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1519088451.0 python-miio-0.5.0.1/docs/plug.rst0000644000175000017500000000035500000000000016220 0ustar00tprtpr00000000000000Plug ==== .. 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: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/docs/troubleshooting.rst0000644000175000017500000000536400000000000020505 0ustar00tprtpr00000000000000Troubleshooting =============== 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1549371166.0 python-miio-0.5.0.1/docs/vacuum.rst0000644000175000017500000001365100000000000016554 0ustar00tprtpr00000000000000Vacuum ====== 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 ` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/docs/yeelight.rst0000644000175000017500000000442300000000000017063 0ustar00tprtpr00000000000000Yeelight ======== .. 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 ` ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1585507846.5106072 python-miio-0.5.0.1/miio/0000755000175000017500000000000000000000000014521 5ustar00tprtpr00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500788.0 python-miio-0.5.0.1/miio/__init__.py0000644000175000017500000000347400000000000016642 0ustar00tprtpr00000000000000# 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/airconditioningcompanion.py0000644000175000017500000003640500000000000022167 0ustar00tprtpr00000000000000import 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]) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/airdehumidifier.py0000644000175000017500000002310200000000000020223 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584295509.0 python-miio-0.5.0.1/miio/airfilter_util.py0000644000175000017500000000251500000000000020114 0ustar00tprtpr00000000000000import 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/airfresh.py0000644000175000017500000002241200000000000016677 0ustar00tprtpr00000000000000import 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/airfresh_t2017.py0000644000175000017500000003014100000000000017532 0ustar00tprtpr00000000000000import 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/airhumidifier.py0000644000175000017500000003313700000000000017723 0ustar00tprtpr00000000000000import 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 ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500788.0 python-miio-0.5.0.1/miio/airhumidifier_jsq.py0000644000175000017500000002106700000000000020577 0ustar00tprtpr00000000000000import 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))]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/airhumidifier_mjjsq.py0000644000175000017500000001514300000000000021124 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/airpurifier.py0000644000175000017500000004331200000000000017417 0ustar00tprtpr00000000000000import 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584295509.0 python-miio-0.5.0.1/miio/airpurifier_miot.py0000644000175000017500000003072700000000000020455 0ustar00tprtpr00000000000000import 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/airqualitymonitor.py0000644000175000017500000002365700000000000020704 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/alarmclock.py0000644000175000017500000002177700000000000017221 0ustar00tprtpr00000000000000import 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/aqaracamera.py0000644000175000017500000002277700000000000017350 0ustar00tprtpr00000000000000"""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"]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/ceil.py0000644000175000017500000001563400000000000016020 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/ceil_cli.py0000644000175000017500000001053700000000000016644 0ustar00tprtpr00000000000000import 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585421538.0 python-miio-0.5.0.1/miio/chuangmi_camera.py0000644000175000017500000001770000000000000020203 0ustar00tprtpr00000000000000"""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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/chuangmi_ir.py0000644000175000017500000001516000000000000017363 0ustar00tprtpr00000000000000import 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), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/chuangmi_plug.py0000644000175000017500000001647000000000000017725 0ustar00tprtpr00000000000000import 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 ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/cli.py0000644000175000017500000000205500000000000015644 0ustar00tprtpr00000000000000import 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/click_common.py0000644000175000017500000002212500000000000017532 0ustar00tprtpr00000000000000"""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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/cooker.py0000644000175000017500000006413400000000000016365 0ustar00tprtpr00000000000000import 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, ] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1585507846.5106072 python-miio-0.5.0.1/miio/data/0000755000175000017500000000000000000000000015432 5ustar00tprtpr00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1548870856.0 python-miio-0.5.0.1/miio/data/cooker_profiles.json0000644000175000017500000003160700000000000021521 0ustar00tprtpr00000000000000{ "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" } ] } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/device.py0000644000175000017500000001327000000000000016335 0ustar00tprtpr00000000000000import 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] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/discovery.py0000644000175000017500000002270500000000000017110 0ustar00tprtpr00000000000000import 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585489963.0 python-miio-0.5.0.1/miio/exceptions.py0000644000175000017500000000073600000000000017262 0ustar00tprtpr00000000000000class 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/extract_tokens.py0000644000175000017500000001665300000000000020143 0ustar00tprtpr00000000000000import 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585421538.0 python-miio-0.5.0.1/miio/fan.py0000644000175000017500000005702000000000000015643 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500788.0 python-miio-0.5.0.1/miio/gateway.py0000644000175000017500000004213300000000000016537 0ustar00tprtpr00000000000000import 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"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585421538.0 python-miio-0.5.0.1/miio/heater.py0000644000175000017500000002122600000000000016346 0ustar00tprtpr00000000000000import 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585489963.0 python-miio-0.5.0.1/miio/miioprotocol.py0000644000175000017500000002027400000000000017617 0ustar00tprtpr00000000000000"""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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584295509.0 python-miio-0.5.0.1/miio/miot_device.py0000644000175000017500000000320400000000000017361 0ustar00tprtpr00000000000000import 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}], ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507037.0 python-miio-0.5.0.1/miio/philips_bulb.py0000644000175000017500000001510100000000000017545 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/philips_eyecare.py0000644000175000017500000001725200000000000020247 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584291922.0 python-miio-0.5.0.1/miio/philips_eyecare_cli.py0000644000175000017500000001216700000000000021076 0ustar00tprtpr00000000000000import 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/philips_moonlight.py0000644000175000017500000001740300000000000020630 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507037.0 python-miio-0.5.0.1/miio/philips_rwread.py0000644000175000017500000001556300000000000020121 0ustar00tprtpr00000000000000import 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)]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/plug_cli.py0000644000175000017500000000450600000000000016676 0ustar00tprtpr00000000000000import 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/powerstrip.py0000644000175000017500000001757600000000000017331 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/protocol.py0000644000175000017500000001624000000000000016737 0ustar00tprtpr00000000000000"""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), ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/pwzn_relay.py0000644000175000017500000001157700000000000017300 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1585507846.5139406 python-miio-0.5.0.1/miio/tests/0000755000175000017500000000000000000000000015663 5ustar00tprtpr00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1507444682.0 python-miio-0.5.0.1/miio/tests/__init__.py0000644000175000017500000000000000000000000017762 0ustar00tprtpr00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585489963.0 python-miio-0.5.0.1/miio/tests/dummies.py0000644000175000017500000000505700000000000017707 0ustar00tprtpr00000000000000class 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1548870856.0 python-miio-0.5.0.1/miio/tests/test_airconditioningcompanion.json0000644000175000017500000001317200000000000024705 0ustar00tprtpr00000000000000{ "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" } ] }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_airconditioningcompanion.py0000644000175000017500000002302600000000000024363 0ustar00tprtpr00000000000000import 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_airdehumidifier.py0000644000175000017500000001453600000000000022437 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584295509.0 python-miio-0.5.0.1/miio/tests/test_airfilter_util.py0000644000175000017500000000300400000000000022307 0ustar00tprtpr00000000000000from 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 ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_airfresh.py0000644000175000017500000001470000000000000021101 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_airfresh_t2017.py0000644000175000017500000001705700000000000021746 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_airhumidifier.py0000644000175000017500000005502500000000000022124 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500788.0 python-miio-0.5.0.1/miio/tests/test_airhumidifier_jsq.py0000644000175000017500000002330100000000000022771 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_airhumidifier_mjjsq.py0000644000175000017500000001062400000000000023324 0ustar00tprtpr00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_airpurifier.py0000644000175000017500000003144000000000000021617 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584295509.0 python-miio-0.5.0.1/miio/tests/test_airpurifier_miot.py0000644000175000017500000001500700000000000022650 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_airqualitymonitor.py0000644000175000017500000001351100000000000023071 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_ceil.py0000644000175000017500000001430400000000000020212 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1548870856.0 python-miio-0.5.0.1/miio/tests/test_chuangmi_ir.json0000644000175000017500000001250500000000000022105 0ustar00tprtpr00000000000000{ "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 ] } ] }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_chuangmi_ir.py0000644000175000017500000001046200000000000021564 0ustar00tprtpr00000000000000import 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_chuangmi_plug.py0000644000175000017500000001501600000000000022121 0ustar00tprtpr00000000000000from 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"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_click_common.py0000644000175000017500000000033600000000000021733 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_fan.py0000644000175000017500000007226600000000000020055 0ustar00tprtpr00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584291922.0 python-miio-0.5.0.1/miio/tests/test_heater.py0000644000175000017500000001145000000000000020545 0ustar00tprtpr00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_philips_bulb.py0000644000175000017500000002046400000000000021756 0ustar00tprtpr00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_philips_eyecare.py0000644000175000017500000001377200000000000022453 0ustar00tprtpr00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_philips_moonlight.py0000644000175000017500000002042400000000000023026 0ustar00tprtpr00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_philips_rwread.py0000644000175000017500000001237500000000000022320 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_powerstrip.py0000644000175000017500000001637700000000000021530 0ustar00tprtpr00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585489963.0 python-miio-0.5.0.1/miio/tests/test_protocol.py0000644000175000017500000000567100000000000021146 0ustar00tprtpr00000000000000import 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_toiletlid.py0000644000175000017500000001147400000000000021274 0ustar00tprtpr00000000000000from 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 == {} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584365257.0 python-miio-0.5.0.1/miio/tests/test_vacuum.py0000644000175000017500000001425500000000000020603 0ustar00tprtpr00000000000000import 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_waterpurifier.py0000644000175000017500000000352300000000000022167 0ustar00tprtpr00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_wifirepeater.py0000644000175000017500000001320500000000000021763 0ustar00tprtpr00000000000000from 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"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/tests/test_yeelight.py0000644000175000017500000001503600000000000021113 0ustar00tprtpr00000000000000from 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585421538.0 python-miio-0.5.0.1/miio/toiletlid.py0000644000175000017500000001350200000000000017065 0ustar00tprtpr00000000000000import 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"]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/updater.py0000644000175000017500000000602100000000000016536 0ustar00tprtpr00000000000000import 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/utils.py0000644000175000017500000000623200000000000016236 0ustar00tprtpr00000000000000import 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/vacuum.py0000644000175000017500000005300700000000000016400 0ustar00tprtpr00000000000000import 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585505798.0 python-miio-0.5.0.1/miio/vacuum_cli.py0000644000175000017500000004463600000000000017237 0ustar00tprtpr00000000000000import 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/vacuumcontainers.py0000644000175000017500000004106200000000000020464 0ustar00tprtpr00000000000000# -*- 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 = " 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507535.0 python-miio-0.5.0.1/miio/version.py0000644000175000017500000000004700000000000016561 0ustar00tprtpr00000000000000# flake8: noqa __version__ = "0.5.0.1" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500865.0 python-miio-0.5.0.1/miio/viomivacuum.py0000644000175000017500000002256500000000000017451 0ustar00tprtpr00000000000000import 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)} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585421538.0 python-miio-0.5.0.1/miio/waterpurifier.py0000644000175000017500000001317600000000000017773 0ustar00tprtpr00000000000000import 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"]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/wifirepeater.py0000644000175000017500000001065400000000000017567 0ustar00tprtpr00000000000000import 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"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/miio/wifispeaker.py0000644000175000017500000001353600000000000017414 0ustar00tprtpr00000000000000import 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"]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585500852.0 python-miio-0.5.0.1/miio/yeelight.py0000644000175000017500000002112200000000000016703 0ustar00tprtpr00000000000000import 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) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1585507846.5139406 python-miio-0.5.0.1/python_miio.egg-info/0000755000175000017500000000000000000000000017614 5ustar00tprtpr00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507846.0 python-miio-0.5.0.1/python_miio.egg-info/PKG-INFO0000644000175000017500000001354500000000000020721 0ustar00tprtpr00000000000000Metadata-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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507846.0 python-miio-0.5.0.1/python_miio.egg-info/SOURCES.txt0000644000175000017500000000522300000000000021502 0ustar00tprtpr00000000000000.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././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507846.0 python-miio-0.5.0.1/python_miio.egg-info/dependency_links.txt0000644000175000017500000000000100000000000023662 0ustar00tprtpr00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507846.0 python-miio-0.5.0.1/python_miio.egg-info/entry_points.txt0000644000175000017500000000033000000000000023106 0ustar00tprtpr00000000000000[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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507846.0 python-miio-0.5.0.1/python_miio.egg-info/requires.txt0000644000175000017500000000016700000000000022220 0ustar00tprtpr00000000000000construct click>=7 cryptography zeroconf attrs pytz appdirs tqdm netifaces [Android backup extraction] android_backup ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585507846.0 python-miio-0.5.0.1/python_miio.egg-info/top_level.txt0000644000175000017500000000000500000000000022341 0ustar00tprtpr00000000000000miio ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/requirements.txt0000644000175000017500000000024000000000000017044 0ustar00tprtpr00000000000000click cryptography construct zeroconf attrs pytz # for tz offset in vacuum appdirs # for user_cache_dir of vacuum_cli tqdm netifaces # for updater pre-commit ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1519088451.0 python-miio-0.5.0.1/requirements_docs.txt0000644000175000017500000000011100000000000020051 0ustar00tprtpr00000000000000sphinx doc8 restructuredtext_lint sphinx-autodoc-typehints sphinx-click ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1585507846.5139406 python-miio-0.5.0.1/setup.cfg0000644000175000017500000000004600000000000015405 0ustar00tprtpr00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1584288842.0 python-miio-0.5.0.1/setup.py0000644000175000017500000000335300000000000015302 0ustar00tprtpr00000000000000import 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", ] }, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1585505438.0 python-miio-0.5.0.1/tox.ini0000644000175000017500000000332000000000000015075 0ustar00tprtpr00000000000000[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/*