Skip to content

Add TL-WR844N router client - #167

Merged
AlexandrErohin merged 1 commit into
AlexandrErohin:betafrom
DeadmaroZ-TLOTL:add-tl-wr844n-client
Jul 29, 2026
Merged

Add TL-WR844N router client#167
AlexandrErohin merged 1 commit into
AlexandrErohin:betafrom
DeadmaroZ-TLOTL:add-tl-wr844n-client

Conversation

@DeadmaroZ-TLOTL

Copy link
Copy Markdown
Contributor

Summary

  • add a dedicated TplinkWR844NRouter client for TL-WR844N v1.0
  • handle firmware that returns plaintext 00000 data blocks after C80-style authorization
  • parse client devices from the DHCP lease block so client MAC/IP/hostnames are mapped correctly
  • register the client in the provider and public package exports

Testing

  • PYTHONPATH=. ..\\..\\.venv_tplink\\Scripts\\python.exe -m unittest discover ./test (317 tests)
  • live checked provider detection, firmware, and status against a TL-WR844N v1.0 running firmware 1.10.0 Build 211011 Rel.66152n(4555)

@AlexandrErohin

Copy link
Copy Markdown
Owner

@DeadmaroZ-TLOTL Thank you for PR! It looks like overrides for get_firmware, get_status are mostly same with parrent methods. Also I see that https://github.com/AlexandrErohin/TP-Link-Archer-C6U/blob/main/test/test_client_c80.py#L326 has block 00000. Could you tell me what the response is that brakes get_firmware from TplinkC80Router?

@DeadmaroZ-TLOTL

Copy link
Copy Markdown
Contributor Author

Thanks for checking. You are right about get_firmware; the parsed block is compatible with the parent method, so I removed the get_firmware override and now only rely on the plaintext-aware _decrypt_data() hook there.

The response that breaks TplinkC80Router.get_firmware() is not different in shape after decryption. The issue is that TL-WR844N returns this C80-style block as the raw HTTP response body even for the authenticated request:

00000
id 0|1,0,0
fullName 300Mbps%20Wi-Fi%20Router
facturer TP-Link
modelName TL-WR844N
modelVer 1.0
softVer 1.10.0%20Build%20211011%20Rel.66152n(4555)
hardVer TL-WR844N%201.0
prodId 0x8440001

So the unmodified parent method calls _decrypt_data(response.text), tries to AES-decrypt that plaintext 00000... body, and fails with:

ValueError: Data must be padded to 16 byte boundary in CBC mode

That is why the subclass keeps only the plaintext handling in _decrypt_data() for firmware.

get_status still needs a custom path because the TL-WR844N response I captured does not provide the C80 client block (13|1,0,0) in the same way; the usable connected-device data is in the DHCP lease block (9|1,0,0).

@AlexandrErohin

Copy link
Copy Markdown
Owner

@DeadmaroZ-TLOTL Does your router response anything for (13|1,0,0) ? And does your router response for any of these 33|1,1,0 , 33|2,1,0, 33|1,2,0, 33|2,2,0 ?

@DeadmaroZ-TLOTL

Copy link
Copy Markdown
Contributor Author

I re-checked this against the live TL-WR844N v1.0 router today after the C80-style authorization.

All of these requests do get an HTTP 200 response and the raw body starts with plaintext 00000, but the router does not return a matching id 13|1,0,0 block and it also does not return any matching id 33|... block for these requests:

13|1,0,0   -> 00000 response, no id 13|1,0,0 block
33|1,1,0   -> 00000 response, no id 33|1,1,0 block
33|2,1,0   -> 00000 response, no id 33|2,1,0 block
33|1,2,0   -> 00000 response, no id 33|1,2,0 block
33|2,2,0   -> 00000 response, no id 33|2,2,0 block
combined request with all of the above -> 00000 response, still no id 13 and no id 33 blocks

Instead the router returns a set of other blocks. The useful client data is consistently in id 9|1,0,0, with DHCP-style fields like:

hostName <idx> ...
mac <idx> ...
ip <idx> ...
state <idx> ...

On the live router this block currently contains the active clients; the other entries are empty/zero-filled. That is why I used 9|1,0,0 for TL-WR844N status parsing rather than the C80 13|1,0,0 client block or the 33|... Wi-Fi blocks.

@AlexandrErohin

Copy link
Copy Markdown
Owner

@DeadmaroZ-TLOTL I think that some other routers may have same behavior with yours - so checking in support method only compatible with your router will limit the use of this client. I think would be great to combine your get_status and get_status from TplinkC80Router to provide compatibility to more routers.

Could you run this code and post the output?

def get_status(self) -> Status:
    request_text = '#'.join([
        '1|1,0,0',
        '4|1,0,0',
        '23|1,0,0',
        '13|1,0,0',
        '33|1,1,0',
        '33|2,1,0',
        '33|1,2,0',
        '33|2,2,0',
        '33|1,9,0',
        '33|2,9,0',
    ])
    response_text = self._request_plaintext(request_text)
    data_blocks = self._parse_data_blocks(response_text)
    print(data_blocks)

    request_text = '#'.join([
        '1|1,0,0',
        '4|1,0,0',
        '9|1,0,0',
        '23|1,0,0',
        '0|1,0,0',
    ])
    response_text = self._request_plaintext(request_text)
    data_blocks = self._parse_data_blocks(response_text)
    print(data_blocks)

@DeadmaroZ-TLOTL

Copy link
Copy Markdown
Contributor Author

I re-ran direct probes against the TL-WR844N.

