Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic miot support #1581

Merged
merged 7 commits into from
Dec 5, 2022
Merged

Add generic miot support #1581

merged 7 commits into from
Dec 5, 2022

Conversation

rytilahti
Copy link
Owner

@rytilahti rytilahti commented Nov 8, 2022

Foreword

  • If you are using the miiocli tool or the library to control your devices: it would be appreciated if you could test if this is working fine for you.
  • If you are using homeassistant and arrived here as this PR was marked to close your issue: you need to wait a bit longer until this code is ready to be released.

Description

This PR adds a new integration, genericmiot, that uses the miotspec files to provide support for all miot devices that can be controlled over local network.
The basic functionality is implemented and has been tested to work on a Mi Smart LED Bulb (Warm White) (yeelink.light.mono6) light and on different simulated devices.

This PR does not implement sending commands over the cloud interface, which may be necessary on some devices to function.
If you want to test or help with this, please read read further :-)

Current functionality

  • Obtaining status (status command)
  • Changing settings (settings, set commands)
  • Executing simple actions (actions, call_action commands)

TODO

Instructions to test

  1. You can either test against a simulator or a real device:

    • 1a. To test against a simulated device, use the miot-simulator located under the devtools command:
    miiocli -d devtools miot-simulator --model <model> # e.g., dreame.vacuum.p2140p
    
    • 1b. To test against a real device, obtain the token and the address
    miiocli cloud list
    
  2. Export the address and token to avoid repeating --ip and --token for every command. The examples below are after doing these two exports:

export MIIO_GENERICMIOT_IP=127.0.0.1
export MIIO_GENERICMIOT_TOKEN=00000000000000000000000000000000
  1. Try if the status is being reported correctly (following commands are tested against a simulated dreame vacuum mentioned above:
miiocli genericmiot status

Status (vacuum:status): Paused (value: 3) (from: Sweeping (1), Idle (2), Paused (3), Error (4), Go Charging (5), Charging (6), Mopping (7))
Device Fault (vacuum:fault): 10  (min: 0, max: 100, step: 1)
[S] Mode (vacuum:mode): Strong (value: 2) (from: Silent (0), Basic (1), Strong (2), Full Speed (3))
Battery Level (battery:battery-level): 10 % (min: 0, max: 100, step: 1)
Charging State (battery:charging-state): Charging (value: 1) (from: Charging (1), Not Charging (2), Go Charging (5))
Brush Left Time (brush-cleaner:brush-left-time): 1 day, 17:00:00 (min: 0, max: 200, step: 1)
Brush Life Level (brush-cleaner:brush-life-level): 16 % (min: 0, max: 100, step: 1)
Filter Life Level (filter:filter-life-level): 48 % (min: 0, max: 100, step: 1)
Filter Left Time (filter:filter-left-time): 3 days, 22:00:00 (min: 0, max: 150, step: 1)
工作模式 (vacuum-extend:work-mode): 2  (min: 0, max: 50, step: 1)
清扫时长 (vacuum-extend:cleaning-time): 3 days, 11:51:00 (min: 0, max: 32767, step: 1)
清扫面积 (vacuum-extend:cleaning-area): 17352  (min: 0, max: 32767, step: 1)
[S] 清扫模式 (vacuum-extend:cleaning-mode): 3 (value: 3) (from: 0 (0), 1 (1), 2 (2), 3 (3))
[S]  (vacuum-extend:mop-mode): 3  (min: 1, max: 3, step: 1)
水箱状态 (vacuum-extend:waterbox-status): 1 (value: 1) (from: 0 (0), 1 (1))
 (vacuum-extend:task-status): Notask (value: 0) (from: Notask (0), AutoClean (1), CustomClean (2), SelectAreanClean (3), SpotArea (4))
[S] break-point-restart (vacuum-extend:break-point-restart): 关闭 (value: 0) (from: 关闭 (0), 打开 (1))
[S] carpet-press (vacuum-extend:carpet-press): 关闭 (value: 0) (from: 关闭 (0), 打开 (1))
serial-number (vacuum-extend:serial-number): piid 14 
keep-sweeper-time (vacuum-extend:keep-sweeper-time): 338 days, 21:18:00 (min: -1, max: 1000000, step: 1)
faults (vacuum-extend:faults): piid 18 
[S] 使能 (do-not-disturb:enable): True
[S] 勿扰起始时间 (do-not-disturb:start-time): piid 2 
[S] 勿扰结束时间 (do-not-disturb:end-time): piid 3 
[S] 音量 (audio:volume): 19  (min: 0, max: 100, step: 1)
[S] 语音包id (audio:voice-packet-id): piid 2 
语音包切换时的状态 (audio:voice-change-state): piid 3 
主机时区获取 (time:time-zone): piid 1 
[S] timer-clean (time:timer-clean): piid 2 
首次清扫的开始时间 (clean-logs:first-clean-time): 2164597218  (min: 0, max: 4294967295, step: 1)
总清扫时间 (clean-logs:total-clean-time): 2044084 days, 21:41:00 (min: 0, max: 4294967295, step: 1)
总清扫次数 (clean-logs:total-clean-times): 3387871266  (min: 0, max: 4294967295, step: 1)
总清扫面积 (clean-logs:total-clean-area): 3821312175  (min: 0, max: 4294967295, step: 1)
[S] save-map-status (vslam-extend:save-map-status): 关闭 (value: 0) (from: 关闭 (0), 打开 (1))
  1. The variables marked with [S] are settings (also accessible using miiocli genericmiot settings) that can be changed:
miiocli genericmiot set vacuum:mode 0  # set to silent

...
  1. List actions:
miiocli genericmiot actions

vacuum:start-sweep              Start Sweep
vacuum:stop-sweeping            Stop Sweeping
battery:start-charge            Start Charge
brush-cleaner:reset-brush-life          Reset Brush Life
filter:reset-filter-life                Reset Filter Life
vacuum-extend:stop-clean                停止清扫(非暂停,停止清扫任务)
audio:position          定位我的机器人
audio:play-sound                试听语音
  1. Invoking actions:
miiocli genericmiot call_action vacuum:start-sweep  # start cleaning

Homeassistant support

The homeassistant support is currently very rudimentary, and implements only the base platforms (switch, button, select, sensor, binary_sensor, number, ..), but that will hopefully change in the near future. Here's how it is looking currently:

image

Help needed!
If you are a developer and want to help, please feel free to contact me per e-mail or via discord.
The current code is living in the entities_from_upstream branch: https://github.com/rytilahti/home-assistant/tree/xiaomi_miio/feat/entities_from_upstream) – any help is welcome!

Below only links to close related issues and pull requests.

List of related issues and pull requests

Closes #1394
Closes #1550
Closes #1571
Closes #1300
Closes #870
Closes #1563
Closes #1217
Closes #1397
Closes #1434
Closes #1553
Closes #1544
Closes #1541
Closes #1418
Closes #1271
Closes #778
Closes #1513
Closes #1512
Closes #1330
Closes #1319
Closes #1273
Closes #1485
Closes #1455
Closes #1447
Closes #1441
Closes #1133
Closes #1428
Closes #1412
Closes #1375
Closes #1400
Closes #839
Closes #1182
Closes #1338
Closes #812
Closes #1326
Closes #844
Closes #1162
Closes #1267
Closes #1218
Closes #1283
Closes #913
Closes #1209
Closes #1001
Closes #1061
Closes #835
Closes #974
Closes #896
Closes #889
Closes #837
Closes #618
Closes #901

Obsoletes and closes #672
Obsoletes and closes #1254
Obsoletes and closes #1029
Obsoletes and closes #1019

@Entretoize
Copy link

Maybe I can't help as a developper, I have this vaccum. One question, I've tried your command and get Error: No such command 'genericmiot'. maybe you can help ?

@rytilahti
Copy link
Owner Author

I must warn you that it is not yet ready for end users, but if you really want to test this, you need to clone and install this PR. I suppose the simplest way is to install the github client (https://cli.github.com/) and then do something like this:

git clone https://github.com/rytilahti/python-miio/
gh pr checkout 1581  # changes the working copy to this PR
poetry install
poetry shell  # opens a poetry shell to avoid using 'poetry run miiocli' for every command invokation
miiocli genericmiot --ip <addr> --token <token> status

@znut
Copy link

znut commented Nov 28, 2022

Hi, just drop by to say thanks and it's a great work! 👍
I've tried on my 2 models dreame.vacuum.p2140a and ijai.vacuum.v10, it works perfectly fine.
What I've tried (for both model)

  • status: report correctly
  • actions and call_action: works fine (tried few commands start / stop / charge / position)
  • settings and set: works fine (water / vacuum power)

Only thing I wanted to try but cannot find a way was the map thing but figured it might not be with the robot itself but the cloud.

@rytilahti
Copy link
Owner Author

Hi, just drop by to say thanks and it's a great work! +1
I've tried on my 2 models dreame.vacuum.p2140a and ijai.vacuum.v10, it works perfectly fine.

Thanks for giving it a try and reporting back, @znut! I'm really glad to hear that it's working on real devices, too :-) Would you mind letting me know what the output looks like, either here or via email? I have a single brightness-only bulb (and the simulator), so I'm just curious what the property values are to gain a better understanding of how these work.

Only thing I wanted to try but cannot find a way was the map thing but figured it might not be with the robot itself but the cloud.

Some of the map handling is most likely done in the cloud, but there are also several parametrized actions and write-only properties (if you search "skipped" in the console output, you will see them at least for the ijai vacuum) that are not yet supported at all. I will figure out a way to support those in some future PR after implementing standardized names, icons, etc. for homeassistant to use.

@znut
Copy link

znut commented Nov 29, 2022

@rytilahti sure, these are from ijai.vacuum.v10

status
------
Status (vacuum:status): Charging (value: 4) (from: Sleep (0), Idle (1), Paused (2), Go Charging (3), Charging (4), Sweeping (5), Sweeping and Mopping (6), Mopping (7), Upgrading (8))
Device Fault (vacuum:fault): 2105  (min: 0, max: 3000, step: 1)
[S] Mode (vacuum:mode): Sweep And Mop (value: 1) (from: Sweep (0), Sweep And Mop (1), Mop (2))
[S] Sweep Type (vacuum:sweep-type): Global (value: 0) (from: Global (0), Edge (2), Point (4), Remote (5))
Battery Level (battery:battery-level): 100 % (min: 0, max: 100, step: 1)
[S] Alarm (alarm:alarm): Play (value: 1) (from: Play (1))
[S] Volume (alarm:volume): 6 % (min: 0, max: 10, step: 1)
[S] repeat-state (sweep:repeat-state): Closed (value: 0) (from: Closed (0))
door-state (sweep:door-state): 尘盒 (value: 1) (from: 无 (0), 尘盒 (1), 水箱 (2), 二合一水箱 (3))
cloth-state (sweep:cloth-state): 没装 (value: 0) (from: 没装 (0), 装了 (1))
[S] suction-state (sweep:suction-state): 关 (value: 0) (from: 关 (0), 节能 (1), 标准 (2), 强劲 (3))
[S]  (sweep:water-state): 低 (value: 0) (from: 低 (0), 中 (1), 高 (2))
[S] mop-route (sweep:mop-route): Bowtype (value: 0) (from: Bowtype (0))
side-brush-life (sweep:side-brush-life): 75 % (min: 0, max: 100, step: 1)
side-brush-hours (sweep:side-brush-hours): 5 days, 16:00:00 (min: 0, max: 180, step: 1)
main-brush-life (sweep:main-brush-life): 87 % (min: 0, max: 100, step: 1)
main-brush-hours (sweep:main-brush-hours): 13 days, 4:00:00 (min: 0, max: 360, step: 1)
hypa-life (sweep:hypa-life): 75 % (min: 0, max: 100, step: 1)
hypa-hours (sweep:hypa-hours): 5 days, 16:00:00 (min: 0, max: 180, step: 1)
mop-life (sweep:mop-life): 77 % (min: 0, max: 100, step: 1)
mop-hours (sweep:mop-hours): 5 days, 20:00:00 (min: 0, max: 180, step: 1)
[S] time-zone (sweep:time-zone): -25200  (min: -99999, max: 99999, step: 1)
[S] cur-lang (sweep:cur-lang): ZH_CN
 (sweep:cleaning-time): 0  (min: 0, max: 120, step: 1)
 (sweep:cleaning-area): 0  (min: 0, max: 1200, step: 1)
multi-prop-vacuum (sweep:multi-prop-vacuum): [0,0,0,0,0,0,-25200,68,2971,"3448226V2208A02156"] None
[S] consumablesinfo (sweep:consumablesinfo): [87_316,75_136,75_136,77_140] None
charge-pose (sweep:charge-pose): [0,0] None
all-enable-count (order:all-enable-count): hello
[S] target-point (point-zone:target-point): 0,0
[S] remember-state (map:remember-state): Close (value: 0) (from: Close (0))
cur-map-id (map:cur-map-id): 0  (min: 0, max: 4294967295, step: 1)
map-num (map:map-num): 0  (min: 0, max: 5, step: 1)
 (map:cur-cleaning-path): hello
 (map:build-map): 不新建图(不写) (value: 0) (from: 不新建图(不写) (0), 只建图不清扫 (1), 建图+清扫(暂时无效) (2))
has-new-map (map:has-new-map): Without Map To Save (value: 0) (from: Without Map To Save (0), Wait To Rename Map (1))
chargingbase (map:chargingbase): 147_150 None
[S] map-privacy (map:map-privacy): Enable (value: 0) (from: Enable (0), DisEnable (1))
[S] robot-location (map:robot-location): 146_144_3.000000 None
dnd-enable (disturb:dnd-enable): 关 (value: 0) (from: 关 (0), 开 (1))
dnd-start-hour (disturb:dnd-start-hour): 0:00:00 (min: 0, max: 23, step: 1)
dnd-start-minute (disturb:dnd-start-minute): 0:00:00 (min: 0, max: 59, step: 1)
dnd-end-hour (disturb:dnd-end-hour): 0:00:00 (min: 0, max: 23, step: 1)
dnd-end-minute (disturb:dnd-end-minute): 0:00:00 (min: 0, max: 59, step: 1)
multi-prop-dnd (disturb:multi-prop-dnd): [0,23,0,9,0]
actions
-------
vacuum:start-sweep              Start Sweep
vacuum:stop-sweeping            Stop Sweeping
vacuum:start-only-sweep         Start Only Sweep
vacuum:start-sweep-mop          Start Sweep Mop
vacuum:start-mop                Start Mop
battery:start-charge            Start Charge
sweep:set-calibration           set-calibration
order:get               get
point-zone:start-point-clean            start-point-clean
point-zone:start-zone-clean             start-zone-clean
map:get-map-list                get-map-list
map:reset-map           reset-map
map:reset-map-ii                reset-map-ii
map:upmapdata           upmapdata
map:getchargingbase             getchargingbase
language:get-download-status            get-download-status
language:update-log             update-log
language:clean-downlad-status           clean-downlad-status
settings
--------
# vacuum:mode (Mode)  urn: urn:miot-spec-v2:property:mode:00000008:ijai-v10:1
  siid: 2
  piid: 4
# vacuum:sweep-type (Sweep Type)  urn: urn:miot-spec-v2:property:sweep-type:000000D3:ijai-v10:1
  siid: 2
  piid: 8
# alarm:alarm (Alarm)  urn: urn:miot-spec-v2:property:alarm:00000012:ijai-v10:1
  siid: 4
  piid: 1
# alarm:volume (Volume)  urn: urn:miot-spec-v2:property:volume:00000013:ijai-v10:1
  siid: 4
  piid: 2
# sweep:repeat-state (repeat-state)  urn: urn:ijai-spec:property:repeat-state:00000001:ijai-v10:1
  siid: 7
  piid: 1
# sweep:suction-state (suction-state)  urn: urn:ijai-spec:property:suction-state:00000005:ijai-v10:1
  siid: 7
  piid: 5
# sweep:water-state ()  urn: urn:ijai-spec:property:water-state:00000006:ijai-v10:1
  siid: 7
  piid: 6
# sweep:mop-route (mop-route)  urn: urn:ijai-spec:property:mop-route:00000007:ijai-v10:1
  siid: 7
  piid: 7
# sweep:time-zone (time-zone)  urn: urn:ijai-spec:property:time-zone:00000014:ijai-v10:1
  siid: 7
  piid: 20
# map:remember-state (remember-state)  urn: urn:ijai-spec:property:remember-state:00000001:ijai-v10:1
  siid: 10
  piid: 1
# map:map-privacy (map-privacy)  urn: urn:ijai-spec:property:map-privacy:00000017:ijai-v10:1
  siid: 10
  piid: 23

@znut
Copy link

znut commented Nov 29, 2022

> set vacuum:mode 0

Running command set
[{'did': 'vacuum:mode', 'siid': 2, 'piid': 4, 'code': 0}]

> status
...
[S] Mode (vacuum:mode): Sweep (value: 0) (from: Sweep (0), Sweep And Mop (1), Mop (2))
...
> call_action vacuum:start-sweep-mop

Running command call_action
{'code': 0, 'out': []}

> call_action language:get-download-status

Running command call_action
{'code': 0, 'out': [{'piid': 1, 'value': 'en_US'}, {'piid': 2, 'value': 'en_US'}, {'piid': 3, 'value': 0}, {'piid': 4, 'value': 0}]}

BTW, I just commented out all the warning debug log because it flooded my console every time lol

@znut
Copy link

znut commented Nov 29, 2022

This is from the dreame

> status

Status (vacuum:status): Idle (value: 2) (from: Sweeping (1), Idle (2), Paused (3), Error (4), Go Charging (5), Charging (6), Mopping (7))
Device Fault (vacuum:fault): 0  (min: 0, max: 100, step: 1)
[S] Mode (vacuum:mode): Basic (value: 1) (from: Silent (0), Basic (1), Strong (2), Full Speed (3))
Battery Level (battery:battery-level): 100 % (min: 0, max: 100, step: 1)
Charging State (battery:charging-state): Charging (value: 1) (from: Charging (1), Not Charging (2), Go Charging (5))
Brush Left Time (brush-cleaner:brush-left-time): 8 days, 7:00:00 (min: 0, max: 200, step: 1)
Brush Life Level (brush-cleaner:brush-life-level): 99 % (min: 0, max: 100, step: 1)
Filter Life Level (filter:filter-life-level): 45 % (min: 0, max: 100, step: 1)
Filter Left Time (filter:filter-left-time): 2 days, 20:00:00 (min: 0, max: 150, step: 1)
工作模式 (vacuum-extend:work-mode): 14  (min: 0, max: 50, step: 1)
清扫时长 (vacuum-extend:cleaning-time): 0:50:00 (min: 0, max: 32767, step: 1)
清扫面积 (vacuum-extend:cleaning-area): 51  (min: 0, max: 32767, step: 1)
[S] 清扫模式 (vacuum-extend:cleaning-mode): 1 (value: 1) (from: 0 (0), 1 (1), 2 (2), 3 (3))
[S]  (vacuum-extend:mop-mode): 3  (min: 1, max: 3, step: 1)
水箱状态 (vacuum-extend:waterbox-status): 1 (value: 1) (from: 0 (0), 1 (1))
 (vacuum-extend:task-status): Notask (value: 0) (from: Notask (0), AutoClean (1), CustomClean (2), SelectAreanClean (3), SpotArea (4))
[S] break-point-restart (vacuum-extend:break-point-restart): 打开 (value: 1) (from: 关闭 (0), 打开 (1))
[S] carpet-press (vacuum-extend:carpet-press): 关闭 (value: 0) (from: 关闭 (0), 打开 (1))
serial-number (vacuum-extend:serial-number): 33239/BFACX12N301638
keep-sweeper-time (vacuum-extend:keep-sweeper-time): 0:00:00 (min: -1, max: 1000000, step: 1)
faults (vacuum-extend:faults): 0
[S] 使能 (do-not-disturb:enable): False
[S] 勿扰起始时间 (do-not-disturb:start-time): 22:00
[S] 勿扰结束时间 (do-not-disturb:end-time): 08:00
[S] 音量 (audio:volume): 75  (min: 0, max: 100, step: 1)
[S] 语音包id (audio:voice-packet-id): EN
语音包切换时的状态 (audio:voice-change-state): {\"id\":\"\",\"state\":\"idle\",\"progress\":0}
主机时区获取 (time:time-zone): Asia/Bangkok
[S] timer-clean (time:timer-clean): 65-1-07:30-1111111-1-5-1-2-0;78-0-16:00-1111111-1-5-1-2-0
首次清扫的开始时间 (clean-logs:first-clean-time): 1662720039  (min: 0, max: 4294967295, step: 1)
总清扫时间 (clean-logs:total-clean-time): 3 days, 9:23:00 (min: 0, max: 4294967295, step: 1)
总清扫次数 (clean-logs:total-clean-times): 102  (min: 0, max: 4294967295, step: 1)
总清扫面积 (clean-logs:total-clean-area): 4816  (min: 0, max: 4294967295, step: 1)
[S] save-map-status (vslam-extend:save-map-status): 打开 (value: 1) (from: 关闭 (0), 打开 (1))
> call_action audio:play-sound

Running command call_action
{'did': 'call-7-2', 'siid': 7, 'aiid': 2, 'out': [], 'code': 0}
> sensors

Running command sensors
{'vacuum:status': SensorDescriptor(id='vacuum:status', type=<class 'int'>, name='Status', property='vacuum:status', unit=None), 'vacuum:fault': SensorDescriptor(id='vacuum:fault', type=<class 'int'>, name='Device Fault', property='vacuum:fault', unit=None), 'battery:battery-level': SensorDescriptor(id='battery:battery-level', type=<class 'int'>, name='Battery Level', property='battery:battery-level', unit=None), 'battery:charging-state': SensorDescriptor(id='battery:charging-state', type=<class 'int'>, name='Charging State', property='battery:charging-state', unit=None), 'brush-cleaner:brush-left-time': SensorDescriptor(id='brush-cleaner:brush-left-time', type=<class 'int'>, name='Brush Left Time', property='brush-cleaner:brush-left-time', unit=None), 'brush-cleaner:brush-life-level': SensorDescriptor(id='brush-cleaner:brush-life-level', type=<class 'int'>, name='Brush Life Level', property='brush-cleaner:brush-life-level', unit=None), 'filter:filter-life-level': SensorDescriptor(id='filter:filter-life-level', type=<class 'int'>, name='Filter Life Level', property='filter:filter-life-level', unit=None), 'filter:filter-left-time': SensorDescriptor(id='filter:filter-left-time', type=<class 'int'>, name='Filter Left Time', property='filter:filter-left-time', unit=None), 'vacuum-extend:work-mode': SensorDescriptor(id='vacuum-extend:work-mode', type=<class 'int'>, name='工作模式', property='vacuum-extend:work-mode', unit=None), 'vacuum-extend:cleaning-time': SensorDescriptor(id='vacuum-extend:cleaning-time', type=<class 'int'>, name='清扫 时长', property='vacuum-extend:cleaning-time', unit=None), 'vacuum-extend:cleaning-area': SensorDescriptor(id='vacuum-extend:cleaning-area', type=<class 'int'>, name='清扫面积', property='vacuum-extend:cleaning-area', unit=None), 'vacuum-extend:waterbox-status': SensorDescriptor(id='vacuum-extend:waterbox-status', type=<class 'int'>, name='水箱状态', property='vacuum-extend:waterbox-status', unit=None), 'vacuum-extend:task-status': SensorDescriptor(id='vacuum-extend:task-status', type=<class 'int'>, name='', property='vacuum-extend:task-status', unit=None), 'vacuum-extend:serial-number': SensorDescriptor(id='vacuum-extend:serial-number', type=<class 'str'>, name='serial-number', property='vacuum-extend:serial-number', unit=None), 'vacuum-extend:keep-sweeper-time': SensorDescriptor(id='vacuum-extend:keep-sweeper-time', type=<class 'int'>, name='keep-sweeper-time', property='vacuum-extend:keep-sweeper-time', unit=None), 'vacuum-extend:faults': SensorDescriptor(id='vacuum-extend:faults', type=<class 'str'>, name='faults', property='vacuum-extend:faults', unit=None), 'do-not-disturb:start-time': SensorDescriptor(id='do-not-disturb:start-time', type=<class 'str'>, name='勿扰起始时间', property='do-not-disturb:start-time', unit=None), 'do-not-disturb:end-time': SensorDescriptor(id='do-not-disturb:end-time', type=<class 'str'>, name='勿扰结束时间', property='do-not-disturb:end-time', unit=None), 'audio:voice-packet-id': SensorDescriptor(id='audio:voice-packet-id', type=<class 'str'>, name='语音包id', property='audio:voice-packet-id', unit=None), 'audio:voice-change-state': SensorDescriptor(id='audio:voice-change-state', type=<class 'str'>, name='语音包切换时 的状态', property='audio:voice-change-state', unit=None), 'time:time-zone': SensorDescriptor(id='time:time-zone', type=<class 'str'>, name='主机时区获取', property='time:time-zone', unit=None), 'time:timer-clean': SensorDescriptor(id='time:timer-clean', type=<class 'str'>, name='timer-clean', property='time:timer-clean', unit=None), 'clean-logs:first-clean-time': SensorDescriptor(id='clean-logs:first-clean-time', type=<class 'int'>, name='首次清扫的开始时间', property='clean-logs:first-clean-time', unit=None), 'clean-logs:total-clean-time': SensorDescriptor(id='clean-logs:total-clean-time', type=<class 'int'>, name='总清扫时间', property='clean-logs:total-clean-time', unit=None), 'clean-logs:total-clean-times': SensorDescriptor(id='clean-logs:total-clean-times', type=<class 'int'>, name='总清扫次数', property='clean-logs:total-clean-times', unit=None), 'clean-logs:total-clean-area': SensorDescriptor(id='clean-logs:total-clean-area', type=<class 'int'>, name='总清扫面积', property='clean-logs:total-clean-area', unit=None)}

@xcraftster
Copy link

xcraftster commented Dec 5, 2022

Guys, after installing this PR and running this command:

'miiocli -d devtools miot-simulator --model zhimi.heater.nb1'

it gives me error:

ImportError: cannot import name 'MiotSpec' from 'micloud' (/home/user/.cache/pypoetry/virtualenvs/python-miio-Po5YGcTv-py3.8/lib/python3.8/site-packages/micloud/__init__.py)

@rytilahti
Copy link
Owner Author

rytilahti commented Dec 5, 2022

@xcraftster ah, sorry for that. This depends on Squachen/micloud#11 being installed, I'll remove the latest commit from this PR and create a separate PR for that. edit: moved to #1610

@rytilahti rytilahti force-pushed the feat/genericmiot_integration branch from 4e9771c to 9abeac1 Compare December 5, 2022 11:21
@codecov-commenter
Copy link

codecov-commenter commented Dec 5, 2022

Codecov Report

Merging #1581 (ffef7fb) into master (1756fbc) will decrease coverage by 0.58%.
The diff coverage is 32.58%.

@@            Coverage Diff             @@
##           master    #1581      +/-   ##
==========================================
- Coverage   80.69%   80.10%   -0.59%     
==========================================
  Files         157      159       +2     
  Lines       15417    15651     +234     
  Branches     3427     3476      +49     
==========================================
+ Hits        12441    12538      +97     
- Misses       2724     2859     +135     
- Partials      252      254       +2     
Impacted Files Coverage Δ
miio/miot_device.py 88.15% <ø> (-0.31%) ⬇️
miio/integrations/genericmiot/genericmiot.py 25.45% <25.45%> (ø)
miio/miot_models.py 70.06% <57.14%> (-3.80%) ⬇️
miio/devicefactory.py 88.13% <90.00%> (+7.36%) ⬆️
miio/integrations/genericmiot/__init__.py 100.00% <100.00%> (ø)
miio/gateway/gateway.py 44.16% <0.00%> (ø)
miio/integrations/fan/dmaker/fan.py 94.84% <0.00%> (ø)
miio/integrations/fan/dmaker/fan_miot.py 90.64% <0.00%> (ø)
miio/integrations/vacuum/roborock/vacuum.py 62.21% <0.00%> (ø)
miio/integrations/light/yeelight/yeelight.py 79.60% <0.00%> (ø)
... and 7 more

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@rytilahti rytilahti changed the title Add genericmiot integration Add generic miot support Dec 5, 2022
@rytilahti
Copy link
Owner Author

I'm going to merge this now to make it easier to test and to continue developing it further. Thanks for your input @znut, it's great to have some real-world testing done :-)

@rytilahti rytilahti merged commit 27e74d2 into master Dec 5, 2022
@rytilahti rytilahti deleted the feat/genericmiot_integration branch December 5, 2022 20:38
@pplucky
Copy link

pplucky commented Dec 5, 2022

I must warn you that it is not yet ready for end users, but if you really want to test this, you need to clone and install this PR. I suppose the simplest way is to install the github client (https://cli.github.com/) and then do something like this:

git clone https://github.com/rytilahti/python-miio/
gh pr checkout 1581  # changes the working copy to this PR
poetry install
poetry shell  # opens a poetry shell to avoid using 'poetry run miiocli' for every command invokation
miiocli genericmiot --ip <addr> --token <token> status

I tried the latest master (after this being merged) with model zhimi.heater.mc2 and unfortunately just got the following:

ERROR:miio.click_common:Exception: not enough values to unpack (expected 2, got 1)
Traceback (most recent call last):
  File "/home/pi/python-miio/miio/click_common.py", line 54, in __call__
    return self.main(*args, **kwargs)
  File "/home/pi/.cache/pypoetry/virtualenvs/python-miio-RFxuN8Yf-py3.9/lib/python3.9/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/home/pi/.cache/pypoetry/virtualenvs/python-miio-RFxuN8Yf-py3.9/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/pi/.cache/pypoetry/virtualenvs/python-miio-RFxuN8Yf-py3.9/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/pi/.cache/pypoetry/virtualenvs/python-miio-RFxuN8Yf-py3.9/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/pi/.cache/pypoetry/virtualenvs/python-miio-RFxuN8Yf-py3.9/lib/python3.9/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/home/pi/python-miio/miio/click_common.py", line 310, in wrap
    result_msg = result_msg_fmt(**kwargs)
  File "/home/pi/python-miio/miio/integrations/genericmiot/genericmiot.py", line 28, in pretty_status
    props = result.property_dict()
  File "/home/pi/python-miio/miio/integrations/genericmiot/genericmiot.py", line 109, in property_dict
    service, prop_name = did.split(":")
ValueError: not enough values to unpack (expected 2, got 1)

I also tried with my hfjh.fishbowl.v2 and I got this instead:

WARNING:miio.miot_cloud:more than a single match for model hfjh.fishbowl.v2: [ReleaseInfo(model='hfjh.fishbowl.v2', status='released', type='urn:miot-spec-v2:device:fish-tank:0000A0A2:hfjh-v2:1', version=1), ReleaseInfo(model='hfjh.fishbowl.v2', status='released', type='urn:miot-spec-v2:device:fish-tank:0000A0A2:hfjh-v2:2', version=2)], filtering with status=released
WARNING:miio.integrations.genericmiot.genericmiot:Got inputs for action, skipping as handling is unknown: urn=<URN urn:hfjh-spec:action:set-feed-single:00002801:hfjh-v2:1 parent:namespace='hfjh-spec' type='service' name='function' internal_id='00007801' model='hfjh-v2' version=1> description='set-feed-single' service=MiotService(siid=4, urn=<URN urn:hfjh-spec:service:function:00007801:hfjh-v2:1 parent:None>, description='function') aiid=1 inputs=[MiotProperty(urn=<URN urn:hfjh-spec:property:feed-num:0000000d:hfjh-v2:1 parent:namespace='hfjh-spec' type='service' name='function' internal_id='00007801' model='hfjh-v2' version=1>, description='feed-num', service=MiotService(siid=4, urn=<URN urn:hfjh-spec:service:function:00007801:hfjh-v2:1 parent:None>, description='function'), piid=13, format=<class 'int'>, access=['read', 'notify', 'write'], unit='none', range=[1, 30, 1], choices=None, value=None)] outputs=[]
WARNING:miio.integrations.genericmiot.genericmiot:Skipping write-only: urn=<URN urn:hfjh-spec:property:feed-record:00000010:hfjh-v2:1 parent:namespace='hfjh-spec' type='service' name='function' internal_id='00007801' model='hfjh-v2' version=1> description='feed-record' service=MiotService(siid=4, urn=<URN urn:hfjh-spec:service:function:00007801:hfjh-v2:1 parent:None>, description='function') piid=16 format=<class 'str'> access=[] unit='none' range=None choices=None value=None
WARNING:miio.integrations.genericmiot.genericmiot:Skipping write-only: urn=<URN urn:hfjh-spec:property:log:00000011:hfjh-v2:1 parent:namespace='hfjh-spec' type='service' name='function' internal_id='00007801' model='hfjh-v2' version=1> description='log' service=MiotService(siid=4, urn=<URN urn:hfjh-spec:service:function:00007801:hfjh-v2:1 parent:None>, description='function') piid=17 format=<class 'str'> access=[] unit='none' range=None choices=None value=None
[S] Switch Status (fish-tank:on): True
[S] Water Pump (fish-tank:water-pump): True
[S] Pump Flux (fish-tank:pump-flux): 5  (min: 5, max: 100, step: 1)
 (function:mcu-type): One (value: 1) (from: None (0), One (1))
[S] ledboard-model (function:ledboard-model): Color (value: 1) (from: Sun (0), Color (1), Stream (2))
[S] ledboard-brightness (function:ledboard-brightness): 0  (min: 0, max: 100, step: 1)
[S]  (function:ledboard-sun): 16737792 None (min: 0, max: 16777215, step: 1)
[S] ledboard-color (function:ledboard-color): 16777215 None (min: 0, max: 16777215, step: 1)
[S] ledboard-stream (function:ledboard-stream): 0  (min: 0, max: 360, step: 1)
[S] ledboard-speed (function:ledboard-speed): 100  (min: 0, max: 100, step: 1)
[S] ledboard-time-switch (function:ledboard-time-switch): False
[S] ledboard-time-open (function:ledboard-time-open): 07:00 
[S] ledboard-time-close (function:ledboard-time-close): 22:00 
[S] feed-time-switch (function:feed-time-switch): False
[S] feed-time (function:feed-time): 18:00 
[S] feed-num (function:feed-num): 1  (min: 1, max: 30, step: 1)
[S] key-switch (function:key-switch): False
[S] feed-time-week (function:feed-time-week): 48  (min: 0, max: 127, step: 1)
mcu-type-new (function:mcu-type-new): 1  (min: 0, max: 255, step: 1)

Not sure if there's any other useful insights I can provide.

I also own a careli.fryer.maf02 and can provide any details which may be required/useful.

@rytilahti
Copy link
Owner Author

rytilahti commented Dec 6, 2022

Hi @pplucky and thanks for testing!

I tried the latest master (after this being merged) with model zhimi.heater.mc2 and unfortunately just got the following:

Would you mind opening a new issue and checking with --debug (miiocli -d genericmiot ..) enabled how does the request (get_properties) and its response look like? Here's how it is with the simulator, but the real device firmware may do something that I didn't expect:

DEBUG    127.0.0.1:54321 >>: {'id': 2,                                                                                                                                                                                   miioprotocol.py:176
          'method': 'get_properties',                                                                                                                                                                                                       
          'params': [{'did': 'heater:on', 'piid': 1, 'siid': 2},                                                                                                                                                                            
                     {'did': 'heater:fault', 'piid': 2, 'siid': 2},                                                                                                                                                                         
                     {'did': 'heater:target-temperature', 'piid': 5, 'siid': 2},                                                                                                                                                            
                     {'did': 'countdown:countdown-time', 'piid': 1, 'siid': 3},                                                                                                                                                             
                     {'did': 'environment:temperature', 'piid': 7, 'siid': 4},                                                                                                                                                              
                     {'did': 'physical-controls-locked:physical-controls-locked',                                                                                                                                                           
                      'piid': 1,                                                                                                                                                                                                            
                      'siid': 5},                                                                                                                                                                                                           
                     {'did': 'alarm:alarm', 'piid': 1, 'siid': 6},                                                                                                                                                                          
                     {'did': 'indicator-light:brightness', 'piid': 3, 'siid': 7},                                                                                                                                                           
                     {'did': 'private-service:hw-enable', 'piid': 8, 'siid': 8},                                                                                                                                                            
                     {'did': 'private-service:use-time', 'piid': 9, 'siid': 8}]}                                                                                                                                                            
DEBUG    127.0.0.1:54321 (ts: 2022-12-06 01:05:10, id: 2) << {'id': 2,                                                                                                                                                   miioprotocol.py:206
          'result': [{'code': 0,                                                                                                                                                                                                            
                      'did': 'heater:on',                                                                                                                                                                                                   
                      'piid': 1,                                                                                                                                                                                                            
                      'siid': 2,                                                                                                                                                                                                            
                      'value': True},                           
<snip>
[S] Switch Status (heater:on): True (miot-spec-v2:property:heater:on)
Device Fault (heater:fault): 20  (min: 0, max: 255, step: 1) (miot-spec-v2:property:heater:fault)
[S] Target Temperature (heater:target-temperature): 20 None (min: 18, max: 28, step: 1) (miot-spec-v2:property:heater:target-temperature)
[S] Countdown Time (countdown:countdown-time): 7:00:00 (min: 0, max: 12, step: 1) (miot-spec-v2:property:countdown:countdown-time)
Temperature (environment:temperature): 92 None (min: -30, max: 100, step: 0) (miot-spec-v2:property:environment:temperature)
[S] Physical Control Locked (physical-controls-locked:physical-controls-locked): True (miot-spec-v2:property:physical-controls-locked:physical-controls-locked)
[S] Alarm (alarm:alarm): True (miot-spec-v2:property:alarm:alarm)
[S] Brightness (indicator-light:brightness): 1 % (min: 0, max: 1, step: 1) (miot-spec-v2:property:indicator-light:brightness)
[S] 恒温功能使能 (private-service:hw-enable): True (zhimi-spec:property:private-service:hw-enable)
用户使用时长 (private-service:use-time): 811460129 None (min: 0, max: 2147483647, step: 1) (zhimi-spec:property:private-service:use-time)
country-code (private-service:country-code): 中国香港 (value: 852) (from: 未知 (0), US, 美国 (1), KR,韩国 (82), EU,欧洲 (44), JP,日本 (81), RU,俄罗斯 (7), CN,中国 (86), 中国香港 (852), 中国台湾 (886), FR,法国 (33)) 
(zhimi-spec:property:private-service:country-code)

Not sure if there's any other useful insights I can provide.

I also own a careli.fryer.maf02 and can provide any details which may be required/useful.

When #1618 gets done, it could be useful to introduce metadata files for the namespaces (like zhimi-spec and hfjh-spec above) those devices you have are using, so that the names and icons would automatically show nicely formatted in homeassistant :-)

@pplucky
Copy link

pplucky commented Dec 6, 2022

Would you mind opening a new issue and checking with --debug (miiocli -d genericmiot ..) enabled how does the request (get_properties) and its response look like? Here's how it is with the simulator, but the real device firmware may do something that I didn't expect:

Here it is

@benedikt-wue
Copy link

Hi there,
I tried to manage my dreame.vacuum.p2140p with your awesome integration. But unfortunately no luck... I can start the robot with the command to go home und I can send the robot home with the command start_clean :-)
miiocli dreamevacuum --ip 192.168.188.60 --token 5633594d75716a57336d504a5a4b4463 --model dreame.vacuum.p2140p call_action (...)
Running command call_action
WARNING:miio.miot_device:Unable to find mapping for dreame.vacuum.p2140p, falling back to dreame.vacuum.mc1808
{'did': 'call-3-1', 'siid': 3, 'aiid': 1, 'out': [], 'code': 0}

I tried to implement the commands of p2140p into the file, but it is not recognized. Any help for integration of the p2140p?
Thanks a lot und regards from Germany!

@rytilahti
Copy link
Owner Author

Hi, if you install the git version, you can use the generic miot instead of dreamevacuum to control this vacuum: https://github.com/rytilahti/python-miio/#controlling-modern-miot-devices

If you are having problems with it, please open new, separate issues for them :-)

@benedikt-wue
Copy link

Hi there

That´s what I tried before:
smarthome@smarthome:~/python-miio/miio $ miiocli genericmiot --ip 192.168.188.60 --token 5633594d75716a57336d504a5a4b4463 call_action status
Usage: miiocli [OPTIONS] COMMAND [ARGS]...
Try 'miiocli --help' for help.

Error: No such command 'genericmiot'.

same with
smarthome@smarthome:~/python-miio/ $ miiocli genericmiot --ip 192.168.188.60 --token 5633594d75716a57336d504a5a4b4463 call_action status
Usage: miiocli [OPTIONS] COMMAND [ARGS]...
Try 'miiocli --help' for help.

Error: No such command 'genericmiot'.

Any ideas? I just cloned the git like
git clone https://github.com/rytilahti/python-miio.git

So I don´t have an idea why it´s not working...

@rytilahti
Copy link
Owner Author

You need to install it, too, if you didn't do that already. The simplest way is to install it directly using pip (the second command for the development version at https://github.com/rytilahti/python-miio#installation) or running pip install . in the cloned repository directory.

But please create either a new issue or a discussion in the discussions forum (https://github.com/rytilahti/python-miio/discussions) as this is not directly related to this pull request :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment