mihai-dinculescu / tapo

Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with light bulbs (L510, L520, L530, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P115, P300), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315).
MIT License
313 stars 30 forks source link

Add P300 support #192

Closed Michal-Szczepaniak closed 2 months ago

Michal-Szczepaniak commented 3 months ago

Initial P300 support. Sorry for more chaotic PR this time but my hope is you can sort out what is missing :)

So what I gathered. P300 is more like a hub the h100 where it has 3 child plugs. From P300 the only diff is the lack of overheated. Then it has 3 child generic plugs, that also don't have overheated and the last_state differs. For some reason it couldn't find field model which i commented out but it is present. Sadly P300 cannot measure power so there's that.

I have tiny request. The children have position field which is number 1-3 corresponding to the plug number that's on the device. I would really love you if you made it possible to do power_strip.on(1) that would make my life infinitely easier.

I'm including device and child jsons in case i missed something, i'm sure they will be useful.

Device info: Object {"avatar": String(""), "device_id": String("<redacted>"), "fw_id": String("<redacted>"), "fw_ver": String("<redacted>"), "has_set_location_info": Bool(false), "hw_id": String("<redacted>"), "hw_ver": String("1.0"), "ip": String("<redacted>"), "lang": String("en_US"), "latitude": Number(<redacted>), "longitude": Number(<redacted>), "mac": String("<redacted>"), "model": String("P300"), "nickname": String(""), "oem_id": String("<redacted>"), "region": String("<redacted>"), "rssi": Number(-45), "signal_level": Number(3), "specs": String(""), "ssid": String("<redacted>"), "time_diff": Number(60), "type": String("SMART.TAPOPLUG")}

Child info:

{"child_device_list": Array [Object {"auto_off_remain_time": Number(0), "auto_off_status": String("off"), "avatar": String(""), "bind_count": Number(1), "category": String("plug.powerstrip.sub-plug"), "default_states": Object {"type": String("last_states")}, "device_id": String("<redacted>"), "device_on": Bool(false), "fw_id": String("<redacted>"), "fw_ver": String("<redacted>"), "has_set_location_info": Bool(false), "hw_id": String("<redacted>"), "hw_ver": String("1.0"), "latitude": Number(<redacted>), "longitude": Number(<redacted>), "mac": String("<redacted>"), "model": String("P300"), "nickname": String("<redacted>"), "oem_id": String("<redacted>"), "on_time": Number(0), "original_device_id": String("<redacted>"), "overheat_status": String("normal"), "position": Number(3), "region": String("<redacted>"), "slot_number": Number(3), "status_follow_edge": Bool(true), "type": String("SMART.TAPOPLUG")}, Object {"auto_off_remain_time": Number(0), "auto_off_status": String("off"), "avatar": String(""), "bind_count": Number(1), "category": String("plug.powerstrip.sub-plug"), "default_states": Object {"type": String("last_states")}, "device_id": String("<redacted>"), "device_on": Bool(false), "fw_id": String("<redacted>"), "fw_ver": String("<redacted>"), "has_set_location_info": Bool(false), "hw_id": String("<redacted>"), "hw_ver": String("1.0"), "latitude": Number(<redacted>), "longitude": Number(<redacted>), "mac": String("<redacted>"), "model": String("P300"), "nickname": String("<redacted>"), "oem_id": String("<redacted>"), "on_time": Number(0), "original_device_id": String("<redacted>"), "overheat_status": String("normal"), "position": Number(2), "region": String("<redacted>"), "slot_number": Number(3), "status_follow_edge": Bool(true), "type": String("SMART.TAPOPLUG")}, Object {"auto_off_remain_time": Number(0), "auto_off_status": String("off"), "avatar": String(""), "bind_count": Number(1), "category": String("plug.powerstrip.sub-plug"), "default_states": Object {"type": String("last_states")}, "device_id": String("<redacted>"), "device_on": Bool(false), "fw_id": String("<redacted>"), "fw_ver": String("<redacted>"), "has_set_location_info": Bool(false), "hw_id": String("<redacted>"), "hw_ver": String("1.0"), "latitude": Number(<redacted>), "longitude": Number(<redacted>), "mac": String("<redacted>"), "model": String("P300"), "nickname": String("<redacted>"), "oem_id": String("<redacted>"), "on_time": Number(0), "original_device_id": String("<redacted>"), "overheat_status": String("normal"), "position": Number(1), "region": String("<redacted>"), "slot_number": Number(3), "status_follow_edge": Bool(true), "type": String("SMART.TAPOPLUG")}], "start_index": Number(0), "sum": Number(3)}
mihai-dinculescu commented 3 months ago

Thank you for this! It takes the implementation for P300 very far. Can you please post the device info for one of the children?

Regarding your question about ergonomics, how about replacing p300(&self, device_id: impl Into<String>) with something like plug(&self, identifier: P300PlugIdentifier) where P300PlugIdentifier is something like

pub enum P300PlugIdentifier<'a> {
    ByDeviceId(&'a str),
    ByNickname(&'a str),
    ByPosition(u8),
}

This will allow for uses like:

device.plug(P300PlugIdentifier::ByDeviceId("<id>"))
device.plug(P300PlugIdentifier::ByNickname("plug 1"))
device.plug(P300PlugIdentifier::ByPosition(1))

The latter two (or maybe all three, really?) would have to call get_child_device_list internally to get the device ID from the nickname or position.

Michal-Szczepaniak commented 3 months ago

Would be great :)

Doesn't child device list have the child device info? But anyway here's child device info

{"auto_off_remain_time": Number(0), "auto_off_status": String("off"), "avatar": String(""), "bind_count": Number(1), "category": String("plug.powerstrip.sub-plug"), "default_states": Object {"type": String("last_states")}, "device_id": String("<redacted>"), "device_on": Bool(false), "fw_id": String("<redacted>"), "fw_ver": String("<redacted>"), "has_set_location_info": Bool(false), "hw_id": String("<redacted>"), "hw_ver": String("1.0"), "latitude": Number(<redacted>), "longitude": Number(<redacted>), "mac": String("<redacted>"), "model": String("P300"), "nickname": String("<redacted>="), "oem_id": String("<redacted>"), "on_time": Number(0), "original_device_id": String("<redacted>"), "overheat_status": String("normal"), "position": Number(1), "region": String("<redacted>"), "slot_number": Number(3), "status_follow_edge": Bool(true), "type": String("SMART.TAPOPLUG")}

mihai-dinculescu commented 3 months ago

Doesn't child device list have the child device info?

It should, but considering that the Tapo API is undocumented and has plenty of surprises, I just wanted to double check :)

I've made some changes to separate the hub and power strip children. This should greatly simplify their usage.

Michal-Szczepaniak commented 3 months ago

Lol how you were able to push changes to my fork is beyond me but good work :D

mihai-dinculescu commented 3 months ago

Lol how you were able to push changes to my fork is beyond me but good work :D

That's thanks to a fairly new feature in GitHub to "Allow edits from maintainers". It's a checkbox on PR creation that's checked by default AFAIK.

mihai-dinculescu commented 2 months ago

I've updated the child's device info and added multiple ways to find devices.

Can you please have a try and let me know your findings and thoughts?

Michal-Szczepaniak commented 2 months ago

Sure thing, might take me a bit though

Michal-Szczepaniak commented 2 months ago

Okay first issue Error: Serde(Error("invalid type: string \"normal\", expected internally tagged enum OverheatStatus", line: 1, column: 491))

It's on the getting child devices step

mihai-dinculescu commented 2 months ago

Ah, good catch. Can you try again, please?

Michal-Szczepaniak commented 2 months ago

tried the iteration aka byId and tried byPosition and both work fine 👌