For 13|1,0,0: the router does answer with HTTP 200 and a plaintext body starting with 00000\r\n. The response is not a small/single deterministic block though; it is a large snapshot with multiple id ...|1,0,0 blocks. In one fresh probe the response did include id 13|1,0,0, for example:

00000
id 4|1,0,0
ip 192.168.2.1
mask 255.255.255.0
...
id 13|1,0,0
ip 0 192.168.2.100
ip 1 192.168.2.102
ip 2 192.168.2.103
...

But repeated direct probes for the same 13|1,0,0 request also returned large plaintext snapshots without the id 13|1,0,0 block. So on this firmware it looks like the requested block id is not reliably honored in the C80 way.

For the Wi-Fi requests:

33|1,1,0
33|2,1,0
33|1,2,0
33|2,2,0

all four requests returned HTTP 200 / plaintext 00000... responses, but none of the responses contained the requested id 33|... block. They returned unrelated id ...|1,0,0 snapshot blocks instead.

That is why I avoided the parent C80 Wi-Fi block parsing for WR844N. The useful client data I can consistently get is from the DHCP-style 9|1,0,0 block (hostName, mac, ip, state, expires). For Wi-Fi capabilities I set 2.4G host Wi-Fi true and 5G/guest/IoT false based on the TL-WR844N model capability instead of reading 33|... blocks.

@AlexandrErohin

Copy link
Copy Markdown
Owner

@DeadmaroZ-TLOTL Do these methods work good for your router? reboot, set_wifi, get_ipv4_status, get_ipv4_reservations, get_dhcp_leases, get_vpn_status ?

@DeadmaroZ-TLOTL

Copy link
Copy Markdown
Contributor Author

I ran the exact two request groups from your snippet against the live TL-WR844N after C80-style authorization.

The full data_blocks output is very large and contains local hostnames/MAC/IPs, so I am posting a redacted/truncated version. The important part is the block IDs returned by each request.

First request group

1|1,0,0#4|1,0,0#23|1,0,0#13|1,0,0#33|1,1,0#33|2,1,0#33|1,2,0#33|2,2,0#33|1,9,0#33|2,9,0

Response:

len=72368
starts='00000\r\nid 50|1,0,0\r\n'
block_ids=[
  '50|1,0,0', '7|1,0,0', '36|1,0,0', '5|1,0,0',
  '10|1,0,0', '8|1,0,0', '2|1,0,0', '3|1,0,0',
  '45|1,0,0', '96|1,0,0', '9|1,0,0', '75|1,0,0',
  '52|1,0,0', '34|1,0,0', '4|1,0,0', '26|1,0,0',
  '18|1,0,0', '27|1,0,0', '1|1,0,0', '0|1,0,0',
  '6|1,0,0', '21|1,0,0'
]

There is no 13|1,0,0 block and no 33|... block in that response, even though those IDs were requested. It does include 9|1,0,0, and that block has the DHCP/client shape:

9|1,0,0:
  hostName 0 <redacted>
  hostName 1 <redacted>
  ...
  mac 0 <redacted>
  mac 1 <redacted>
  ...
  ip 0 <redacted>
  ip 1 <redacted>
  ...
  state 0 ...
  expires 0 ...

It also includes expected generic blocks like:

1|1,0,0:
  authKey <redacted>
  mac 0 <redacted>
  mac 1 <redacted>

4|1,0,0:
  ip <redacted>
  mask <redacted>
  mode 0
  smartIp 0

0|1,0,0:
  fullName 300Mbps%20Wi-Fi%20Router
  facturer TP-Link
  modelName TL-WR844N
  modelVer 1.0
  softVer 1.10.0%20Build%20211011%20Rel.66152n(4555)
  hardVer TL-WR844N%201.0

Second request group

1|1,0,0#4|1,0,0#9|1,0,0#23|1,0,0#0|1,0,0

Response:

len=16106
starts='00000\r\nid 5|1,0,0\r\np'
block_ids=[
  '5|1,0,0', '20|1,0,0', '99|1,0,0', '23|1,0,0',
  '38|1,0,0', '59|1,0,0', '7|1,0,0', '25|1,0,0',
  '45|1,0,0', '0|1,0,0', '46|1,0,0', '1|1,0,0',
  '6|1,0,0', '3|1,0,0', '21|1,0,0'
]

This one did return 23|1,0,0, 0|1,0,0, and 1|1,0,0, but it did not return 4|1,0,0 or 9|1,0,0 in this run, despite them being requested.

So the behavior seems a bit odd on this firmware: the router returns valid plaintext 00000 snapshots, but the returned block set is not reliably the same as the requested block set. That is why the TL-WR844N status code currently has to be defensive: use C80-like blocks when they are present, but fall back to the DHCP-style 9|1,0,0 block when it is present, and avoid assuming the requested 13|... or 33|... blocks exist.

@AlexandrErohin

AlexandrErohin commented Jul 24, 2026

Copy link
Copy Markdown
Owner

@DeadmaroZ-TLOTL Sorry for the delay. Thank you for testing. I tried to make a more universal client using the results of your testing and your code example. Could you please test these updates? dbe848d I created test branch for that

@AlexandrErohin

Copy link
Copy Markdown
Owner

@DeadmaroZ-TLOTL I have released v5.26.0 . Could you test it please?

@AlexandrErohin
AlexandrErohin changed the base branch from main to beta July 29, 2026 13:28
@AlexandrErohin
AlexandrErohin merged commit 23f7fcf into AlexandrErohin:beta Jul 29, 2026
AlexandrErohin added a commit that referenced this pull request Jul 29, 2026
* Add TL-WR844N router client (#167)

* fix for TL-WR844N

---------

Co-authored-by: DeadmaroZ-TLOTL <150045808+DeadmaroZ-TLOTL@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants