Skip to content

Commit

Permalink
chore(cli): download without device_type and discovery add get_sn (#343)
Browse files Browse the repository at this point in the history
1. download without device_type
2. discover to get_sn
  • Loading branch information
wuwentao authored Dec 24, 2024
1 parent d9e4f79 commit 2ab6b67
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 12 deletions.
43 changes: 37 additions & 6 deletions midealocal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import logging
import sys
from argparse import ArgumentParser, Namespace
from argparse import ArgumentParser, BooleanOptionalAction, Namespace
from pathlib import Path
from typing import Any, NoReturn

Expand All @@ -28,7 +28,7 @@
NoSupportedProtocol,
)
from midealocal.devices import device_selector
from midealocal.discover import discover
from midealocal.discover import SERIAL_TYPE1_LENGTH, discover
from midealocal.exceptions import SocketException
from midealocal.version import __version__

Expand Down Expand Up @@ -86,15 +86,20 @@ async def _get_keys(self, device_id: int) -> dict[int, dict[str, Any]]:

async def discover(self) -> list[MideaDevice]:
"""Discover device information."""
device_list: list[MideaDevice] = []

devices = discover(ip_address=self.namespace.host)

device_list: list[MideaDevice] = []
if len(devices) == 0:
_LOGGER.error("No devices found.")
return device_list

# Dump only basic device info from the base class
_LOGGER.info("Found %d devices.", len(devices))
# get sn
if self.namespace.get_sn:
_LOGGER.info("Found devices: %s", devices)
return device_list
for device in devices.values():
keys = (
{0: {"token": "", "key": ""}}
Expand Down Expand Up @@ -171,10 +176,11 @@ def save(self) -> None:

async def download(self) -> None:
"""Download lua from cloud."""
device_type = int.from_bytes(self.namespace.device_type or bytearray())
device_sn = str(self.namespace.device_sn)
# model and device_type will be get from SN or host
model: str | None = None
device_type: int = 0

# download with host ip
if self.namespace.host:
devices = discover(ip_address=self.namespace.host)

Expand All @@ -186,14 +192,33 @@ async def download(self) -> None:
device_type = device["type"]
device_sn = device["sn"]
model = device["model"]
# download with SN
elif self.namespace.device_sn:
device_sn = str(self.namespace.device_sn)
# manual input device_type exist
if self.namespace.device_type:
device_type = int.from_bytes(self.namespace.device_type or bytearray())
# no device type input, parse device_type from SN
elif len(device_sn) == SERIAL_TYPE1_LENGTH:
device_type = int.from_bytes(bytes.fromhex(device_sn[4:6]))
# parse model from SN
model = str(device_sn[9:17])
else:
_LOGGER.error("host or sn is mandatory")
return

cloud = await self._get_cloud()
_LOGGER.debug("Try to authenticate to the cloud.")
if not await cloud.login():
_LOGGER.error("Failed to authenticate to the cloud.")
return

_LOGGER.debug("Download lua file for %s [%s]", device_sn, hex(device_type))
_LOGGER.debug(
"Download lua file for %s [%s] %s",
device_sn,
hex(device_type),
model,
)
lua = await cloud.download_lua(str(Path()), device_type, device_sn, model)
_LOGGER.info("Downloaded lua file: %s", lua)

Expand Down Expand Up @@ -332,6 +357,12 @@ def main() -> NoReturn:
help="Hostname or IP address of a single device to discover.",
default=None,
)
discover_parser.add_argument(
"--get_sn",
help="Get device SN with host ip.",
default=False,
action=BooleanOptionalAction,
)
discover_parser.set_defaults(func=cli.discover)

decode_msg_parser = subparsers.add_parser(
Expand Down
94 changes: 88 additions & 6 deletions tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def setUp(self) -> None:
device_sn="",
user=False,
debug=True,
get_sn=False,
attribute="power",
value="0",
attr_type="bool",
Expand Down Expand Up @@ -108,8 +109,8 @@ async def test_discover(self) -> None:
"type": "AC",
"ip_address": "192.168.0.2",
"port": 6444,
"model": "AC123",
"sn": "AC123",
"model": "AC123000",
"sn": "0000AC12300000000000000000000000",
}
mock_cloud_instance = AsyncMock()
mock_device_instance = MagicMock()
Expand Down Expand Up @@ -146,6 +147,14 @@ async def test_discover(self) -> None:
99: {"token": "token", "key": "key"},
}

# test V3 device get_sn
self.namespace.get_sn = True
await self.cli.discover() # test V3 device get_sn
mock_discover.assert_called()
# set get_sn to default False after test done
self.namespace.get_sn = False

# test V3 device
await self.cli.discover() # V3 device
authenticate_mock.assert_called()
refresh_status_mock.assert_called_with(True)
Expand Down Expand Up @@ -226,14 +235,18 @@ async def test_download(self) -> None:
"type": 0xAC,
"ip_address": "192.168.0.2",
"port": 6444,
"model": "AC123",
"sn": "AC123",
"model": "ABCD1234",
"sn": "0000AC000ABCD1234000000000000000",
}
mock_cloud_instance = AsyncMock()
with (
patch(
"midealocal.cli.discover",
side_effect=[{}, {1: mock_device}, {1: mock_device}],
side_effect=[
{}, # test no device
{1: mock_device}, # test cloud login failed
{1: mock_device}, # test download lua with host ip
],
) as mock_discover,
patch.object(
self.cli,
Expand All @@ -242,25 +255,94 @@ async def test_download(self) -> None:
),
):
await self.cli.download() # No device found
# default is discover with host ip, test result is None
mock_discover.assert_called_once_with(ip_address=self.namespace.host)
mock_discover.reset_mock()

mock_cloud_instance.login.side_effect = [False, True]
mock_cloud_instance.login.side_effect = [
False, # test cloud login failed
True, # test download lua with host ip
True, # test download lua with SN
True, # test download lua with SN
]
await self.cli.download() # Cloud login failed
# default is discover with host ip
mock_discover.assert_called_once_with(ip_address=self.namespace.host)
mock_discover.reset_mock()
# test cloud login failed
mock_cloud_instance.login.assert_called_once()
mock_cloud_instance.login.reset_mock()

# download lua with host (default is host)
await self.cli.download()
# default is discover with host ip
mock_discover.assert_called_once_with(ip_address=self.namespace.host)
mock_discover.reset_mock()
# cloud login pass
mock_cloud_instance.login.assert_called_once()
mock_cloud_instance.login.reset_mock()
mock_cloud_instance.download_lua.assert_called_once_with(
str(Path()),
mock_device["type"],
mock_device["sn"],
mock_device["model"],
)
mock_cloud_instance.download_lua.reset_mock()
mock_cloud_instance.download_plugin.assert_called_once_with(
str(Path()),
mock_device["type"],
mock_device["sn"],
)
mock_cloud_instance.download_plugin.reset_mock()

# download lua with SN (set host to None and match elif)
self.namespace.host = None
self.namespace.device_sn = mock_device["sn"]
await self.cli.download()
# skip discover and cloud login pass
mock_cloud_instance.login.assert_called_once()
mock_cloud_instance.login.reset_mock()
mock_cloud_instance.download_lua.assert_called_once_with(
str(Path()),
mock_device["type"],
mock_device["sn"],
mock_device["model"],
)
mock_cloud_instance.download_lua.reset_mock()
mock_cloud_instance.download_plugin.assert_called_once_with(
str(Path()),
mock_device["type"],
mock_device["sn"],
)
mock_cloud_instance.download_plugin.reset_mock()

# download lua with SN and device_type
self.namespace.host = None
self.namespace.device_sn = mock_device["sn"]
self.namespace.device_type = bytes.fromhex("AC")
await self.cli.download()
# skip discover and cloud login pass
mock_cloud_instance.login.assert_called_once()
mock_cloud_instance.login.reset_mock()
mock_cloud_instance.download_lua.assert_called_once_with(
str(Path()),
mock_device["type"],
mock_device["sn"],
mock_device["model"],
)
mock_cloud_instance.download_lua.reset_mock()
mock_cloud_instance.download_plugin.assert_called_once_with(
str(Path()),
mock_device["type"],
mock_device["sn"],
)
mock_cloud_instance.download_plugin.reset_mock()

# test no host and no device_sn
self.namespace.host = None
self.namespace.device_sn = None
# result is None
await self.cli.download()

async def test_set_attribute(self) -> None:
"""Test set attribute."""
Expand Down

0 comments on commit 2ab6b67

Please sign in to comment.